Hooks are the closest thing Claude Code has to a programmable assistant. They run shell commands at specific moments — before a tool fires, after a file is edited, when a session starts or stops. If a behavior should happen every time, hooks are how.
Where they live
Hooks are configured in ~/.claude/settings.json (personal, applies to all projects) or .claude/settings.json (project, checked in via git). Same shape:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$tool_path\"" }
]
}
]
}
}
That hook formats any file Claude edits or writes, automatically.
The four hook patterns worth knowing
1. Auto-format on save
Stop nagging Claude about formatting. Let the tools do it.
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$tool_path\"" }
]
}]
The $tool_path env var holds whatever file Claude just touched. Pair with eslint, ruff, gofmt — whatever your team already runs on save.
2. Run tests on every meaningful change
When Claude finishes a code edit, kick off the relevant tests. Surface failures immediately so Claude can self-correct in the same turn.
"PostToolUse": [{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "npm test -- --findRelatedTests \"$tool_path\"" }
]
}]
Best paired with Jest's --findRelatedTests (or pytest's similar logic) so you only run the tests touched by the change.
3. Block dangerous commands in sensitive directories
Hooks can return non-zero to block a tool call. Use this for guardrails: prevent Claude from rm -rf in production directories, prevent migrations from running outside staging.
"PreToolUse": [{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "scripts/validate-bash-command.sh" }
]
}]
Your script reads $tool_command, exits 1 to block, prints to stderr to explain why. Claude reads the explanation and adjusts.
4. Notify when long jobs finish
Claude is happy to wait 8 minutes for a build. You aren't. A hook on Stop can ping you when a long-running session ends.
"Stop": [{
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Claude session done\" with title \"Claude Code\"'" }
]
}]
(Mac.) Linux: notify-send. Windows: a PowerShell toast. Or pipe to your own Slack/Discord webhook.
What to avoid
- Hooks that block silently. Always print to stderr explaining why you blocked. Otherwise Claude has to guess and the loop gets worse.
- Hooks that take more than a few seconds. They run synchronously and slow down every tool call. Long jobs belong in background processes you trigger from a hook, not the hook itself.
- Hooks that mutate state Claude is reasoning about. If your hook auto-fixes a file Claude is mid-edit on, you'll surprise Claude. Reformatting after the edit is fine; rewriting logic isn't.
Where to go next
- See 10 Claude Code slash commands —
/hookslists what's currently configured. - For a sharable hook config, drop one in
.claude/settings.jsonat your repo root and commit it. Now the whole team gets the same guardrails. - Combine with CLAUDE.md setup — hooks are enforcement; CLAUDE.md is intent. Both compound.