A clean module for comparison

Read it on the web: robgruhl.github.io/claude-mod-learnings
Claude Code mods are TypeScript or JavaScript plugins that run inside Claude Code and hook its own event chain: tool calls, prompts, model requests, subagents and what the interface draws. They shipped on 1 October 2026 in v2.1.287. This repo teaches them from an explain-like-I'm-five version up to policy engineering, backs the claims with experiments we ran, and includes a facilitated tutorial you run in Claude Code.
It's the sister site of Decision Tier (repo), a field guide to decision models. The two meet on Mods meet the cascade. This is an independent study, not an Anthropic project. Everything was checked against Claude Code 2.1.296 on 2026-10-12. The API is early access, so trust the declarations your own build writes over anything here.
| Path | What it is |
|---|---|
site/pages/ | The page fragments: ELI5, 101, 201, 301, four architecture pages, three evidence pages and the tutorial page |
site/build.py, site/template.html | Standard-library build that wraps each fragment and writes docs/, which GitHub Pages serves. Run python3 site/build.py. |
experiments/ | Small mods that each test one claim, run with claude plugin test (and one live headless probe). Results in experiments/RESULTS.md. Also cascade-guard, a tiered Bash guard. |
tutorial/ | The Mod Skeleton, a facilitated tutorial. cd tutorial && claude and Claude becomes your facilitator: one module at a time, predict before each check, every check run for real, one commit per module. Modelled on walkingskeleton.org. |
tutorial/.reference/skeleton-guard/ | The tutorial's finished mod, validated and tested on 2.1.296 |
hooks/hooks.json names one module exporting register(on, options). Every hook is ($, e, next).return next(e)), rewrite (next({ ...e, … })) or answer without calling next, so nothing beneath it runs..catch to fail closed. We confirmed both cases on 2.1.296.claude plugin validate on a mod before you install it and read its calls: line.$, which is why the loader can list a mod's calls. It refuses a module that hides $ in a destructure, an alias or a helper it can't follow.$.state until /clear, and $.store across sessions. session.start doesn't fire again after /clear, so reload from the store in classic.SessionStart.claude plugin test your test's hooks sit beneath the mod and stand in for Claude Code, so answer every event.tool.call guard doesn't see @-mentioned files. Hook prompt.mention too.setTimeout and setInterval exist in the hooks runtime, though the docs say there are no timer globals. Waiting on one counts against a hook's budget..catch as kind: 'throw'. If the handler also answers in the wrong shape, the guard fails open anyway.claude- prefix: anthropic-, anthropics- and cc-plugin- prefixes, plus exact names like claude-code.Details and outputs are in experiments/RESULTS.md.
Text and illustrations © Rob Gruhl, CC BY 4.0. Code (the build, experiments, tutorial reference mod and code samples) is MIT; see LICENSE. Illustrations were made with gpt-image-2, and the site was built with Claude Code.
hooks/register.ts 15 lines1import type { EngineInterface, Register } from 'claude-code'
2
3async function note($: EngineInterface, text: string) {
4 await $.ui.toast(text)
5}
6
7export const register: Register = on => {
8 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
9 await note($, e.command)
10 const home = await $.env.get('HOME')
11 await $.store.set('last', { command: e.command, home })
12 return next(e)
13 })
14}
15