Originally published at claudeguide.io/claude-code-hooks-deep-dive
Claude Code hooks intercept tool calls and lifecycle events so you can run shell scripts before or after the agent acts. Six event types fire at predictable moments — PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit, and Notification — each respecting exit-code semantics (exit 0 = continue, exit 2 = block) in 2026. Below are eight production-tested patterns that go well beyond the bash-formatter examples in the official docs: destructive-command blocking, auto-formatting, prompt rewriting, cost guardrails, test triggers, git stash safety nets, and richer session bootstrap.
The 6 Hook Event Types
Hooks live in ~/.claude/settings.json (user) or .claude/settings.json (project). Each event accepts a matcher (tool name pattern) and a list of commands to run.
| Event | Fires when | Exit-code 2 effect |
|---|---|---|
PreToolUse |
Before any tool call | Cancels the tool call; stderr returned to Claude |
PostToolUse |
After tool call succeeds | Output appended as context (cannot undo) |
UserPromptSubmit |
After you press enter, before Claude sees prompt | Replaces or augments the prompt |
SessionStart |
New session or claude --resume
|
Output injected as initial context |
Stop |
Claude finishes responding | Output shown but does not block |
Notification |
Claude is idle, awaiting input | Side-effect only (sound, banner, etc.) |
The hook receives JSON on stdin (tool name, args, working directory, transcript path). Stdout becomes additional context for Pre/Post/SessionStart. Anything you echo to stderr with exit code 2 in PreToolUse halts the call — that is your security gate.
Two subtleties trip people up. First, exit code 1 is not a block — it is treated as a soft failure and Claude proceeds anyway, which is rarely what you want. Always use exit 2 for hard blocks. Second, PostToolUse cannot undo the action that already happened; if you need to prevent damage, your gate must live in PreToolUse. The Post event is for observation, formatting, logging, and feedback. Treat the two halves as different tools with different powers.
Pattern 1 — Block Destructive Bash
The cheapest insurance you will buy. Match Bash, scan the command for ruinous patterns, exit 2 if matched.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/block-destructive.sh" }
]
}
]
}
}
#!/usr/bin/env bash
# ~/.claude/hooks/block-destructive.sh
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -qE '(rm -rf /|rm -rf ~|dropdb|DROP DATABASE|git push.*--force.*(main|master)|:
---
## Pattern 5 — Cost Guardrail in PostToolUse
Claude Code writes a transcript JSONL containing token usage per turn. A `Stop` hook can tally cost and warn when a session crosses a budget.
bash
!/usr/bin/env bash
~/.claude/hooks/cost-guard.sh
input=$(cat)
transcript=$(echo "$input" | jq -r '.transcript_path')
[[ -f "$transcript" ]] || exit 0
Sonnet 4.5 pricing as a rough proxy: $3/M input, $15/M output
total=$(jq -s '
map(select(.message.usage)) |
map(.message.usage.input_tokens * 0.000003 + .message.usage.output_tokens * 0.000015) |
add // 0
' "$transcript")
awk -v t="$total" 'BEGIN { if (t+0
Where to Go Next
Hooks are leverage. They turn one-time prompt instructions ("don't force-push, please?") into permanent system behavior. Pair them with a curated CLAUDE.md and a tight permissions list and you have a workspace that defends itself.
For the broader picture, see the Claude Code complete guide for the full feature surface, and how to build a custom Claude Code skill for packaging multi-step workflows that hooks alone cannot express.
Top comments (0)