Hooks: the rules my agent can't talk its way out of.
Ten published configs, three production saves, three expensive failures. Configs verified against the official hooks reference on 2026-06-10.
Jump to section tap to open
The 30-second answer
Claude Code hooks are shell commands (or HTTP endpoints, or short LLM prompts) that Claude Code runs automatically at lifecycle events — PreToolUse, PostToolUse, Stop, SessionStart. They live in settings.json, not the chat, so the model can't skip or forget them. CLAUDE.md is preference; hooks are enforcement. Most operators need three events and about five hooks.
What is a Claude Code hook?
A hook is a command that fires at a lifecycle event, outside the model's control. The official event list has grown to roughly thirty — SubagentStop, PreCompact, WorktreeCreate, the works — but three cover most operator needs:
- PreToolUse — fires before any tool call. Validate, block, audit-log.
- PostToolUse — fires after. Format, test, notify.
- Stop — fires when the agent thinks it's done. Your script gets the last word.
They live in two places: ~/.claude/settings.json (global — every session on your machine) or <repo>/.claude/settings.json (committed — the whole team inherits the guardrails). The full mental model is in Chapter 16; this page is the reference card.
The line that earns the page: I typed "please run prettier on this file" 47 times in one week. Then I wrote one PostToolUse hook. I haven't typed it since (Ch 16).
What does a hook config actually look like?
A matcher (which tool, regex'd) plus a command. The contract — verified against the official hooks reference on 2026-06-10 — is stricter than it looks:
- Exit 0 — success; stdout is parsed for JSON output fields.
- Exit 2 — blocking error; stderr is fed back into the model's context, so the agent reads the reason and adapts.
- Exit 1 (and everything else) — a non-blocking error. The action proceeds.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'git push origin main'; then echo 'Blocked: push to main requires a human.' >&2; exit 1; fi"
}
]
}
]
}
}
Which hooks should you actually run?
All ten configs published across the Playbook, one verdict each. The five team hooks are in Ch 16; the five operator scripts live on /resources as copy-paste blocks.
| Hook | Event | What it does | Verdict |
|---|---|---|---|
| format-on-save | PostToolUse | prettier on every Write|Edit | keep — killed the 47×/week nag |
| test-on-write | PostToolUse | run the matching test file | keep on small trees; see the 15k-file failure below |
| block-push-to-main | PreToolUse | greps tool input for git push origin main | keep, everywhere — three "oh no" saves at Belkins (Ch 16) |
| commit-message-template | PreToolUse | enforce commit format | situational — teams yes, solo it's friction |
| slack-notify-on-long-task | Stop | ping when a long run ends | situational — only if you actually walk away |
| secrets-scan | PreToolUse Write|Edit | 8 live-key patterns, blocks the write | keep, everywhere — it has fired on a real Anthropic key |
| claude-md-size-guard | SessionStart | warn past 300 lines | keep — a 340-line CLAUDE.md doubled a token bill in a week |
| prompt-cache-guard | UserPromptSubmit | warn before blowing a warm cache | situational — matters at API spend, noise on Max |
| agent-watchdog-stall-detect | PreToolUse Task | JSONL log of agent spawns | keep if you run swarms; it's telemetry, not a gate |
| commit-msg-enforcer | Stop | lint the final commit message | situational — warn-only, never block on style |
What did these hooks catch in production?
- block-push-to-main: three "oh no" moments at Belkins in a year — each one a force of habit the model talked itself into, stopped by a grep (Ch 16).
- format-on-save: "please run prettier" went from 47 times a week to zero.
- secrets-scan: caught an Anthropic key a subagent helpfully "moved into the example" — five seconds of grep versus rotating a key across four projects (/resources). Its documented blind spot: it only scans the tool-input payload; content passed another way is silently allowed.
- claude-md-size-guard: a 340-line CLAUDE.md was doubling the token bill — the file reads on every turn. Warn-only at 300 lines.
Which hook ideas failed?
The section the official docs will never have: my own cleverness, priced.
The keystroke storm. An on-save syntax-check hook met a 1.5-second autosave. Each check spawned a 6–8-second subagent; by minute three there were 90 running concurrently. Hooks have no rate limiter unless you build one in. The fix was two lines of discipline: a 30-second per-file debounce, plus a ~/.claude/hooks/disabled kill-switch file every hook checks first (Ch 28).
The typecheck that never finished. A typecheck-on-every-edit hook on a 15,000-file tree ran for months and never completed once — its output piped through a truncator that always exited clean. Maximum cost, zero benefit, zero errors. Found only by the self-audit, not by any alert.
The 2× evaluator. Forcing a /goal evaluator up to Opus via a custom hook config: a 47-turn session at roughly twice the expected cost, for judgment Haiku was already delivering (Ch 38).
How do hooks relate to /goal and /loop?
A Stop hook is the loop primitive: the agent thinks it's done, your script disagrees, the agent keeps going. /goal is literally a wrapper around a prompt-based Stop hook with a Haiku evaluator reading the transcript (Ch 38). Two consequences worth knowing: enterprises that lock hooks (disableAllHooks, allowManagedHooksOnly) lose /goal with them; and if you can measure "done" with code, skip the evaluator entirely — a real script returning the verdict is more honest than a Haiku judging a feeling. Determinism beats vibes when the vibes can cost $11.
How do you stop hooks from rotting?
- Telemetry before deletion. A one-line JSONL invocation logger per hook. "Zero fires in seven weeks, across two independent telemetry sources" is kill evidence; a feeling isn't (/self-audit).
- Warn-only for heuristics. Anything that can false-positive (size guards, style lints) warns; only deterministic policy (push-to-main, live keys) blocks with exit 2.
- Never break the parent agent because logging broke. Every script ends in a way that degrades to exit 0 when its own plumbing fails.
- Quarterly re-verify against the official reference. The exit-code semantics under this page's own Ch 16 examples moved once already. Re-stamp the verified date or assume drift.
Want the next hook to write? List the three corrections you've typed more than five times this month — those are your next three hooks (Ch 17).
#!/usr/bin/env bash
# secrets-scan: block writes containing live API keys
# PreToolUse on Write|Edit — fires before the file lands on disk
#
# Failure mode: only scans the new content payload from $CLAUDE_TOOL_INPUT.
# If Claude passes content via stdin instead, this hook sees nothing and
# silently allows. Verify with the test command below after any harness update.
set -euo pipefail
payload="${CLAUDE_TOOL_INPUT:-}"
[ -z "$payload" ] && exit 0
# Real-money patterns. Order: most specific first so error message is useful.
patterns=(
'sk-ant-api03-[A-Za-z0-9_-]{20,}' # Anthropic
'sk-proj-[A-Za-z0-9_-]{20,}' # OpenAI project
'sk-[A-Za-z0-9]{32,}' # OpenAI legacy / generic
'rk_live_[A-Za-z0-9]{20,}' # Stripe restricted live
'sk_live_[A-Za-z0-9]{20,}' # Stripe secret live
'AKIA[0-9A-Z]{16}' # AWS access key
'ghp_[A-Za-z0-9]{36}' # GitHub PAT
'(password|passwd|secret)[[:space:]]*=[[:space:]]*["'"'"'][^"'"'"']{8,}'
)
for p in "${patterns[@]}"; do
if echo "$payload" | grep -Eq "$p"; then
echo "secrets-scan: blocked write — matched pattern /$p/" >&2
echo "secrets-scan: move the value to .env.local + add to .gitignore" >&2
exit 2
fi
done
exit 0
# Wire in ~/.claude/settings.json:
# { "hooks": { "PreToolUse": [
# { "matcher": "Write|Edit", "hooks": [
# { "type": "command", "command": "/Users/vlad/.claude/hooks/secrets-scan.sh" }
# ]}
# ]}}
FAQ
What are hooks in Claude Code?
Shell commands, HTTP endpoints, or short LLM prompts that run automatically at lifecycle events. Defined in settings.json — the model can't skip them.
Where do Claude Code hooks go?
~/.claude/settings.json for global, <repo>/.claude/settings.json committed for the team.
How do I block Claude Code from pushing to main?
A PreToolUse hook on Bash that greps the tool input and exits 2 — the stderr message goes back into the model's context, so it adapts instead of retrying.
Can a hook stop Claude Code from leaking secrets?
Yes — PreToolUse on Write|Edit scanning live-key patterns. It only sees the tool-input payload; re-verify the blind spot after harness updates.
What's the difference between a Stop hook and /goal?
/goal wraps a prompt-based Stop hook with a Haiku evaluator. A raw Stop hook is deterministic — if you can measure "done" with code, use the hook.
Related: best practices · the full copy-paste vault · command cheat sheet · Ch 16: Hooks and Custom Subagents