●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Claude Code Hooks: A Complete Field Guide to All 8 Hook Types and How to Pick the Right One
Claude Code hooks are powerful, but most people give up before figuring out which event does what. Here's the field guide I wish I had when I started — six months of running hooks in production, distilled.
I spent six months running Claude Code in production before I truly understood hooks. The documentation covers what they do, but not why you'd pick one over another in a real scenario. Halfway through, I realized I'd been setting up hooks wrong the whole time: two hooks fighting each other, one never firing because I'd misconfigured the timing, another creating an infinite loop.
The frustration wasn't the hooks themselves—it was the mental model. I was treating them like simple on/off switches when they're actually a control flow system that governs the entire conversation between you, Claude Code, and your tools.
This guide is for people who got hooks working once, then broke something and gave up. Or people who want to avoid that entirely. I'll walk through every hook type, show you when to use it, and reveal the three production bugs I hit that taught me the hard way.
The Design Philosophy Behind Hooks
Claude Code hooks aren't just event listeners. They're the connective tissue between what you ask Claude to do and what actually gets executed.
Think of the traditional flow: user prompt → code generation → done. One direction, no feedback loop.
Hooks create: user ⇄ Claude Code (with hooks) ⇄ tools (git, npm, shell, API calls). Bidirectional. The hook system manages that conversation. Each hook type handles a specific phase.
The first principle: separate concerns. Don't put PostToolUse logic in PreToolUse. Don't use Stop to handle single-tool failures. Mixing them is where everything breaks.
The second: hooks are synchronous by default, but support async. If you put a Promise in a hook, Claude Code will wait. But wait too long and the session times out. The balance is tight—that's our first production trap.
The third: hooks fire in sequence, not in parallel. Misunderstanding this sequence is the most common footgun. Get this wrong and you'll spend hours debugging why a value is undefined or why a tool runs twice.
The fourth: each hook phase has a different responsibility. Don't make SessionStart do what SessionEnd should do. Don't make PreToolUse handle post-execution validation. This separation is what makes debugging possible.
The 8 Hooks: A Complete Reference
Hook
Fires
Responsibility
Common Use
SessionStart
Immediately after session begins
Initialize environment
Set up dependencies, env files, initial checks
UserPromptSubmit
When user sends a message (before processing)
Pre-filter and enrich input
Detect code, validate syntax, security checks
PreToolUse
Right before a tool executes
Validate preconditions
Security gates, permission checks, state validation
PostToolUse
Right after a tool executes
Verify results
Confirm success, handle errors, log outcomes
Notification
Anytime, independently
Send external messages
Slack notifications, email alerts, webhooks
Stop
When deciding whether to end session
Evaluate completion
Auto-stop on goal achievement, cleanup
SubagentStop
When a subagent finishes
Collect and pass results
Feed subagent output to parent workflow
SessionEnd
Before session closes
Final cleanup
Generate reports, archive logs, notifications
This table is deceptive because it makes them look independent. They're not. Understanding the firing sequence is the foundation of hooks that actually work.
One note before we go deeper: the JSON in this guide is a conceptual pseudo-schema, written to make each hook's responsibility obvious. The format you actually put in your settings file is different, and I show it in full later under "How the Config File Actually Looks." Hold the map of responsibilities in your head first; we'll wire it to the real syntax afterward.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The real hooks config format in settings.json — matcher, command, the stdin JSON contract, and exit code 2 to block — not the conceptual pseudo-schema
✦The firing order of all 8 hooks, and why environment variables don't survive between PreToolUse and PostToolUse (with the fix)
✦Measured per-tool overhead that hooks add, and the latency budget that keeps them from getting in the way
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
UserPromptSubmit — Every time the user sends a message
PreToolUse — Before each tool call
PostToolUse — After each tool call (on success)
Notification — Any time during the flow, runs in parallel
Stop — Evaluated at completion checks
SessionEnd — Once, at the very end
Subagents have their own sequence within the parent sequence.
This ordering matters more than anything else. Skip this section and you'll spend days debugging mysterious failures.
The reason I emphasize this: I once set environment variables in PreToolUse, expecting them to be available in PostToolUse. They weren't. Spent three hours before realizing they're different processes—environment variables don't cross boundaries.
PreToolUse vs PostToolUse: The Foundational Distinction
You'll use these two constantly. Getting them right means 80% of your automation works.
PreToolUse: Runs before the tool. Use it for "should I even run this?"
Example: before running npm install, check if package-lock.json is corrupted. If it is, regenerate it first. If you don't do this PreToolUse check, npm will silently install slightly wrong versions, and your app won't break for weeks.
When you push to a GPG-protected branch, PreToolUse verifies your key exists before attempting the push. PostToolUse verifies the remote actually accepted your signature.
UserPromptSubmit: Input Processing and Security
This fires when the user sends a message, before Claude Code processes it. Use it to clean, validate, or enrich user input.
Real example: users sometimes paste code snippets. That code might use imports that aren't installed. Catch that before Claude Code tries to run it.
Real injection example: user pastes SQL from a bug report.
Query: SELECT * FROM users WHERE id = 'x' OR '1'='1'
A good UserPromptSubmit hook detects this pattern and alerts before Claude Code tries to execute it. This has saved me several times when users pasted problematic queries from bug reports.
You can also use this hook to inject context. For instance, if the user mentions "fix the login flow," you can automatically prepend the current auth.ts file and the test file to the context, saving the user from having to explicitly ask.
Stop vs SubagentStop: Don't Confuse These
Stop: End the entire session.
Use it when the goal is accomplished. All tests pass, coverage is above 80%, no errors. Stop here.
This hook is useful for fully automated workflows. You set a goal, Claude Code works toward it, and once the goal is met, the session closes automatically. No user intervention needed.
SubagentStop: End just one subagent, then feed its results back to the parent.
Use it for parallel work. "Backend development subagent is done. Send results to frontend development subagent."
SubagentStop lets you orchestrate these workflows. When backend finishes, notify the integration subagent. When frontend finishes, trigger E2E tests.
Confusing these means:
Using SubagentStop when you meant Stop → session keeps running, extra unexpected work happens
Using Stop when you meant SubagentStop → parallel work stops prematurely
I learned this the hard way. I had a backend subagent finish, but used Stop instead of SubagentStop. The parent session immediately closed without running the frontend integration tests. Took me an hour to realize the order was wrong.
SessionStart and SessionEnd: Environment Bookends
SessionStart: Runs first, before anything else. Initialize the environment.
SessionStart is where you set up everything Claude Code needs before touching your code. Check Node version, install dependencies, generate environment files, run security audits.
The power here is that Claude Code never makes the mistake of "oh, I needed Node 18 but only 16 is installed." You're baking in those requirements upfront.
SessionEnd: Runs last, before the session closes. Cleanup and reporting.
SessionEnd is where you clean up and report results. Generate a summary of what was done, send it to stakeholders, archive logs for auditing, delete temporary files.
This is useful for compliance. If your workflow touches production or sensitive data, SessionEnd lets you ensure all audit trails are preserved before the session closes.
Notification: Fire and Forget External Communication
Send data to external systems. Slack, email, webhooks—doesn't matter. This hook fires independently and doesn't block the session.
Three Production Traps I Hit (And How to Avoid Them)
Trap 1: Misunderstanding Hook Firing Order
Hooks don't fire simultaneously. They fire in a strict sequence:
SessionStart
UserPromptSubmit
PreToolUse
PostToolUse (only if tool succeeds)
Notification (parallel)
Stop (evaluated)
SessionEnd
I once set an environment variable in PreToolUse and tried to use it in PostToolUse. It was undefined. Why? Because PreToolUse runs in one subprocess and PostToolUse runs in a different subprocess. Environment variables don't cross subprocess boundaries.
If your hook makes an async call (API request, database query, CI status check), Claude Code will wait. But only for a configurable limit. Exceed it and the session times out.
I wrote a hook to wait for CI to finish after a git push. 30 seconds in the test repo, no problem. On a real project with a 20-minute CI pipeline, the session timed out.
The on_timeout action is critical. If CI is slow, don't fail the hook—keep going and notify later via a separate async process.
Another approach: split the work. PreToolUse triggers the CI build. PostToolUse checks if it's done, but doesn't wait. A separate background job checks later and notifies.
Everything above was a conceptual diagram of responsibilities. Here's the shape that actually runs in my setup (Claude Code v2.x). Key names have shifted between versions, so confirm the details against your own installation — but the skeleton is stable.
Hooks live under the hooks key of .claude/settings.json (per project) or ~/.claude/settings.json (per user), as an array per event name. The important shift: a hook's body isn't a JSON declaration, it's a shell command. Your decision logic lives inside your own script.
matcher is a regex against the tool name — Bash, or Edit|Write for several, or an empty string for all tools. The exact-match behavior for hyphenated tool names was tightened in a specific version, so if a hook goes silent right after an update, suspect that first: when an update stops your hook from firing (v2.1.195 matcher change).
The script receives a JSON payload on stdin. The tool name and its arguments are in there.
#!/usr/bin/env bash# guard-git-push.shinput=$(cat) # read the full JSON from stdincmd=$(echo "$input" | jq -r '.tool_input.command // ""')if echo "$cmd" | grep -qE 'git push .*(-f|--force)'; then echo "Force-pushing to main is blocked. Consider --force-with-lease." >&2 exit 2 # exit 2 = block this tool call, feed the reason back to Claudefiexit 0 # exit 0 = allow it through
The single biggest difference from the declarative schema is how you signal a block. Exit with code 2 and whatever you wrote to stderr is handed back to Claude while the tool call is stopped. Exit 0 and it sails through. Once you internalize that two-value contract, every gate, validation, and notification described earlier as "pseudo-schema" is just an ordinary shell script. Which also means debugging a hook comes down to one question: can you run that script by hand? Pipe a sample JSON into it before you ever blame the config file.
Measuring Hook Overhead
It's easy to miss, but every hook spawns a separate process. Each tool call pays a small startup cost, and those costs stack. You won't feel it day to day — until you hang something heavy off PostToolUse.
I measured this on the CI integration for a wallpaper app I build solo. For the same git commit, I compared three setups — no hook, a lightweight validation script, and running npm audit on every call — and timed the added latency per call. Median values on my machine, for reference:
Setup
Added time per call (median)
Felt like
No hook
0 ms
baseline
Lightweight script (branch in jq, exit fast)
~40–80 ms
unnoticeable
PostToolUse running npm audit every time
~1,800–3,200 ms
a clear wait
A few tens of milliseconds of process startup never register in a conversation. What bites is running anything that touches the network or disk on every call. That npm audit above only needed to run right after an install, not on every command.
My operating rule of thumb: keep a synchronous hook's added time under 200 ms. Anything heavier gets its firing narrowed with matcher and conditions, or pushed async so I only receive a notification. The faster a hook is, the more invisible it stays while always on — and that quietness is exactly what lets you keep it running instead of ripping it out.
Operating Hooks in Production
Test first. Hooks that touch git or npm can silently break things. Test in a dummy project first. Create a test repository, set up your hooks, run a few scenarios, and verify behavior matches expectations.
Log everything. When something goes wrong, you need to know which hook fired, in what order, and what it did.
Version-control hook configs. Commit your hook definitions to git. Later you'll ask, "when did this hook get added?" Git history answers it.
git add .claude-code/hooks.jsongit commit -m "feat: Add security gate for main branch pushes"
Hooks are the lever that turns Claude Code from a code generator into an automated workflow engine. The mental model is the hard part. Once you have it—sequence, dependencies, separation of concerns—the execution is straightforward. Start small, test thoroughly, and you'll have something powerful.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.