DEV Community

Cover image for Claude Code Hooks Lifecycle: Every Event, Explained
Aoba Works
Aoba Works

Posted on Originally published at en.aobaworks.com

Claude Code Hooks Lifecycle: Every Event, Explained

Originally published at Unattended.

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 PreToolUse and PostToolBatch before the next model call, so you stop the spending instead of just recording it (reference).
  • Unattended runs: StopFailure matches error types such as rate_limit and billing_error, which otherwise show up as a quiet exit (reference).
  • Budget: prompt hooks send text to a model and agent hooks 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, UserPromptSubmit gets 30, and all SessionEnd hooks 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." }
Enter fullscreen mode Exit fullscreen mode

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.

  • StopFailure fires when a turn ends because of an API error. Its matcher accepts values like rate_limit, overloaded, authentication_failed, billing_error and max_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/.
  • PostToolUseFailure fires after any failed tool call. Write those failures to a file so you can check them in the morning.
  • Notification with idle_prompt or permission_prompt tells you the run is stuck waiting for a human (reference).
  • SessionEnd reports 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.

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

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:

  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:

  • /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


This article was created with the help of AI. Sources are linked inline; check the official documentation for the latest details.

Top comments (0)