Holds risky shell commands and counts them in a band above the prompt

<img src="logo.png" alt="Explore Claude Code" width="360">
<strong>Learn Claude Code by exploring it.</strong>
<a href="https://exploreclaudecode.com"><img src="https://img.shields.io/badge/live%20demo-exploreclaudecode.com-c47a50?style=flat-square" alt="Live Demo"></a> <a href="https://github.com/LukeRenton/explore-claude-code/blob/main/LICENSE"><img src="https://img.shields.io/github/license/LukeRenton/explore-claude-code?style=flat-square&color=8e82ad" alt="License"></a> <a href="https://github.com/LukeRenton/explore-claude-code/stargazers"><img src="https://img.shields.io/github/stars/LukeRenton/explore-claude-code?style=flat-square&color=b8965e" alt="Stars"></a> <img src="https://img.shields.io/badge/zero%20dependencies-vanilla%20JS-3a3632?style=flat-square" alt="Zero Dependencies">
A simulated Claude Code project you can click through. Every file and folder in the sidebar is a real Claude Code concept — the same .claude/ directory, config files, and scaffolding you'd find in an actual repo. Click any file to learn what it does, how to set it up, and see annotated examples you can copy into your own projects.
<img src="current.png" alt="Screenshot" width="820">
| Folder / File | Feature |
|---|---|
CLAUDE.md | Project memory that persists across sessions |
.claude/settings.json | Permissions, tool access, and guardrails |
.claude/rules/ | Contextual conventions scoped to specific file types |
.claude/commands/ | Custom slash commands for saved workflows |
.claude/skills/ | Knowledge folders Claude loads autonomously |
.claude/agents/ | Subagents for specialised, delegated tasks |
.claude/hooks/ | Shell scripts that run on Claude lifecycle events |
.claude/plugins/ | Extend Claude with custom tools and resources |
.claude/mods/ | JavaScript hooks that run inside your session and draw custom UI |
.mcp.json | MCP server config for external tool integrations |
src/ | Example source code sitting alongside real config |
| built-in/ | Features that ship with Claude Code (no setup required) |
built-in/bundled-skills/ | /simplify, /batch, /code-review, /run, /plugin-authoring, and more |
The explorer is split into two sections. Everything under .claude/ is project config you create and commit. Everything under built-in/ covers features that ship with Claude Code out of the box, no setup required. A visual separator divides the two.
Every piece of content is written as if it were a real config file in a real repo. You're not reading about the config, you're reading the config itself, annotated so you understand every line. When you're done exploring, you can copy the scaffolding straight into your own projects.
Ctrl/Cmd + K: a palette that jumps to any file or feature by title, path, or badge/help, /init, /doctor, and more)The fastest way to get started is the live site:
No install, no signup, no build step. Just open it and start clicking.
If you want to run it locally, clone the repo and point any static server at the site/ directory:
git clone https://github.com/LukeRenton/explore-claude-code.git
cd explore-claude-code
npx serve site
# or
python -m http.server -d site 8080
# or just open site/index.html directly in your browser
The entire site is static HTML, CSS, and vanilla JavaScript. Zero build steps, zero frameworks, zero bundlers.
explore-claude-code/
├── site/
│ ├── index.html # Single-page app entry point
│ ├── data/
│ │ └── manifest.json # Drives the entire UI (tree, content, badges, features)
│ ├── content/ # Source markdown and config files
│ ├── js/
│ │ ├── app.js # Main controller, routing, keyboard nav, theme
│ │ ├── file-explorer.js # Sidebar tree with animated canvas connectors
│ │ ├── content-loader.js # Custom markdown parser and renderer
│ │ ├── terminal.js # Interactive terminal panel
│ │ ├── search.js # Cmd/Ctrl+K search palette
│ │ ├── progress.js # Feature completion tracking (localStorage)
│ │ └── icons.js # Hand-crafted SVG icon library
│ └── css/ # Variables, layout, components, syntax, terminal, search, void
├── logo.png
└── README.md
All educational content is stored in site/data/manifest.json and the source files in site/content/. The manifest is the single source of truth for the tree structure, badges, feature groupings, and content references. To add or change content, that's where you go.
Contributions are welcome! Here are some areas where help would be great:
If you'd like to add or update educational content, the two places to look are:
site/data/manifest.json for tree structure and metadatasite/content/ for the actual markdown and config filesFeel free to open an issue if you have ideas or spot something that could be better.
If you found this useful, consider giving it a star! It helps others discover the project.
hooks/my-mod.mjs 74 lines1// hooks/my-mod.mjs
2//
3// A mod that does two things:
4// 1. Holds risky Bash commands (rm -rf, force pushes) before they run
5// 2. Shows a band above the prompt counting how many it has held
6//
7// Compare it with .claude/hooks/my-hook/my-hook.sh. Same guard, but
8// this one remembers what it did and draws it on screen.
9//
10// Every hook has the same shape:
11// on(event, matcher?, async ($, e, next) => { ... })
12//
13// $ the mods API (session, state, ui, process, fs, http, ...)
14// e the event (here: the tool call, or the component drawing)
15// next hands the event down the chain to Claude Code
16
17const RISKY = [
18 { pattern: /\brm\s+-rf\b/, reason: "'rm -rf' deletes without asking" },
19 { pattern: /\bgit\s+push\b.*--force\b/, reason: "force-pushing rewrites shared history" },
20];
21
22// State lives in the host, not in this file. Module-level variables
23// reset every time you save (hot reload). $.state survives.
24const held = { plugin: "my-mod", key: "held" };
25
26export function register(on) {
27 // ── Answer: deny risky commands ─────────────────────────────────
28 // The matcher means this hook only sees Bash calls.
29 on("tool.call", { tool: "Bash" }, async ($, e, next) => {
30 const command = String(e.command ?? "");
31 const hit = RISKY.find((r) => r.pattern.test(command));
32
33 // Not risky: pass it on untouched.
34 if (!hit) return next(e);
35
36 // Risky: record it, then answer without calling next().
37 // Claude Code never runs the command, and Claude sees the reason.
38 const { value: list = [] } = await $.state.get(held);
39 await $.state.set(held, [...list, { command, reason: hit.reason }]);
40
41 return { deny: `Held by my-mod: ${hit.reason}` };
42 });
43
44 // ── Draw: a band above the prompt ───────────────────────────────
45 // Reading $.state here subscribes this render to it, so the band
46 // redraws by itself whenever the tool.call hook adds an entry.
47 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
48 const { value: list = [] } = await $.state.get(held);
49
50 // Step aside if there is nothing to show, or a survey owns the spot.
51 if (list.length === 0 || e.props.hasSurvey) return next(e);
52
53 const { Box, Text } = $.ui.resolve(e);
54 const last = list[list.length - 1];
55
56 return Box({
57 paddingX: 1,
58 children: [
59 Text({ children: `my-mod held ${list.length} command(s). Last: ${last.reason}` }),
60 ],
61 });
62 });
63
64 // ── Observe vs. rewrite ─────────────────────────────────────────
65 // The other two moves look like this:
66 //
67 // Observe: let it through, then look at the result
68 // const result = await next(e);
69 // return result;
70 //
71 // Rewrite: change what downstream hooks and Claude Code see
72 // return next({ ...e, command: saferVersion(e.command) });
73}
74types/index.d.ts 22 lines1// types/index.d.ts: The mod's type contract
2//
3// Declares every $.state key this mod reads or writes, so editors
4// can type-check $.state.get / $.state.set calls and
5// `claude plugin validate` can confirm the mod uses what it declares.
6//
7// plugin.json points here with: "types": "./types/index.d.ts"
8//
9// Claude Code also generates its own API types into
10// .claude-plugin/types/. Don't edit those; edit this file.
11
12declare module "claude-code" {
13 interface PluginState {
14 "my-mod": {
15 // Commands the tool.call hook refused to run, oldest first.
16 held: { command: string; reason: string }[];
17 };
18 }
19}
20
21export {};
22