The Complete Hook Event Lifecycle
Claude Code supports 30+ hook event types fired at precise moments in the agent loop. Learn the exact timing, input schemas, and event-specific fields for each lifecycle phase.
Learning objectives
- List all 30+ hook event types and when each fires in the agent lifecycle
- Understand the common JSON input schema shared across all hooks
- Recognize event-specific fields like tool_name, file_path, and error details
- Map async and turn-local events to their positions in the decision loop
ToolDix original visual
Frame
Name the outcome and constraints.
Build
Try one bounded workflow.
Review
Keep evidence, revise, and share.
Claude Code fires hooks at roughly 30 distinct points in its lifecycle. Each event carries a standard JSON input payload with session metadata, plus event-specific fields. Understanding the complete lifecycle—when each event fires, what data it receives, and how events chain—is essential for writing hooks that actually integrate with the agent loop rather than fighting it.
The complete hook event taxonomy
Claude Code organizes hooks into four categories by timing: session-level (once per session), turn-level (once per user input), action-level (per tool call), and async (any time).
| Event | Category | Fires When | Common Use |
|---|---|---|---|
SessionStart | Session | New or resumed session begins (with --init, --maintenance, or normal startup) | Initialize environment, start logging, load external state |
Setup | Session | Initialization with --init-only, --init, or --maintenance flag | One-time setup tasks (clone repos, install dependencies) |
UserPromptSubmit | Turn | User submits a prompt; Claude is about to process it | Log user intent, validate prompt, emit metrics |
UserPromptExpansion | Turn | Slash command (like /mcp) is about to expand | Custom slash command handling, pre-expansion logging |
PreToolUse | Action | Agent decided to call a tool; hook fires before execution | Validate tool args, block dangerous patterns, rate-limit |
PermissionRequest | Action | Permission dialog is about to appear to the user | Audit what needs approval, auto-approve safe patterns |
PermissionDenied | Action | Tool was rejected by auto-mode classifier or permission rule | Log denials, alert on policy violations |
PostToolUse | Action | Tool executed successfully; hook runs before agent sees result | Parse tool output, validate results, trigger webhooks |
PostToolUseFailure | Action | Tool exited with error (non-zero exit code or exception) | Log tool failures, alert on retries, extract structured error |
PostToolBatch | Turn | All parallel tool calls in a batch complete | Aggregate results, validate state across tools, coordinate side effects |
Stop | Turn | Claude finishes responding; turn ends normally | Send notifications, commit logs, trigger downstream actions |
StopFailure | Turn | Turn ends due to API error (rate limit, overload, auth failure) | Log API failures, alert ops, trigger retry logic |
SubagentStart | Action | Subagent spawned with scoped tools | Log delegation, track subagent task, set up monitoring |
SubagentStop | Action | Subagent finishes and reports back | Validate subagent results, integrate into main context |
TaskCreated | Action | Agent uses TaskCreate to spawn a background task | Track task creation, validate task scope |
TaskCompleted | Action | Background task completes and result returns | Log task completion, integrate result into context |
TeammateIdle | Action | Agent team teammate goes idle (in multi-agent setups) | Alert, rebalance load, or escalate |
Notification | Async | Claude Code sends notification (UI alert, desktop, or terminal) | Log all notifications, route to external systems |
MessageDisplay | Async | Assistant message text renders to user | Log conversation, extract key decisions from message |
InstructionsLoaded | Async | CLAUDE.md or custom rules file loaded | Log instruction changes, audit instruction content |
ConfigChange | Async | Settings file changes on disk (hooks, permissions, etc.) | Validate new config, alert on risky changes |
CwdChanged | Async | Working directory changes | Validate directory, emit metrics on path changes |
FileChanged | Async | File on disk changes (detected by file watcher) | Trigger on-file-change rules, validate file content |
WorktreeCreate | Async | New worktree created (git worktree) | Log worktree, set up isolation, tag in metrics |
WorktreeRemove | Async | Worktree deleted | Clean up monitoring, remove tags |
PreCompact | Turn | Context window compaction begins | Back up transcript, log compact reason |
PostCompact | Turn | Context compaction completes | Validate compacted context, log tokens saved |
Elicitation | Action | MCP server requests user input via elicitation protocol | Log MCP requests, validate elicitation |
ElicitationResult | Action | User responds to MCP elicitation | Log MCP responses, validate answer |
SessionEnd | Session | Session terminates (user clears, resumes elsewhere, logs out, or other) | Finalize logs, commit state, trigger cleanup |
Common JSON input schema
Every hook receives a JSON object on stdin with these fields present:
{
"session_id": "sess_abc123def456",
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"transcript_path": "/path/to/.claude/sessions/transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"effort": { "level": "medium" },
"hook_event_name": "EventName",
"agent_id": "subagent-uuid-or-null",
"agent_type": "agent-name"
}
Common fields:
session_id(string): Unique ID for the current session. Use this to correlate hook calls across a single session.prompt_id(UUID): Unique ID for the current user prompt. Correlates hooks fired within the same turn.transcript_path(string): Absolute path to the session transcript (JSONL format), readable by the hook at runtime.cwd(string): Current working directory at hook-fire time.permission_mode(string): One ofdefault,plan,acceptEdits,auto,dontAsk, orbypassPermissions.effort(object):{ "level": "low" | "medium" | "high" | "xhigh" | "max" }— the user's effort setting.hook_event_name(string): The name of the event firing (e.g.,PreToolUse,Stop).agent_id(string or null): UUID of the subagent if firing within a subagent context; null for main agent.agent_type(string): The agent type (e.g.,Explore,Organize, custom name, or null).
Event-specific fields by category
Session-level events
SessionStart
{
"hook_event_name": "SessionStart",
"source": "startup|resume|clear|compact|fork",
"model": "claude-sonnet-5",
"agent_type": "Explore",
"session_title": "My Project Setup"
}
source: How the session started.startup= new session;resume=/resumecommand;clear=/clear;compact= auto-compaction triggered;fork=/forkcommand.model: The model Claude Code is using for this session.agent_type: Type of agent (defaults to primary agent type).session_title: User-provided or auto-generated session label.
Setup
{
"hook_event_name": "Setup",
"setup_flag": "init|init-only|maintenance"
}
setup_flag: Which CLI flag triggered setup (--init,--init-only,--maintenance).
SessionEnd
{
"hook_event_name": "SessionEnd",
"reason": "clear|resume|logout|other",
"token_count": 45000
}
reason: Why the session ended.token_count: Total tokens used in the session.
Turn-level events
UserPromptSubmit
{
"hook_event_name": "UserPromptSubmit",
"user_prompt": "Write a test file",
"tokens": 150
}
user_prompt: The exact text the user submitted.tokens: Estimated tokens in the prompt.
UserPromptExpansion
{
"hook_event_name": "UserPromptExpansion",
"slash_command": "/mcp",
"expanded_text": "expanded command text"
}
slash_command: The slash command (e.g.,/mcp,/fork).expanded_text: What the command expands to.
Stop and StopFailure
{
"hook_event_name": "Stop",
"message_length": 2400,
"tool_call_count": 5,
"turn_number": 3
}
{
"hook_event_name": "StopFailure",
"error_type": "rate_limit|overloaded|authentication_failed|other",
"error_message": "Rate limit exceeded",
"retry_after": 30
}
Action-level events (tools)
PreToolUse
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
},
"tool_call_id": "tool_abc123"
}
tool_name: The built-in tool (e.g.,Bash,Read,Edit,Write) or MCP tool name (mcp__server__action).tool_input: The exact arguments passed to the tool (varies by tool).tool_call_id: Unique ID for this tool call, used to correlate withPostToolUse.
PostToolUse and PostToolUseFailure
{
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_call_id": "tool_abc123",
"execution_time_ms": 250,
"output_length": 1024,
"exit_code": 0
}
{
"hook_event_name": "PostToolUseFailure",
"tool_name": "Bash",
"tool_call_id": "tool_abc123",
"error": "Command exited with code 127: command not found",
"exit_code": 127
}
PermissionRequest
{
"hook_event_name": "PermissionRequest",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build" },
"permission_required": "write"
}
PostToolBatch
{
"hook_event_name": "PostToolBatch",
"tool_call_count": 3,
"successful_count": 3,
"failed_count": 0,
"total_execution_time_ms": 800
}
SubagentStart and SubagentStop
{
"hook_event_name": "SubagentStart",
"subagent_id": "subagent-uuid",
"subagent_type": "TestRunner",
"task_description": "Run end-to-end tests"
}
{
"hook_event_name": "SubagentStop",
"subagent_id": "subagent-uuid",
"success": true,
"output": "All tests passed"
}
Async events
FileChanged
{
"hook_event_name": "FileChanged",
"file_path": "/home/user/my-project/src/index.js",
"change_type": "modified|created|deleted"
}
ConfigChange
{
"hook_event_name": "ConfigChange",
"config_source": "user_settings|project_settings|policy_settings",
"file_path": "~/.claude/settings.json",
"change_summary": "hooks config updated"
}
Notification
{
"hook_event_name": "Notification",
"notification_type": "permission_prompt|idle_prompt|auth_success|task_completed",
"title": "Permission required",
"body": "Claude wants to run: rm -rf build"
}
CwdChanged
{
"hook_event_name": "CwdChanged",
"old_cwd": "/home/user/project-a",
"new_cwd": "/home/user/project-b"
}
WorktreeCreate and WorktreeRemove
{
"hook_event_name": "WorktreeCreate",
"worktree_path": "/home/user/my-project/worktree-branch",
"branch": "feature/new-auth"
}
Exit code and output semantics
After your hook script finishes, Claude Code checks its exit code:
| Exit Code | Behavior | Notes | |-----------|----------|-------| | 0 | Success | Hook succeeded. Claude Code parses stdout as JSON and acts on any decision fields. | | 2 | Blocking error | Hook must reject the action. stderr is shown to user; action blocked. Used for validation failures. | | Other (1, 127, etc.) | Non-blocking error | Hook encountered an error but doesn't block. stderr is logged; execution continues. |
If your hook outputs valid JSON on stdout with exit code 0, Claude Code reads these decision fields:
{
"continue": true,
"stopReason": "Optional message if continue=false",
"suppressOutput": false,
"systemMessage": "Optional warning to show user",
"decision": "allow|block|defer",
"reason": "Why this decision"
}
continue(boolean): Iffalse, stop processing (used by SessionEnd hooks to trigger cleanup before exit).decision(string): For hooks that gate actions, set toblockto prevent;allowto force-allow;deferto leave to user.systemMessage(string): Warning or info to display to the user.
Hook firing order in a single turn
To visualize how events chain in a realistic session, here's the exact sequence for one complete turn where the agent calls two tools in parallel:
UserPromptSubmit (hook)
↓
UserPromptExpansion (hook, if slash command)
↓
[Agent thinks, decides to call 2 tools]
↓
PreToolUse (hook for tool 1)
↓
[Tool 1 runs]
↓
PostToolUse or PostToolUseFailure (hook for tool 1)
↓
PreToolUse (hook for tool 2, in parallel)
↓
[Tool 2 runs]
↓
PostToolUse or PostToolUseFailure (hook for tool 2)
↓
PostToolBatch (hook, after all parallel tools done)
↓
[Agent observes results, reasons about next step]
↓
Stop or StopFailure (hook)
Async events (FileChanged, ConfigChange, Notification) can fire at any point and do not interrupt this sequence.
Matcher support: filtering which events trigger your hook
You don't have to run a hook for every event. In your settings file, you can register matchers that constrain when a hook runs:
{
"hooks": [
{
"event": "PreToolUse",
"matcher": "Bash",
"command": "validate.sh"
}
]
}
Each event type supports different matcher patterns:
- Tool events (
PreToolUse,PostToolUse, etc.): Match by tool name regex (e.g.,Bash,Edit|Write,mcp__.*). SessionStart: Match by source (startup,resume,clear,compact).Setup: Match by CLI flag (init,init-only,maintenance).FileChanged: Match by filename glob (e.g.,.env|.envrc,src/**/*.js).Notification: Match by type (permission_prompt,idle_prompt,auth_success).SubagentStart/SubagentStop: Match by agent type name.ConfigChange: Match by source (user_settings,project_settings,policy_settings).StopFailure: Match by error type (rate_limit,overloaded,authentication_failed).
Common mistake
Assuming event-specific fields (like tool_name in PreToolUse) are always present in the hook input. Some hooks fire at moments where a tool isn't involved (e.g., Stop), so they don't have tool_name or tool_input. Always write defensive hooks that check for the presence of optional fields and provide sensible defaults. The documented schema above shows which fields are guaranteed for each event type; anything not listed should be treated as optional.
Sources and license context
These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.
- Hooks Reference (opens code.claude.com in a new tab)External · code.claude.com (Anthropic terms apply)
- Hooks Guide (opens code.claude.com in a new tab)External · code.claude.com (Anthropic terms apply)
Keep going
Read these next on ToolDix.
Original lessons that build on what you just read.