Claude Code Hooks Lifecycle: Every Event, Explained
Claude Code hooks are shell commands, HTTP calls, MCP tools or model prompts that run at fixed points in a session. A run starts with SessionStart, loops through UserPromptSubmit → PreToolUse → PostToolUse/PostToolUseFailure → PostToolBatch for each step, ends each turn with Stop (or StopFailure when the API errors), and finishes with SessionEnd (Hooks reference). Four of these events do most of the work in unattended automations. PreToolUse, PostToolBatch and Stop can halt runaway work. StopFailure reports runs that died quietly.
Which option is right for you?
Choose your first hook based on the problem you have now. The table below matches common situations to an event.
| Your situation | What to pick first | First page to open |
|---|---|---|
| Beginner, never written a hook | A Notification hook that alerts you when Claude is waiting | Hooks guide: Set up your first hook |
| Daily automation that sometimes loops or burns usage | A PreToolUse or PostToolBatch budget guard | Hooks reference: Decision control |
| Overnight or cron runs that "finish" with nothing done | StopFailure + Stop hooks that log and verify | Hooks reference and /claude-code-cron-job-setup-run-automations-overnight/ |
| Tight budget (Pro plan, shared usage limits) | Command hooks only. Skip prompt/agent hooks | Claude pricing |
Why each pick makes sense:
- Beginner: a notification hook can't block anything, so a mistake in it won't break your session.
- Daily use: you can block at
PreToolUseandPostToolBatchbefore the next model call, so you stop the spending instead of just recording it (reference). - Unattended runs:
StopFailurematches error types such asrate_limitandbilling_error, which otherwise show up as a quiet exit (reference). - Budget:
prompthooks send text to a model andagenthooks run a subagent, so both use model calls. Command hooks are just your own scripts and cost nothing extra.
The lifecycle of one automation run, in order
Every hook event belongs to a phase of the run. Knowing the phase tells you whether a hook can still change the outcome. The table follows the official reference, which currently lists more than 30 events (Hooks reference).
| Phase | Event | Fires when | Can block? |
|---|---|---|---|
| Start | SessionStart | Session begins or resumes (matcher: startup, resume, clear, compact, fork) | No |
| Start | InstructionsLoaded | A CLAUDE.md or .claude/rules/*.md file is loaded | No |
| Input | UserPromptSubmit | A prompt is submitted, before Claude processes it, including turns Claude Code starts by itself | Yes |
| Tool loop | PreToolUse | Before a tool call executes | Yes |
| Tool loop | PermissionRequest / PermissionDenied | A call needs a permission decision / auto mode denied it | No |
| Tool loop | PostToolUse / PostToolUseFailure | After a tool call succeeds / fails | No (exit code), but decision: "block" feeds back |
| Tool loop | PostToolBatch | After a batch of parallel tool calls resolves, before the next model call | Yes |
| Delegation | SubagentStart / SubagentStop | Subagent spawned / finished | Stop: Yes |
| Context | PreCompact / PostCompact | Before / after context compaction (manual or auto) | Pre: Yes |
| Turn end | Stop | Claude finishes responding | Yes |
| Turn end | StopFailure | Turn ends because of an API error | No |
| Waiting | Notification | Claude Code sends a notification (e.g. permission_prompt, idle_prompt) | No |
| End | SessionEnd | Session terminates | No |
The reference also lists events for tasks, teams, worktrees, model switches, MCP elicitation and file changes, such as TaskCompleted, TeammateIdle and FileChanged. Most solo setups don't need them on day one (reference).
Three timing facts that matter
- All matching hooks run in parallel, so you can't rely on one hook finishing before another starts (reference).
- Default timeouts differ by event. Most hooks get 600 seconds,
UserPromptSubmitgets 30, and allSessionEndhooks share a 1.5-second budget that can be raised to at most 60 seconds (reference). Keep cleanup work at session end small. - Hooks from different places add up. User, project, local, plugin, skill and subagent hooks all run; a lower level doesn't replace a higher one (reference).
Which hooks actually stop runaway token use
Only hooks that fire before the next model call can stop a runaway run. In practice that means PreToolUse, PostToolBatch, UserPromptSubmit and the universal continue: false field. Logging hooks like PostToolUse only record what already happened.
The kill switch: continue: false
Any hook can return JSON with "continue": false. The reference says this "stops Claude entirely," and stopReason explains why (reference). It's the most reliable way for an unattended run to stop itself.
PostToolBatch: the per-step budget checkpoint
PostToolBatch fires after a whole batch of tool calls resolves and before Claude makes its next model call (reference). That makes it a good place to count steps. For example, a script could keep a counter file in the session's scratchpad_dir (an input field available from v2.1.257; see the Hooks reference and Claude Code changelog). Once the count passes a limit you choose, it returns:
{ "continue": false, "stopReason": "Step budget of 80 tool batches reached. Stopping for review." }
The number 80 is only an example. Choose a limit based on how many steps your normal runs take.
PreToolUse: block the expensive call itself
PreToolUse returns permissionDecision with the value allow, deny, ask or defer. It can also rewrite the call with updatedInput (reference). Use a matcher such as Bash or mcp__. and the if field (for example "Bash(git )") so the hook only runs on the calls you care about. One limitation: deny blocks that one call, and Claude may try a different approach afterwards. Pair it with a counter if you want the run to end.
Stop: useful, but it can add usage
When a Stop hook exits with code 2, Claude doesn't stop and the conversation keeps going (reference). That's helpful for checks like "tests must pass before you're done." It's also the most common cause of loops that burn usage. Your hook should check the stop_hook_active input field, which is true when Claude is already continuing because a Stop hook blocked it. If it's true, exit 0 (Amit Kothari, claudefa.st). A GitHub issue (#54360) reports cases where the flag didn't propagate, so add a hard cap as well. For example, allow at most 3 blocks per session.
PreCompact: an early warning about long runs
PreCompact with the auto matcher fires before automatic compaction and can block (reference). If a routine job hits auto-compaction, it has probably grown larger than you meant it to. Logging it is a cheap signal.
Which hooks catch silent failures
Silent failures show up in four events: PostToolUseFailure, StopFailure, Notification and SessionEnd. None of them can block. Their job is to tell you something went wrong.
StopFailurefires when a turn ends because of an API error. Its matcher accepts values likerate_limit,overloaded,authentication_failed,billing_errorandmax_output_tokens(reference). Send yourself a message on these. Without it, a cron run that hit a rate limit looks the same as one that completed. For more on why runs stop, see /why-claude-code-stops-mid-automation-and-how-to-fix-it/.PostToolUseFailurefires after any failed tool call. Write those failures to a file so you can check them in the morning.Notificationwithidle_promptorpermission_prompttells you the run is stuck waiting for a human (reference).SessionEndreports the reason the session ended (clear,logout,prompt_input_exit,other). Keep it quick because of the 1.5-second default budget.
Hook failures can be silent too. If a command hook exits with code 1 or 3+, that's a non-blocking error: only the first line of stderr appears as a notice (reference). From v2.1.295 you can set "onFailure": "block" so a broken safety hook stops the run instead of letting it continue unchecked (reference, changelog).
Set up your first lifecycle hook
Start with a project-level Stop guard plus a StopFailure logger. Together they cover both runaway loops and quiet failures.
- Choose the file. Use
.claude/settings.jsonto share hooks with your repo,.claude/settings.local.jsonfor hooks only you use (gitignored), or~/.claude/settings.jsonfor all your projects (reference). - Add a
hooksblock. Example:
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-guard.sh" } ] }
],
"StopFailure": [
{ "matcher": "rate_limit|billing_error|server_error",
"hooks": [ { "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/alert.sh" } ] }
]
}
}
- Write
stop-guard.sh. It reads JSON from stdin. As the first thing it does, it exits 0 ifstop_hook_activeis true. After that it runs your check, such as tests, and exits 2 with a reason on stderr if the check fails. - Make scripts executable with
chmod +x .claude/hooks/*.sh. - Verify. Type
/hooksin Claude Code to open the read-only hook browser. Select All events to confirm each hook appears with its source (user, project, local, plugin, skill or subagent) (reference). The file watcher normally picks up direct edits to settings files without a restart. - Test with a small prompt before you hand the hook to a scheduled run. To schedule it afterwards, see /claude-code-cron-job-setup-run-automations-overnight/.
Common mistakes and fixes
Most hook problems come from a short list of misunderstandings.
| Mistake | Fix |
|---|---|
| Stop hook loops forever | Check stop_hook_active first and add a hard block counter (issue #54360) |
| Hook exits 1 and nothing happens | Only exit 2 blocks. Codes 1 and 3+ are non-blocking errors (reference) |
Matcher on Stop ignored | Stop, UserPromptSubmit and PostToolBatch don't support matchers. They always fire (reference) |
| Path breaks with spaces | Use exec form (command + args) with ${CLAUDE_PROJECT_DIR} (reference) |
| Huge stdout clutters context | Output over 10,000 characters is saved to a file with a preview, so keep it short (reference) |
| Expecting order between hooks | Matching hooks run in parallel. Put dependent steps in one script |
If you want to undo the file edits a run made, rather than prevent them, see /claude-code-checkpoints-how-rollback-works-in-automations/.
Next steps
You can have useful guards in place today with three small changes.
Do it yourself today:
- Add the
Stopguard andStopFailurealert from the steps above to one project. - Add a
PostToolBatchstep counter that returnscontinue: falseat a limit you choose. - Run
/hooks, select All events, and confirm what's actually active, including hooks from plugins.
If you'd rather not do it yourself: a paid plan doesn't write hooks for you, but it's required to run Claude Code. Claude Code is included in all paid plans and shares your plan's usage limits. Pro costs $20/month or $17/month billed annually, Max starts at $100/month, and Team Standard seats cost $25/month monthly or $20 annually (as of October 2026; check the official page). Pro suits solo builders running a few jobs a day. It doesn't suit long nightly multi-agent runs, which can hit the limits. Max gives you more usage but doesn't fix a hook that loops. If you'd rather hire someone, a freelance consultant can set up hooks for you. Give them the Hooks reference as the spec, and make sure you understand every script before it runs unattended.
Related guides on this site:
- /why-claude-code-stops-mid-automation-and-how-to-fix-it/
- /claude-code-checkpoints-how-rollback-works-in-automations/
- /claude-code-cron-job-setup-run-automations-overnight/
FAQ
What is the order of Claude Code hook events?
A typical run goes SessionStart → UserPromptSubmit → PreToolUse → PostToolUse/PostToolUseFailure → PostToolBatch, and the tool steps repeat until Stop or StopFailure. SessionEnd fires when the session closes. Compaction, subagent and notification events fire in between as needed (reference).
Which Claude Code hook can block an action?
PreToolUse, UserPromptSubmit, PostToolBatch, Stop, SubagentStop, PreCompact, ConfigChange and several task, worktree and elicitation events can block with exit code 2 or a JSON decision. SessionStart, Notification, StopFailure and SessionEnd can't (reference).
How do I stop a Claude Code Stop hook from looping?
Read stop_hook_active from the hook's stdin and exit 0 when it's true. Because of a reported propagation bug, also keep a per-session counter and allow the stop after a few blocks (issue #54360).
Where do I put Claude Code hooks?
Put them in ~/.claude/settings.json (all projects), .claude/settings.json (shared project), .claude/settings.local.json (personal project), plugin hooks/hooks.json, or skill and subagent frontmatter. Hooks from all of these locations run together (reference).
Do prompt or agent hooks cost extra?
They use model calls. prompt hooks send text to a model, and agent hooks start a subagent (marked experimental), so they count toward your plan's usage (reference, pricing). Use command hooks for checks that don't need judgment.
Sources
- Hooks reference – Claude Code Docs
- Automate actions with hooks – Claude Code Docs
- Intercept and control agent behavior with hooks – Agent SDK Docs
- Claude Code changelog – GitHub
- Claude pricing
- GitHub issue #54360: Stop hook fires repeatedly (stop_hook_active not propagating)
- How Claude Code stop hooks work – Amit Kothari
- Claude Code Stop Hook: Force Task Completion – claudefa.st
Spotted something out of date or wrong? Tell us at shigoto.ai.note@gmail.com. We check every guide against its official sources.