Claude Code Hooks: auto-format, block dangerous commands, enforce tests — no reminders needed
TL;DR
A hook is a shell command (or HTTP, prompt…) that runs on events like PreToolUse, PostToolUse, Stop. Exit code 2 in PreToolUse blocks the tool call and feeds the reason to Claude; in Stop it forces Claude to keep going. Configure in .claude/settings.json, inspect with /hooks.
You can tell Claude Code in CLAUDE.md to “always format code after editing”, “never run rm -rf”, “run the tests before saying you’re done”. The model will comply… most of the time. Hooks turn those reminders into guarantees: a shell command (or HTTP call, prompt or agent) that runs automatically at a specific point in Claude Code’s lifecycle and can block an action or force Claude to keep working.
How hooks work
A hook has three parts:
- Event: when it runs, e.g.
PreToolUse(before a tool is used) orStop(when Claude wants to stop). - Matcher: a filter, e.g. only the
Bashtool, orEdit|Write. - Handler: what to do — run a shell command, call an HTTP endpoint, call an MCP tool, ask a model (prompt) or spawn a checking subagent.
Claude Code sends the event data as JSON on the hook’s stdin (session_id, cwd, tool_name, tool_input…). The hook answers with an exit code or JSON on stdout.
Commonly used events
| Event | Runs when | Can block? |
|---|---|---|
SessionStart | A session starts/resumes | No — used to add context |
UserPromptSubmit | You submit a prompt | Yes |
PreToolUse | Before Claude uses a tool | Yes — allow, deny, ask, or rewrite input |
PostToolUse | After a tool finishes | No (it already ran) — but can send feedback |
Notification | Claude Code sends a notification (e.g. awaiting approval) | No |
Stop / SubagentStop | Claude / a subagent is about to end its turn | Yes — keeps it working |
PreCompact | Before conversation compaction | Yes |
TaskCompleted, TeammateIdle | Tasks / teammates in agent teams | Yes |
SessionEnd | The session ends | No |
There are many more, such as ConfigChange, FileChanged, PermissionRequest and PreModelSwitch — type /hooks in Claude Code to see configured hooks and where they come from.
Exit codes — the part that matters most
| Exit code | Meaning |
|---|---|
| 0 | Success. If stdout is a single JSON object, its fields are applied. |
| 2 | Blocking error (for events that can block). Stderr is fed back to Claude as the reason. |
| Other (e.g. 1) | Non-blocking error: the action proceeds and you only see a “hook error” notice. |
The most common beginner mistake: using exit 1 to block — it doesn’t block. Mandatory rules need exit 2. For Stop, exit 2 means “you’re not done, keep going”.
Where to configure them
| Location | Scope |
|---|---|
~/.claude/settings.json | All projects on your machine |
.claude/settings.json | Project — commit it for the whole team |
.claude/settings.local.json | Project, your machine only (gitignored) |
| Managed settings | Organisation-wide |
| Plugins, skill/subagent frontmatter | While the plugin is enabled / while the skill or subagent runs |
Example 1: Auto-format after every edit
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}]
}]
}
}
The hook reads JSON from stdin, extracts the edited file path and runs Prettier. Claude no longer needs to remember to format.
Example 2: Block rm
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}]
}]
}
}
#!/bin/bash
# .claude/hooks/block-rm.sh (remember chmod +x)
echo "Deleting files with rm is not allowed. Ask the user first." >&2
exit 2
The if field uses permission-rule syntax (Bash(git *), Edit(*.ts)) to narrow the matcher. Note that if is best-effort; for a hard block, use a permission deny rule rather than relying on a hook alone.
Example 3: Don’t let Claude stop while tests are red
#!/bin/bash
# Hook for the Stop event
if ! npm test --silent > /tmp/test.log 2>&1; then
echo "Tests are still failing. Read /tmp/test.log and fix them before finishing." >&2
exit 2
fi
exit 0
This turns your “Definition of Done” into a rule a machine can check.
Example 4: Deny with a reason via JSON
Instead of exit 2, a PreToolUse hook can print JSON for finer control:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Direct writes to the production database are not allowed"
}
}
permissionDecision accepts allow, deny, ask or defer, optionally with updatedInput to modify parameters before execution. The JSON must be the only thing on stdout — a greeting from your shell profile is enough to break parsing.
Other handler types
http: POSTs the event JSON to a URL — handy for central logging or alerts.mcp_tool: calls a tool on a configured MCP server.prompt: asks a model for a quick single-turn judgement (30-second default timeout).agent: spawns a subagent with Read/Grep/Glob to verify conditions (experimental).
Command hooks time out after 600 seconds by default. $CLAUDE_PROJECT_DIR points to the project root — use it to call scripts by absolute path.
Security and control
- Hooks run arbitrary commands with your permissions — review hooks shipped with plugins, skills and subagents before enabling them.
- Project hooks follow the workspace-trust rules.
- Organisations can use
allowManagedHooksOnlyto allow only admin-provided hooks. - Temporarily disable all hooks with
"disableAllHooks": true. - Debugging:
claude --debugor/debuglogs full hook stderr and output.
How to apply it
- Developers: Start with two cheap, high-value hooks: format after edits, and block
rm/git push --force. - Tech Leads: Put hooks in the repo’s
.claude/settings.jsonand review them like CI config. AStophook that runs tests and lint is the cheapest way to cut “AI says done but the build is red” PRs. - PMs: Every rule “the team keeps forgetting” (update the changelog, never touch old migrations…) is a hook candidate. Automate the checklist instead of repeating it in every review.
Sources & references
- Hooks reference — Claude Code Docs