Reference

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.
hook-block-push-main.json
{
  "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.

HookEventWhat it doesVerdict
format-on-savePostToolUseprettier on every Write|Editkeep — killed the 47×/week nag
test-on-writePostToolUserun the matching test filekeep on small trees; see the 15k-file failure below
block-push-to-mainPreToolUsegreps tool input for git push origin mainkeep, everywhere — three "oh no" saves at Belkins (Ch 16)
commit-message-templatePreToolUseenforce commit formatsituational — teams yes, solo it's friction
slack-notify-on-long-taskStopping when a long run endssituational — only if you actually walk away
secrets-scanPreToolUse Write|Edit8 live-key patterns, blocks the writekeep, everywhere — it has fired on a real Anthropic key
claude-md-size-guardSessionStartwarn past 300 lineskeep — a 340-line CLAUDE.md doubled a token bill in a week
prompt-cache-guardUserPromptSubmitwarn before blowing a warm cachesituational — matters at API spend, noise on Max
agent-watchdog-stall-detectPreToolUse TaskJSONL log of agent spawnskeep if you run swarms; it's telemetry, not a gate
commit-msg-enforcerStoplint the final commit messagesituational — 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).

secrets-scan.sh
#!/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

Stay close

The next edition lands when this list says it does.

No course. No paywall. Operator playbooks weekly. 10K+ subscribers.