DEV Community

Sangmin Lee
Sangmin Lee

Posted on • Originally published at claudeguide.io

Claude Code Hooks: 8 Production Patterns (2026)

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" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode
#!/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.

Enter fullscreen mode Exit fullscreen mode


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)