Building mods.
A mod is a plugin with a hooks module. You write one async function per event, and each one wraps the ones below it. Claude Code ships the runtime and writes type declarations for it, but documents almost none of it. This guide covers what's there, what it costs you, and where it behaves differently from what the types suggest.
Up to date for Claude Code 2.1.286. I read everything here out of the declarations Claude Code writes about itself, then ran it. When a page says something behaves a certain way, the command that showed it is on the page.
If you haven't written a mod before, read these in order. The first three explain how the whole thing works and everything else builds on them. After that, jump to whatever you need.
- The model: read these first
-
01
What a mod is
What a mod is, which builds load one, what changes on Bedrock and Vertex, and a mod you can run in four files.
9 sections
- On by default since 2.1.287
- Bedrock, Vertex and other providers
- It changes without notice
- Claude Code's own features are mods
- The fold
- Where the code runs
- These aren't the settings hooks
- The smallest mod that does something
- What's here and what isn't
-
02
Anatomy of a mod
plugin.json, hooks.json and register(), and what each of them is allowed to say.
11 sections
- The two manifests
- register, and the argument nobody uses
- The import line
- A mod is a closure
- Registering an event twice
- Three ways to name an event
- Matchers
- The loader reads your source, and it's strict
- Getting $ out of a hook without breaking the rules
- Check it before you run it
- Running it
-
03
Tiers, order and next()
The five tiers, how a hook wraps the ones below it, and everything a hook can do with next().
12 sections
- The five tiers
- next
- The two axioms
- next.to
- next.origin
- next.trace
- next.signal and next.budget
- What failure actually does
- .catch
- Serial, and why
- Streaming
- Recursion
- Out of order, once you have the model
-
04
The engine interface
Every noun and verb on $, what's new since 2.1.283, and the calls with surprises in them.
12 sections
- Every verb is also an event
- The nouns
- What's new since 2.1.283
- What arrived between 2.1.272 and 2.1.283
- $.plugin isn't a capability
- $.telemetry is for the built-ins
- The ops with surprises in them
- $ outlives its dispatch
- engine.create builds your $
- Telling TypeScript about a new noun
- A missing noun throws
- The names moved once already
-
05
The event catalogue
Every event you can hook, what each one carries, and how a matcher narrows it.
7 sections
- Engine events
- Operation events
- classic.\*
- Globs
- Events you can ask to rerun
- What still has no event
- Why use hooks at all
-
06
Drawing on the screen
Panes, components, elements per surface, and how to redraw without making the terminal flicker.
9 sections
- ui.render
- $.ui.resolve
- The fifteen components
- Elements, and the one that quietly draws nothing
- Opening a pane
- Interaction
- Client modules
- Redrawing
- The smaller screen verbs
-
07
userConfig and options
Declaring options in plugin.json, what register() gets, and the three ways a config stops your mod loading.
6 sections
- Declaring the fields
- Three ways this goes wrong
- Configuring one
- What arrives at runtime
- $.config is the /config menu
- Reading the person's actual settings
-
08
Testing a mod
The built-in test kit: describe, expect, mock, test and tier, plus drawing your UI in a test.
12 sections
- The five exports
- tier, once, at the top
- A test
- Other plugins in the chain
- Things about the test's $
- Testing what you draw
- mock
- Matchers
- What the kit doesn't have
- Types in your editor
- Validate first
- What I haven't checked
-
09
Confirmed gotchas
Things that cost me an hour each, all reproduced on a real build, with the output that proves it.
11 sections
- 1. A hook that fails doesn't block. It disappears.
- 2. A good deny reason can make the model go around you
- 3. A tool.call hook breaks worktree isolation for subagents
- 4. $.session.usage() is mostly empty until the first turn completes
- 5. The manifest validator doesn't type-check your defaults
- 6. An unconfigured option reads as "", not undefined
- 7. The capability scan checks shape, not spelling
- 8. Names change without warning
- 9. Small things that each cost an hour
- 10. `prompt.compose` is asked every turn, but only its first answer is sent
- What I couldn't reproduce, and what's fixed
-
10
Recipes
Nine short patterns you can paste and adapt, each one run at 2.1.286.
10 sections
- 1. A guard that fails closed
- 2. A redactor on the way back up
- 3. Keeping the engine for the rest of the session
- 4. Watching tool calls without getting in the way
- 5. A status line that keeps itself current
- 6. Adding a noun other mods can use
- 7. A pane
- 8. A command, and coping when it's refused
- 9. A hook that only wants a say
- Where to read real ones
No page or section matches that. The filter reads each page's title, its summary and its section names.
What this is not
This isn't Anthropic's documentation and it doesn't speak for them. Theirs went up with 2.1.287, starting at Mods overview, and it's the contract. Read it first. This guide is what the build does around that contract, the traps I hit, and the places the two disagree, each with the output from a real run. Mods in installed plugins load by default from 2.1.287 unless Anthropic switches them off remotely, and CLAUDE_CODE_ENABLE_FUNCTION_HOOKS does nothing any more. The declarations are theirs, and any of it can change in the next release without a word. Error strings and types quoted here come from 2.1.286 unless a page says otherwise.
It doesn't cover every field of every event either. The declarations run to hundreds of kilobytes, and a lot of that is tool inputs you already know. What's here is the part you need and the part that'll surprise you.