Mastering Claude Code · ~7 min
Stop asking the model to remember the rule. Wire it into the lifecycle so the harness enforces it every single time — before the call, after the call, no sampling involved.
rm -rf"
— and it mostly listens. Mostly is the problem. A rule that holds 95% of the time is a rule you can't trust on
the run that matters. Hooks move the rule out of the prompt and into the harness, where compliance is 100% because the
model never gets a vote.A CLAUDE.md instruction is a suggestion the model may follow. A hook is a shell command the harness runs whether or not the model would have. That single difference — probabilistic versus deterministic — is what makes hooks the right tool for any rule where "usually" isn't good enough.
The guarantee is structural, not aspirational. Hooks are deterministic because the harness runs them, not the model. Claude Code invokes them at fixed points in the request loop — independent of any sampling decision the model makes.
Two fixed points anchor everything in this lesson. PreToolUse fires before the harness dispatches a
tool call — so it can still block it. PostToolUse fires after the result comes back — so it
can react and auto-fix, but the action has already happened. That ordering decides which job each event can do.
Hook input arrives on stdin as JSON — not environment variables — so scripts pipe stdin through jq
to read tool-input fields. A PreToolUse hook that exits 2 cancels the call and sends its stderr
message back to Claude as feedback. That makes exit 2 a reliable block for PreToolUse, PermissionRequest,
UserPromptSubmit, Stop, and config-change events.
A matcher string scopes the hook. "*" or empty matches everything; a plain name or pipe-list
("Bash", "Write|Edit") matches exactly; anything else parses as a regex. For PreToolUse/PostToolUse
the matcher filters on tool name — never the file path.
A PreToolUse blocker that substring-matches rm also blocks rm node_modules and even
echo "term". Those false positives push users to turn the hook off entirely — worse than
no guard. Match precisely, not loosely.
The canonical PostToolUse job is auto-formatting. Claude doesn't reformat to project style on its own, and the usual
fixes are all worse than infrastructure: prompting Claude to format burns tokens on a mechanical task, manual runs get forgotten,
and inconsistent agent code muddies every diff. A PostToolUse hook runs the formatter once after every write,
whether Claude or a human made the edit.
The event passes tool_input.file_path on stdin, so you format that file directly — no glob scan. Need
ordering (formatter then linter)? Chain them in one command; multiple handlers on the same event fire in
parallel, so they can't enforce a sequence on their own.
PostToolUse is the right event for formatting — the file must exist before a formatter can run.
But it cannot block the call; the write already happened. It is a side-effect hook, not a gate. To block a write,
use PreToolUse with a path check instead.One performance note: every PostToolUse hook runs synchronously on the hot path, so a slow formatter
slows every edit. For multi-second formatters set "async": true to run in the background — at the cost of losing
in-loop formatter errors. For black, prettier, gofmt the blocking default is right; they finish in milliseconds.
Hooks fail when the rule they encode is aspirational rather than absolute. The traps:
| Failure | What bites |
|---|---|
| Hidden global state | Hooks are invisible in the transcript unless they exit non-zero — a "misbehaving agent" is often a hook silently rewriting files. |
| Brittle schema coupling | Scripts parse tool_input; a renamed field in a new release makes them fail silently or block valid calls. |
| Oscillating edits | A formatter and a fix-on-save linter in one hook can each reverse the other's output. |
| CLAUDE.md would do | If the rule bends gracefully — prefer a style, avoid a phrase — a prompt fails softer and keeps the model in the loop. |
The deciding question is always the same: is this rule non-negotiable, or does it bend under load? Non-negotiable goes in a hook. Bendable goes in CLAUDE.md.
tool_input from stdin via jq, cancel bad calls before they run.Edit|Write matcher; it's a side-effect, it can't block.Retrieval practice — recall, don't peek
Question 1Hooks are deterministic because…
Question 2In a PreToolUse hook, exiting with code 2…
Question 3Auto-formatting belongs on PostToolUse, not PreToolUse, because…
Question 4A PreToolUse/PostToolUse matcher filters on…
Question 5 · spaced recall from an earlier lessonPlan Mode pays off because…
PostToolUse formatter for your stack? Next in Part 2: Permission Modes — tuning autonomy with
auto mode for speed and hard-deny for the floor.