Claude Code hooks: enforce quality automatically
Instruction files like CLAUDE.md tell an agent what to do. Hooks make parts of it non-optional: shell commands the harness runs around tool calls, able to observe, augment, or block them. The agent can't forget what the harness enforces — and neither can you.
How hooks work
Hooks are configured in .claude/settings.json under a hooks key, grouped by event. The two you'll use most:
- PreToolUse — runs before a tool call (an edit, a shell command). Exit code
2blocks the call, and whatever the hook wrote to stderr is shown to the agent as feedback. - PostToolUse — runs after a tool call succeeds. Ideal for formatting and cleanup.
Each hook receives a JSON payload on stdin describing the tool call (tool name, file path, command, new content). A matcher limits which tools trigger it:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format.sh" }]
}]
}
}
The three hooks worth having everywhere
- Auto-format on edit (PostToolUse on Edit/Write): read the edited file's path from the payload, run the right formatter for its extension. Style debates disappear; every agent edit lands formatted.
- Test-gate before commit (PreToolUse on Bash): when the command is a
git commit, run the project's tests first; on failure, exit 2 with the failing output on stderr. The agent sees which tests failed and fixes the cause instead of committing red. - Secret scan on write (PreToolUse on Edit/Write): grep the new content for key patterns (AWS ids, private key blocks, tokens); exit 2 with the matched pattern named. Secrets stop at the door instead of in the git history.
Why exit code 2 is the whole trick
A blocked call isn't a dead end — it's a conversation. Because stderr is fed back to the agent, a good hook writes an actionable message: "Commit blocked: tests failing (ran: npm test). Last 40 lines: …". The agent reads it, fixes the actual problem, and retries. Bad hooks block silently; good hooks teach.
Pitfalls
- Hooks load at session start — restart the session (or check
/hooks) after changing settings. - Parse the stdin payload with a real JSON parser, not grep — paths and commands contain every character you don't expect.
- Keep PostToolUse hooks fast and always-zero-exit; formatting should never block work.
- Give escape hatches: an
allow-secretmarker comment for deliberate examples beats a hook people disable.