Skip to main content
Claude Code Tutorial: From First Command to Custom Workflows

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.

Intermediate18 minBy ToolDix Editorial

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

Claude Code Tutorial practice loop
1

Frame

Name the outcome and constraints.

2

Build

Try one bounded workflow.

3

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.

ToolDix original diagram
Hooks: middleware around the agent loop
Agent decides
Time to call a tool
PreToolUse hook
Validate or block the tool call
Tool executes
If hook allowed it
PostToolUse hook
Log result, validate output, trigger side effects
Agent observes
And decides next step
Hooks add middleware at specific points without changing how the agent reasons.

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

EventCategoryFires WhenCommon Use
SessionStartSessionNew or resumed session begins (with --init, --maintenance, or normal startup)Initialize environment, start logging, load external state
SetupSessionInitialization with --init-only, --init, or --maintenance flagOne-time setup tasks (clone repos, install dependencies)
UserPromptSubmitTurnUser submits a prompt; Claude is about to process itLog user intent, validate prompt, emit metrics
UserPromptExpansionTurnSlash command (like /mcp) is about to expandCustom slash command handling, pre-expansion logging
PreToolUseActionAgent decided to call a tool; hook fires before executionValidate tool args, block dangerous patterns, rate-limit
PermissionRequestActionPermission dialog is about to appear to the userAudit what needs approval, auto-approve safe patterns
PermissionDeniedActionTool was rejected by auto-mode classifier or permission ruleLog denials, alert on policy violations
PostToolUseActionTool executed successfully; hook runs before agent sees resultParse tool output, validate results, trigger webhooks
PostToolUseFailureActionTool exited with error (non-zero exit code or exception)Log tool failures, alert on retries, extract structured error
PostToolBatchTurnAll parallel tool calls in a batch completeAggregate results, validate state across tools, coordinate side effects
StopTurnClaude finishes responding; turn ends normallySend notifications, commit logs, trigger downstream actions
StopFailureTurnTurn ends due to API error (rate limit, overload, auth failure)Log API failures, alert ops, trigger retry logic
SubagentStartActionSubagent spawned with scoped toolsLog delegation, track subagent task, set up monitoring
SubagentStopActionSubagent finishes and reports backValidate subagent results, integrate into main context
TaskCreatedActionAgent uses TaskCreate to spawn a background taskTrack task creation, validate task scope
TaskCompletedActionBackground task completes and result returnsLog task completion, integrate result into context
TeammateIdleActionAgent team teammate goes idle (in multi-agent setups)Alert, rebalance load, or escalate
NotificationAsyncClaude Code sends notification (UI alert, desktop, or terminal)Log all notifications, route to external systems
MessageDisplayAsyncAssistant message text renders to userLog conversation, extract key decisions from message
InstructionsLoadedAsyncCLAUDE.md or custom rules file loadedLog instruction changes, audit instruction content
ConfigChangeAsyncSettings file changes on disk (hooks, permissions, etc.)Validate new config, alert on risky changes
CwdChangedAsyncWorking directory changesValidate directory, emit metrics on path changes
FileChangedAsyncFile on disk changes (detected by file watcher)Trigger on-file-change rules, validate file content
WorktreeCreateAsyncNew worktree created (git worktree)Log worktree, set up isolation, tag in metrics
WorktreeRemoveAsyncWorktree deletedClean up monitoring, remove tags
PreCompactTurnContext window compaction beginsBack up transcript, log compact reason
PostCompactTurnContext compaction completesValidate compacted context, log tokens saved
ElicitationActionMCP server requests user input via elicitation protocolLog MCP requests, validate elicitation
ElicitationResultActionUser responds to MCP elicitationLog MCP responses, validate answer
SessionEndSessionSession 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 of default, plan, acceptEdits, auto, dontAsk, or bypassPermissions.
  • 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 = /resume command; clear = /clear; compact = auto-compaction triggered; fork = /fork command.
  • 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 with PostToolUse.

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): If false, stop processing (used by SessionEnd hooks to trigger cleanup before exit).
  • decision (string): For hooks that gate actions, set to block to prevent; allow to force-allow; defer to 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.

Keep going

Read these next on ToolDix.

Original lessons that build on what you just read.