SLOPSHOPPER

my-mod

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

newbandguard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · my-mod
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(rm -rf build && git push --force origin main) ⎿ Denied by my-mod: Held by my-mod: 'rm -rf' deletes without asking ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM my-mod held 1 command(s). Last: 'rm -rf' deletes without asking ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
my-mod held 1 command(s). Last: 'rm -rf' deletes without asking
README

<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">

📚 What You'll Learn

Folder / FileFeature
CLAUDE.mdProject memory that persists across sessions
.claude/settings.jsonPermissions, 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.jsonMCP 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.

⌨️ Getting Around

  • Search with Ctrl/Cmd + K: a palette that jumps to any file or feature by title, path, or badge
  • Light and dark themes via a title-bar toggle that remembers your choice and follows your system preference
  • Copy link on any page to share a deep link straight to that entry
  • Arrow keys step through files in order, and the terminal panel runs a slash-command emulator (/help, /init, /doctor, and more)

🚀 Try It

The fastest way to get started is the live site:

👉 exploreclaudecode.com

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

🏗️ Project Structure

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.

🤝 Contributing

Contributions are welcome! Here are some areas where help would be great:

  • Content for new Claude Code features as they ship
  • Accessibility improvements (keyboard nav, screen readers, ARIA)
  • Mobile experience refinements
  • Translations into other languages

If you'd like to add or update educational content, the two places to look are:

  1. site/data/manifest.json for tree structure and metadata
  2. site/content/ for the actual markdown and config files

Feel free to open an issue if you have ideas or spot something that could be better.

⭐ Support

If you found this useful, consider giving it a star! It helps others discover the project.

📄 License

MIT

Source 2 files
hooks/my-mod.mjs 74 lines
1// 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}
74
types/index.d.ts 22 lines
1// 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