Hooks turn a wish into a guarantee
A CLAUDE.md line is a request the model usually honours. A hook is code the harness runs, so it happens every time. Choose hooks only for rules that have no exceptions.
The difference between asking and doing
Write "always run the formatter" in CLAUDE.md and Claude will, most of the time. Most of the time is not a guarantee, because the model reads the line and weighs it against everything else in its context.
A hook removes the weighing. It is your code, attached to a point in Claude Code's lifecycle, and the harness runs it whether or not the model remembers. The model is not asked. The program around the model does it.
That is the whole idea, and it follows from how the loop works: the harness, not the model, executes things.
The events
A hook attaches to an event. They split into two kinds.
Events that can block, using exit code 2, include PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact and ConfigChange. Stopping the action is the point of these.
Events that only observe include SessionStart, SessionEnd, PostToolUse, PostToolUseFailure, Notification, SubagentStart, CwdChanged, FileChanged and PostCompact. They run after something happened, so they can react but not undo.
Handlers come in five types: command, http, mcp_tool, prompt and agent. Only command and http are deterministic. A prompt or agent hook asks another model, so it is judgment again, just in a different place.
Two concrete examples
A formatter on every edit. A PostToolUse hook with the matcher Edit|Write runs your formatter after each file change. The rule "remember to format" is gone, because nothing needs remembering.
A type check before Claude may stop. A Stop hook running this command:
pnpm tsc --noEmit 1>&2 || exit 2If the check fails, the command exits with code 2, the turn is blocked, and the error text on stderr goes back to Claude. It keeps fixing until the check passes. The 1>&2 matters: stderr is the channel that reaches Claude.
$ add a rating field to the review form✻ Edits src/ReviewForm.tsx✻ Hook (Edit): formatter runs✻ Tries to finish✻ Hook (Stop): tsc fails, "Property 'rating' is missing in ReviewProps"✻ Edits src/types.ts✻ Tries to finish, hook passes# Claude never decided to type-check. The hook made finishing conditional on it.
A Stop hook exits with code 2 and writes an error to stderr. What happens?
Rules that have exceptions do not belong here
Hooks are rigid by design. That is their value and their cost. The official advice is to use them only for rules with zero exceptions and to keep preferences in CLAUDE.md. If you have forty hooks, most of them are probably preferences.
There are also limits that protect you from your own hooks. A Stop hook that blocks eight times in a row without a tool call in between is overridden and the turn ends. CLAUDE_CODE_STOP_HOOK_BLOCK_CAP raises that number.
Where they are configured
Hooks live in ~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json, managed settings, a plugin's hooks/hooks.json, and in skill or subagent frontmatter, where they are active only while that skill or subagent runs. Type /hooks to browse what is active.
A matcher of *, empty or omitted matches everything. Plain names separated by | match exactly, and any other character turns it into an unanchored JavaScript regular expression. To narrow a handler further, the if field takes permission-rule syntax such as Bash(git *).
Which rule is a good candidate for a hook?
Chapter complete
You've finished chapter 2 of 6 in Extending Claude Code.
A subagent works in its own context window and returns only a summary. That is the whole benefit, and it is also the whole limit, so it pays to know what comes back.
Next chapter