Hooks and the Hook Chain
A hook is a function that Claude Code calls when an event happens.
You register it with on.
It can watch the event, change it, or answer it.
The events themselves are on the page of their namespace. See the Mod API Overview for the map.
Remove the on call to turn a hook off. disableAllHooks stops installed mods.
Official pages: React to Events, Mods Reference, Mods Overview.
Example
This mod refuses every Bash command that contains rm -rf.
export function register(on) {
on('tool.call', { tool: 'Bash', command: /rm -rf/ }, async () => ({ deny: 'No rm -rf here.' }))
}
Entry Point and Registration
| Item | Status | What it does | How it works | Where it can be used |
|---|---|---|---|---|
register(on, options) | Documented | The entry point. The module exports it. on registers hooks. options holds the user values with defaults filled in. | Runs once when the module loads, and again after a reload. Its return value is dropped. A promise is awaited. $ does not exist yet. | The hooks module named in hooks/. |
on(pattern, hook) | Documented | Registers hook on the events that pattern selects. | pattern is an event name, a glob (*, classic.), or a negation (!tool.). A repeat registration throws. A plugin's registrations nest in order, first outermost. | Inside register only. |
on(pattern, matcher, hook) | Documented | The same, narrowed by a matcher. A matcher is a partial of the event input. | A leaf matches by === or by Reg. An array means any of. An object is a partial of an object. Example: { tool:. | Inside register only. |
on(... | Documented | Sets the handler that runs when the hook throws or overruns its budget. | A second . on one registration throws. So does one after register returned. An answer from the handler within the grace time stands as the hook result. | Inside register only, on the returned registration. |
Pattern * and negation | Documented | * selects every event. !x selects every event except x. | A telemetry event is not selected by * or by a negation for an installed plugin. Hook it by name or with telemetry.. A glob hook types e as the union of the selected events. | Inside register only. |
Arguments of a Hook
A hook is async ($, e, next) => result.
Hooks on turn.step and process.spawn are async generators.
| Item | Status | What it does | How it works | Where it can be used |
|---|---|---|---|---|
$ | Documented | The Mod API object. Frozen. Holds the core nouns and every noun that the engine. steps added. | Write each call in full, as in $.. | Every hook and every closure a hook drew. |
e | Documented | The event input. Deeply frozen plain data. | To change it, pass a copy to next, as in next({ .... A field marked pinned in the type is refused when changed. | Every hook. |
next(e) | Documented | The rest of the chain, as middleware. It runs the hooks beneath, then the engine. It resolves to the event result. | Called with no argument it rejects. A hook that returns without next ends the chain. Returning nothing is a failure. On a streaming event (turn., process.) it returns a Hook. | Every hook. |
next. | Documented | Continues the dispatch at a later tier (append, builtin, or core). The links between are skipped. | Managed hooks only. prepend may name append, builtin, or core. append may name core. No user hook skips the organization. | Mods in prepend or append. |
next. | Documented | An Abort that aborts when the dispatch is abandoned. | It aborts when the user interrupts, when a hook above settled first, or when this hook ran out of budget. Pass it to $. and other waits. | Every hook. |
next. | Documented | { plugin, tier } of who raised the dispatch. The engine is { plugin:. | Set by the host. A plugin cannot write it. Every hook of one dispatch sees the same origin. | Every hook. |
next. | Documented | A type predicate. It says whether this dispatch event is selected by pattern. | Narrows e for a hook on a glob or on *. Under a matcher the narrowing includes it. | Glob and * hooks. |
next. | Documented | The name of the dispatch event, as a value. | Use it in a glob hook to log or switch on. | Every hook. |
next. | Documented | A list of Trace: what settled beneath on the latest next() call. | One entry for each link beneath, nearest first. Empty before next is called. Frozen data. | Every hook. |
next. | Documented | { ms, remaining: the time limit of the hook and what is left. | Read fresh at each access. It stands still while a next or $ call is in flight. In a . handler it is the grace time. At session. it reads the exit bound, which never stops. | Every hook. |
next. | Differs | In a . handler only. Why the hook failed: kind, message, budget. | The docs add the kind re- (the event came from a call by that hook or by your mod's code outside its hooks) and next.. | . handlers. |
next. | Documented | In a . handler only. True when the hook had called next. | In a handler next is replay-safe. When called is true, next(e) returns what the last call settled to and runs nothing again. | . handlers. |
Failure Behavior
A hook that throws, overruns its budget, or returns a wrong shape is skipped.
The hooks beneath and the engine run in its place, or its last next result stands.
So a guard fails open unless its registration carries a .catch that refuses in its place.
Since 2.1.304, when a plugin that runs first skips one of your mod's hooks, the transcript shows a line naming the event and the plugin: its <event> hook was skipped. "<plugin>" runs first and sent the event on past it. (the program's text).
The type file gives this advice at each event where an answer can refuse.
| Event | Status | What happens when the hook fails | The advice |
|---|---|---|---|
tool. | Documented | The tool runs. | Add .. |
tool. | Documented | The verdict beneath stands. | Add a . that refuses. |
ui. | Documented | The hook is skipped. | None. |
ui. | Documented | A Client born in that redraw that fails is heard too, and the engine draws nothing again for it. | To go on, ask with $.. |
ui. | Documented | The focus stays where it was. | next(e) lands the focus on e. and redraws. |
agent. | Documented | The offer passes through. | None. |
agent. | Documented | The subagent starts. | Add .. |
prompt. | Documented | The prompt goes through. | Add .. |
prompt. | Documented | The file is read. | Add .. |
command. | Documented | The command runs. | Add .. |
command. | Documented | The description passes through. | None. |
config. | Documented | The row is written. | Add .. |
config. | Documented | The row passes through. | None. |
skill. | Documented | The text passes through. | None. |
attribution. | Documented | The text passes through. | None. |
plugin. | Documented | The module joins. | Add .. |
turn. | Documented | The answer stays as it was. | None. |
engine. | Documented | It fails, and its plugin unloads, when it replaces another plugin's noun, in user also past its own stub, or adds a seated name. | None. |
Budgets
| Item | Status | What it does | How it works | Where it can be used |
|---|---|---|---|---|
| Hook time per event | Documented | 10 seconds of the hook's own code. | The clock stops during next and $ calls, but a $. counts. Hook is 10000. A streaming hook counts its own code only, never a yield or a read beneath. | Every hook. |
prompt. hook time | Documented | The docs say 50 milliseconds. | Hook has one ms value. We could not find the 50 millisecond value in the type file. | prompt. hooks. |
. grace | Documented | 1 second. | Hook is 1000. The handler gets a fresh budget. | . handlers. |
| Linger after abort | Documented | 5 seconds. | Hook is 5000. Past it the hook is reported as lingering. The dispatch had gone on without it. | Every hook. |
session. hooks together | Documented | The Session hook budget, 1.5 seconds by default. | The whole chain shares one short wall-clock bound that next. reads. | session. hooks. |
The docs set the prompt.edit limit of 50 milliseconds. We did not check it in the program.