How hooks work
In the lifecycle of /debug, we saw how a hook interrupts at each stage. This chapter organizes the mechanism of hooks themselves (where they are registered, how, and how they act).
A hook is a script tied to an event
A hook is an executable script placed in ~/.claude/hooks/ (12 shell scripts at the time this chapter was drafted; 5 more were added in incident response immediately after, bringing the current count to 17 — see the closing :::info for the story). You register it in the hooks section of settings.json tied to an event. Note that the event system and the exit-code specification are provided by Claude Code itself (📦 built-in), while all scripts are the author's custom work (🔧).
"hooks": {
"Stop": [
{ "matcher": "", "hooks": [
{ "type": "command", "command": "~/.claude/hooks/check-plain-question.sh" },
{ "type": "command", "command": "~/.claude/hooks/check-unverified-claim.sh" }
]}
]
}
If you register multiple hooks against the same event, all of them run (in parallel by the official default). Specifying a matcher lets you narrow down by target tool name and the like (e.g., "Edit|Write").
The 8 events
In this environment, hooks are registered against the following 8 events. In terms of the Guides-and-Sensors cross-classification, Guides (before the action) and Sensors (after the action) line up split along the time axis.
| Event | Timing | Registered example | Role |
|---|---|---|---|
SessionStart | At session start | ensure-askuserquestion-loaded.sh / surface-subagent-results.sh | Preparation guidance (Guide) |
UserPromptSubmit | When an utterance is sent | promote-ultracode.sh / surface-salvage-on-resume.sh | Guiding heavy tasks, guidance on resume (Guide) |
PreToolUse | Just before a tool runs | check-protected-files.sh | Blocking dangerous operations (Guard) |
PostToolUse | Just after a tool runs | check-suppression.sh / save-subagent-result.sh / check-rate-limit-midturn.sh | Detecting cover-ups, immediate saving of artifacts (Sensor) |
PostToolUseFailure | Just after a tool fails | salvage-agent-failure.sh | Guidance on recovering the partial logs of a failed subagent |
Stop | Just before the response is finalized | check-plain-question.sh and others, 7 in total | Multi-stage inspection of output (Sensor) |
PreCompact | Before context compaction | precompact-snapshot.sh | Snapshotting state |
Notification | While awaiting permission or idle | say-notify.sh | Voice notification |
Immediately after the first draft of this chapter was written (later the same day), during the execution of a work turn, a 5-hour rate limit was hit head-on, and the investigation results a subagent had spent a long time producing vanished along with the entire session. As recurrence prevention, that same day we added save-subagent-result.sh / surface-subagent-results.sh / salvage-agent-failure.sh / surface-salvage-on-resume.sh / check-rate-limit-midturn.sh — 5 scripts — bringing the current count to 17 (the registration examples in the table earlier in this chapter are the current snapshot, including these 5). The recurrence-prevention design has two stages. The subagent's final report is saved to disk at the moment of completion (Sensor), and at the start or resume of the next session, a path is injected that lets you notice "there is a saved artifact" (Guide). Settling a failure in one go and turning it into a mechanism — a real example of how a harness grows.
ensure-askuserquestion-loaded.sh is the same type. Because the AskUserQuestion tool used to structure questions has a specification where its schema is lazily loaded, calling it while unloaded fails, which was inducing an escape to plain-prose questions. So at session start, guidance saying "load the schema first" is deterministically injected.
The exit code decides the behavior
A hook's action is decided by its exit code.
exit 0: No problem. It passes through without doing anything.exit 2: Either blocks the action or feeds back to the model. ForPreToolUseit blocks the tool execution, forStopit sends the response back, and forUserPromptSubmitthe prompt is discarded. ForPostToolUse, however, the tool has already run, so it cannot block — only the stderr content is fed back to the model.- Any other exit code: A non-blocking error. The first line of stderr is shown to the user, and execution continues as is.
The meaning of exit 2 changes slightly by event. For PreToolUse it stops that tool execution, and for Stop it sends the response back and makes it self-correct. For PostToolUse, since the tool has already executed, it cannot block — stderr is simply fed back to the next step. The "blocking writes to .env" and "sending back plain-prose questions" we saw in the lifecycle of /debug are both blocking forms of exit 2 (the former on PreToolUse, the latter on Stop).
echo "Detected: a plain-prose question pattern was found..." >&2 # the reason on stderr
echo "Action: present structured options with the AskUserQuestion tool." >&2
exit 2 # send back
Write "what the problem is and how to fix it" to stderr and exit 2. With this single move, the model can correct its own output.
A hook is not just a mechanism that crudely stops things. Many Sensors have the following safety devices.
- log-only mode: while the false positives cannot yet be read, it only records without blocking (
check-unverified-claim.shand others). It is promoted to blocking after measuring. - emergency override: disabling via an environment variable like
CLAUDE_HOOK_SKIP_PROTECTED=1was the original design (a later hands-on check found that in-sessionexportis not inherited by hook processes; a permanent opt-out requires exporting before launching the CLI, and one-shot bypasses work better as marker files). - self-loop prevention: a Stop hook looks at
stop_hook_activeand passes through on the second and later rounds of self-correction.
The key points are as follows.
- A hook is a script in
~/.claude/hooks/registered against an event insettings.json - In this environment, hooks are registered against 8 events (from SessionStart to Notification). Claude Code itself provides roughly 30 kinds of hook events as of 2026-06, and this environment uses a subset of them.
exit 0passes through;exit 2sends back (the stderr content becomes feedback to the model)- Safety devices such as log-only, override, and loop prevention are built in
Next, in How MCP works, we dig into the two-stage loading that reads tools in only when needed.