Headless Notes

Claude Code Hooks Lifecycle: Every Event, Explained

By Headless Notes · Published · 9 min read

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 situationWhat to pick firstFirst page to open
Beginner, never written a hookA Notification hook that alerts you when Claude is waitingHooks guide: Set up your first hook
Daily automation that sometimes loops or burns usageA PreToolUse or PostToolBatch budget guardHooks reference: Decision control
Overnight or cron runs that "finish" with nothing doneStopFailure + Stop hooks that log and verifyHooks reference and /claude-code-cron-job-setup-run-automations-overnight/
Tight budget (Pro plan, shared usage limits)Command hooks only. Skip prompt/agent hooksClaude pricing

Why each pick makes sense:

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).

PhaseEventFires whenCan block?
StartSessionStartSession begins or resumes (matcher: startup, resume, clear, compact, fork)No
StartInstructionsLoadedA CLAUDE.md or .claude/rules/*.md file is loadedNo
InputUserPromptSubmitA prompt is submitted, before Claude processes it, including turns Claude Code starts by itselfYes
Tool loopPreToolUseBefore a tool call executesYes
Tool loopPermissionRequest / PermissionDeniedA call needs a permission decision / auto mode denied itNo
Tool loopPostToolUse / PostToolUseFailureAfter a tool call succeeds / failsNo (exit code), but decision: "block" feeds back
Tool loopPostToolBatchAfter a batch of parallel tool calls resolves, before the next model callYes
DelegationSubagentStart / SubagentStopSubagent spawned / finishedStop: Yes
ContextPreCompact / PostCompactBefore / after context compaction (manual or auto)Pre: Yes
Turn endStopClaude finishes respondingYes
Turn endStopFailureTurn ends because of an API errorNo
WaitingNotificationClaude Code sends a notification (e.g. permission_prompt, idle_prompt)No
EndSessionEndSession terminatesNo

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

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.

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.

  1. Choose the file. Use .claude/settings.json to share hooks with your repo, .claude/settings.local.json for hooks only you use (gitignored), or ~/.claude/settings.json for all your projects (reference).
  2. Add a hooks block. 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" } ] }
    ]
  }
}
  1. Write stop-guard.sh. It reads JSON from stdin. As the first thing it does, it exits 0 if stop_hook_active is true. After that it runs your check, such as tests, and exits 2 with a reason on stderr if the check fails.
  2. Make scripts executable with chmod +x .claude/hooks/*.sh.
  3. Verify. Type /hooks in 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.
  4. 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.

MistakeFix
Stop hook loops foreverCheck stop_hook_active first and add a hard block counter (issue #54360)
Hook exits 1 and nothing happensOnly exit 2 blocks. Codes 1 and 3+ are non-blocking errors (reference)
Matcher on Stop ignoredStop, UserPromptSubmit and PostToolBatch don't support matchers. They always fire (reference)
Path breaks with spacesUse exec form (command + args) with ${CLAUDE_PROJECT_DIR} (reference)
Huge stdout clutters contextOutput over 10,000 characters is saved to a file with a preview, so keep it short (reference)
Expecting order between hooksMatching 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:

  1. Add the Stop guard and StopFailure alert from the steps above to one project.
  2. Add a PostToolBatch step counter that returns continue: false at a limit you choose.
  3. 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:

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

Spotted something out of date or wrong? Tell us at shigoto.ai.note@gmail.com. We check every guide against its official sources.