Hooks: Deterministic Automation
Lifecycle-event hooks for guaranteed behaviors — formatters, gates, notifications — that prompting alone can't promise.
Advisory vs guaranteed
Everything in CLAUDE.md is advisory: an instruction the model reads and almost always follows — but "almost always" is doing real work in that sentence. For some behaviors, "almost always" isn't good enough. You don't want the formatter run on most edits. You want it run on every edit.
Hooks are the answer: user-defined shell commands that Claude Code executes automatically at fixed lifecycle points. The model doesn't decide whether a hook runs — the harness runs it, every time, deterministically. The rule of thumb: preferences go in CLAUDE.md; guarantees go in hooks.
The lifecycle events
| Event | Fires | Typical use |
|---|---|---|
SessionStart | New session begins | Inject env status or context |
UserPromptSubmit | You send a prompt | Add context, validate the request |
PreToolUse | Before a tool runs — can block it | Guardrails on dangerous commands |
PostToolUse | After a tool succeeds | Auto-format edited files |
PostToolUseFailure | After a tool fails | Custom error handling |
Notification | Claude needs your input | Desktop notification |
Stop | Claude is about to end its turn | Quality gates — block "done" until tests pass |
SubagentStop | A subagent finishes | Validate subagent output |
PreCompact | Before context compaction | Preserve critical state |
The exit-code protocol
A hook is a shell command, and its exit code is its vote:
- Exit 0 — proceed. For
UserPromptSubmitandSessionStarthooks, anything printed to stdout is added to Claude's context — a clean way to inject fresh information. - Exit 2 — block, and feed stderr back to Claude as feedback. The model reads why it was blocked and adjusts. A
PreToolUsehook exiting 2 stops the tool call itself. - JSON output — for finer control: explicit
deny/ask/allowdecisions, extra context injection, and per-event options.
That exit-2-with-stderr mechanism is the clever part: a hook isn't just a wall, it's a teacher. Block a command and say why, and Claude course-corrects in the same turn.
The canonical five
Auto-format after every edit. A PostToolUse hook matching Edit|Write runs Prettier on the changed file. Formatting stops being something Claude remembers — it's something that happens.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$FILE\"" }
]
}
]
}
}
Protect files. A PreToolUse hook that exits 2 when the target path matches migrations/ or .env, with stderr explaining the policy. Claude sees the explanation and works around the protected file instead of retrying it.
Guard rails on commands. A PreToolUse hook that exits 2 on any bash command containing rm -rf — a hard guarantee no prompt wording can bypass.
Notify when input is needed. A Notification hook fires a desktop notification, so long-running sessions don't silently stall waiting for you.
Audit logging. A PreToolUse hook appends every bash command to a log file — a complete record of what ran, independent of the transcript.
The Stop hook: "looks done" becomes "provably done"
The highest-leverage hook for quality: a Stop hook that runs your test suite and blocks the turn from ending until it passes. Without it, Claude finishes when the work looks done. With it, "done" is defined by a command exiting 0, and failures bounce straight back into the session for fixing. This mechanically closes the most common trust gap in agentic coding — work declared complete that doesn't actually pass.
There's also SessionStart with a compact matcher: after context compaction (when a long conversation is summarized to save space), critical context can get lost in summarization. This hook re-injects it deterministically.
Beyond shell commands
Not every rule is expressible as a deterministic script. "Block edits that remove error handling" needs judgment. For those, prompt-based and agent-based hooks exist — the hook consults a model to decide. Use them where mechanical rules can't reach, and keep plain shell hooks for everything else: deterministic, fast, free.
Practical tip: you don't hand-write hook configs. Ask Claude — "write a hook that runs eslint after every file edit" — and it produces the settings JSON. Review it, then enjoy behavior that no longer depends on anyone remembering anything.