Chapter 1 of 8
Hooks Lifecycle Events Complete Guide
27 lifecycle events explained — PreToolUse, PostToolUse, Notification, and how to build auto-verification loops, formatting, security checks, and cost alerts.
Hooks Lifecycle Events Complete Guide
Claude Code fires hooks at 27 lifecycle events throughout every session. Most users never touch this system. For loop engineers, hooks are the control layer that transforms a conversational agent into a reliable, automated pipeline. This guide covers every event, explains when each one fires, and provides production-ready configuration patterns for auto-verification, security enforcement, and cost monitoring.
The Hook System Architecture
Hooks are user-defined commands, HTTP endpoints, LLM prompts, or MCP tool calls that execute automatically at specific points in Claude Code's lifecycle. The official documentation at code.claude.com/docs/en/hooks describes the full configuration schema. Hooks live in three nesting levels:
┌─────────────────────────────────────────────────────┐
│ Level 1: Event Selection │
│ Which lifecycle event triggers the hook? │
│ (PreToolUse, Stop, Notification, etc.) │
├─────────────────────────────────────────────────────┤
│ Level 2: Matcher Filtering │
│ Which specific tool, notification type, or │
│ session source should trigger it? │
│ (Bash, Write|Edit, permission_prompt) │
├─────────────────────────────────────────────────────┤
│ Level 3: Handler Execution │
│ What runs? command, http, mcp_tool, prompt, agent │
└─────────────────────────────────────────────────────┘
Hooks are defined in JSON settings files at different scopes:
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | Single project | Yes, committed to repo |
.claude/settings.local.json | Single project | No, gitignored |
| Managed policy settings | Organization-wide | Admin-controlled |
Plugin hooks/hooks.json | When plugin enabled | Bundled with plugin |
All 27 Lifecycle Events
Events fall into three cadences: once per session, once per turn, and on every tool call inside the agentic loop. The table below organizes all 27 events by category with their matcher fields.
Session Events (Once Per Session)
| Event | Fires When | Matcher Values | Can Block? |
|---|---|---|---|
SessionStart | Session begins or resumes | startup, resume, clear, compact | No (context only) |
Setup | --init-only or --init in -p mode | init, maintenance | No |
InstructionsLoaded | CLAUDE.md or rules file loads | session_start, nested_traversal, path_glob_match, include, compact | No |
SessionEnd | Session terminates | clear, resume, logout, prompt_input_exit, other | No |
Turn Events (Once Per Turn)
| Event | Fires When | Matcher Values | Can Block? |
|---|---|---|---|
UserPromptSubmit | User submits a prompt | No matcher (always fires) | Yes |
UserPromptExpansion | Slash command expands to prompt | Command name | Yes |
Stop | Claude finishes responding | No matcher | Yes |
StopFailure | Turn ends due to API error | rate_limit, overloaded, server_error, etc. | No |
Notification | Claude sends a notification | permission_prompt, idle_prompt, auth_success, etc. | No |
Agentic Loop Events (Every Tool Call)
| Event | Fires When | Matcher Values | Can Block? |
|---|---|---|---|
PreToolUse | Before tool call executes | Tool name: Bash, Edit, Write, Read, etc. | Yes |
PostToolUse | After tool succeeds | Tool name (same as PreToolUse) | Yes (feedback) |
PostToolUseFailure | After tool fails | Tool name | No |
PostToolBatch | After parallel batch resolves | No matcher | Yes |
PermissionRequest | Permission dialog appears | Tool name | Yes |
PermissionDenied | Auto mode denies tool call | Tool name | No (retry only) |
Subagent and Task Events
| Event | Fires When | Matcher Values | Can Block? |
|---|---|---|---|
SubagentStart | Subagent spawned | general-purpose, Explore, Plan, or custom | No |
SubagentStop | Subagent finishes | Same as SubagentStart | Yes |
TaskCreated | Task created via TaskCreate | No matcher | Yes |
TaskCompleted | Task marked completed | No matcher | Yes |
TeammateIdle | Agent teammate about to go idle | No matcher | Yes |
Environment and File Events
| Event | Fires When | Matcher Values | Can Block? |
|---|---|---|---|
ConfigChange | Config file changes | user_settings, project_settings, local_settings, policy_settings, skills | Yes |
CwdChanged | Working directory changes | No matcher | No |
FileChanged | Watched file changes on disk | Literal filenames: `.envrc | .env` |
PreCompact | Before context compaction | manual, auto | Yes |
PostCompact | After compaction completes | manual, auto | No |
MessageDisplay | Assistant text streams to screen | No matcher | No |
WorktreeCreate | Worktree being created | No matcher | Yes (failure) |
WorktreeRemove | Worktree being removed | No matcher | No |
Elicitation | MCP server requests user input | MCP server name | Yes |
ElicitationResult | User responds to MCP elicitation | MCP server name | Yes |
The Core Loop: PreToolUse and PostToolUse
The two events you will use most are PreToolUse and PostToolUse. Together they form a gate that wraps every tool call Claude makes during its agentic loop.
┌──────────────────────────────────────────────────────────┐
│ Agentic Loop Turn │
│ │
│ Claude decides to use a tool │
│ │ │
│ ▼ │
│ ┌─────────────────┐ deny/ask ┌──────────────────┐ │
│ │ PreToolUse │───────────────►│ Permission Flow │ │
│ │ (can block, │◄───────────────│ or Stop │ │
│ │ modify input) │ allow └──────────────────┘ │
│ └────────┬────────┘ │
│ │ continue │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Tool Executes │ │
│ │ (Bash, Write, │ │
│ │ Edit, Read...) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PostToolUse │ Feed back results, log, │
│ │ (log, validate, │ inject context, or block │
│ │ modify output)│ the agentic loop │
│ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
PreToolUse: Four Decision Outcomes
PreToolUse offers richer control than any other event. The permissionDecision field accepts four values:
| Decision | Effect |
|---|---|
allow | Skips the permission prompt, tool executes immediately |
deny | Prevents the tool call entirely |
ask | Prompts the user to confirm before execution |
defer | Pauses execution for external input (non-interactive mode only) |
Additionally, updatedInput lets you modify tool parameters before execution, and additionalContext injects information into Claude's context. The hook also receives tool_name, tool_input, and tool_use_id on stdin.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "npm run lint --fix"
},
"additionalContext": "Running lint-fix before proceeding."
}
}
When multiple PreToolUse hooks return different decisions, precedence is deny > defer > ask > allow. This means a single security hook can override a permissive automation hook, which is the correct behavior for safety-critical workflows.
PostToolUse: Verification and Feedback
PostToolUse fires after a tool has already executed. The hook receives both tool_input (the arguments sent) and tool_response (the result returned), along with duration_ms. Use it for validation, logging, and output transformation.
The updatedToolOutput field can replace what Claude sees from the tool call. Combined with additionalContext, this lets you build verification loops where Claude receives corrected information and adjusts its next action accordingly.
Five Hook Handler Types
Claude Code supports five handler types that you can attach to any lifecycle event (with some restrictions documented below):
| Type | How It Works | Best For |
|---|---|---|
| command | Runs a shell command, reads JSON from stdin | Linting, security checks, logging |
| http | POSTs JSON to a URL, reads response | External validation services, webhooks |
| mcp_tool | Calls a tool on a connected MCP server | Security scanning, compliance checks |
| prompt | Sends prompt to a Claude model for yes/no decision | Complex evaluations without tool access |
| agent | Spawns a subagent with tool access (Read, Grep, Glob) | Multi-step verification requiring code inspection |
Events that support all five types include PreToolUse, PostToolUse, Stop, UserPromptSubmit, and SubagentStop. Session-level events like SessionStart and Setup only support command and mcp_tool. Notification and file-watching events support command, http, and mcp_tool but not prompt or agent.
Pattern 1: Auto-Verification Loop
The most powerful loop engineering pattern with hooks is the auto-verification loop. The idea is simple: every time Claude claims it is done, a hook verifies whether the work is actually complete before allowing the session to end.
Command-Based Verification
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/verify-completion.sh",
"args": [],
"timeout": 120
}
]
}
]
}
}
The verification script runs tests, checks for lint errors, and returns a decision:
#!/bin/bash
# .claude/hooks/verify-completion.sh
# Run the test suite
TEST_OUTPUT=$(npm test 2>&1)
TEST_EXIT=$?
# Run linter
LINT_OUTPUT=$(npm run lint 2>&1)
LINT_EXIT=$?
if [ $TEST_EXIT -ne 0 ]; then
echo "Tests failed. Claude must fix them before stopping." >&2
exit 2 # Blocking error: prevents Stop
fi
if [ $LINT_EXIT -ne 0 ]; then
jq -nc '{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Lint errors found. Please fix:\n'"$LINT_OUTPUT"'\nRun npm run lint --fix and re-check."
}
}'
exit 0
fi
exit 0 # All checks passed: allow Stop
Agent-Based Verification
For more complex checks that require reading files and understanding code, use an agent hook:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify the implementation is complete. Check: 1) All TODO comments have been resolved. 2) No console.log statements remain in production code. 3) All exported functions have corresponding tests. Read the modified files and run tests if needed. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
The agent hook spawns a subagent that can use Read, Grep, and Glob to inspect the codebase, run commands, and return a structured { "ok": true/false, "reason": "..." } decision. If ok is false, the reason is fed back to Claude as its next instruction, and the conversation continues. Claude Code overrides the hook and ends the turn after 8 consecutive blocks to prevent infinite loops.
Prompt-Based Verification
For lightweight checks that do not need tool access, use a prompt hook with a fast model:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate whether Claude should stop. Check if all requested tasks are complete and no errors remain. $ARGUMENTS",
"model": "claude-haiku-4.5",
"timeout": 15
}
]
}
]
}
}
Pattern 2: Security Enforcement
Hooks can enforce security policies that operate at the tool-call level, blocking dangerous commands before they execute.
Block Destructive Commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"args": []
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-secrets-read.sh",
"args": []
}
]
}
]
}
}
The destructive command blocker reads stdin JSON and denies rm -rf patterns:
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm -rf|rm -r /|mkfs|dd if='; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by security hook"
}
}'
else
exit 0
fi
The secrets reader blocks access to .env and credential files:
#!/bin/bash
FILE_PATH=$(jq -r '.tool_input.file_path')
if echo "$FILE_PATH" | grep -qE '\.env$|\.pem$|\.key$|id_rsa|credentials'; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Access to secrets file blocked: '"$FILE_PATH"'"
}
}'
else
exit 0
fi
Block .env File Writes
The 2026 Claude Code hooks guide at morphllm.com documents a simple pattern for preventing .env file writes using the Write matcher:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"if": "Write(*.env*)",
"command": "jq -n '{hookSpecificOutput: {hookEventName: \"PreToolUse\", permissionDecision: \"deny\", permissionDecisionReason: \".env writes are blocked by policy\"}}'"
}
]
}
]
}
}
Pattern 3: Auto-Formatting on Every Edit
Use PostToolUse on Write and Edit to enforce code style automatically. This is the loop engineering equivalent of a pre-commit hook that runs inside the agent loop.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/auto-format.sh",
"args": [],
"timeout": 30
}
]
}
]
}
}
#!/bin/bash
FILE_PATH=$(jq -r '.tool_input.file_path')
case "$FILE_PATH" in
*.ts|*.tsx|*.js|*.jsx)
npx prettier --write "$FILE_PATH" 2>/dev/null
;;
*.py)
python -m black "$FILE_PATH" 2>/dev/null
;;
*.go)
gofmt -w "$FILE_PATH" 2>/dev/null
;;
esac
exit 0
This runs synchronously, so Claude waits for formatting to complete before continuing. For faster workflows where formatting latency matters, set "async": true to run formatting in the background while Claude proceeds with the next action.
Pattern 4: Notification Hooks for Unattended Loops
When running Claude Code in the background or stepping away from the terminal, notification hooks keep you informed about what needs attention.
Desktop Notification on Permission Prompt
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/desktop-notify.sh"
}
]
}
]
}
}
#!/bin/bash
INPUT=$(cat)
TITLE="Claude Code"
BODY=$(jq -r '.message // "Needs your attention"' <<< "$INPUT")
# Build terminal escape sequence for desktop notification
SEQ=$(printf '\033]777;notify;%s;%s\007' "$TITLE" "$BODY")
jq -nc --arg seq "$SEQ" '{terminalSequence: $seq}'
The terminalSequence field requires Claude Code v2.1.141 or later and supports OSC escape sequences for iTerm2, Windows Terminal, WezTerm, Kitty, and Ghostty. The hook runs without a controlling terminal, so writing directly to /dev/tty will not work -- the terminalSequence field is the correct approach.
Slack Webhook on Stop Failure
Use an HTTP hook to send alerts when Claude encounters API errors:
{
"hooks": {
"StopFailure": [
{
"hooks": [
{
"type": "http",
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
"headers": {
"Content-Type": "application/json"
},
"timeout": 10
}
]
}
]
}
}
Pattern 5: Context Injection After Compaction
Claude Code's context window has a hard limit. When the conversation fills up, automatic compaction summarizes everything and resets. The problem is that critical context -- your project rules, current task status, environment constraints -- can get compressed away.
The PostCompact event fires after every compaction. Use it to re-inject essential context:
{
"hooks": {
"PostCompact": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-context.sh",
"args": []
}
]
}
]
}
}
#!/bin/bash
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
CONTEXT="Project: loopengineering.wiki
Current branch: $BRANCH
Uncommitted changes: $UNCOMMITTED files
Rules: Use Next.js App Router, Tailwind CSS v4, next-intl for i18n.
Build command: npm run build
Test command: npm run dev (manual verification)"
jq -nc --arg ctx "$CONTEXT" '{
"hookSpecificOutput": {
"hookEventName": "PostCompact",
"additionalContext": $ctx
}
}'
Mark Kashef, who consults on agentic operating systems, describes the post-compaction hook as one of the most reliable uses of hooks in production. It fires deterministically, does not depend on Claude remembering to ask, and does not require you to notice that compaction happened. The agent comes back from compaction already knowing what it needs to know.
Pattern 6: Cost Tracking with PostToolUse
Track per-tool-call costs by logging token usage from subagent completions. The PostToolUse input for Agent tool calls includes tool_response.resolvedModel and tool_response.totalTokens.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Agent",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/log-cost.sh",
"args": [],
"async": true
}
]
}
]
}
}
#!/bin/bash
INPUT=$(cat)
MODEL=$(echo "$INPUT" | jq -r '.tool_response.resolvedModel // "unknown"')
TOKENS=$(echo "$INPUT" | jq -r '.tool_response.totalTokens // 0')
DURATION=$(echo "$INPUT" | jq -r '.tool_response.totalDurationMs // 0')
AGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // "general"')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "$TIMESTAMP model=$MODEL tokens=$TOKENS duration_ms=$DURATION agent=$AGENT_TYPE" \
>> "${CLAUDE_PROJECT_DIR}/.claude/cost-log.tsv"
Run this as an async hook so it does not block the agent loop. Over time, the TSV log builds a cost profile of your workflow that you can analyze to optimize model selection and identify expensive loop iterations.
Exit Codes and Decision Control
The exit code from your hook command tells Claude Code what to do next. Understanding this mapping is essential for building reliable hooks.
| Exit Code | Meaning | Effect |
|---|---|---|
0 | Success | Claude Code parses stdout for JSON output |
2 | Blocking error | Action is blocked (event-dependent) |
| Other | Non-blocking error | Shows warning, execution continues |
Exit code 2 is the primary mechanism for blocking actions. Whether a given event can actually be blocked depends on the event type:
Can block with exit code 2?
├─ PreToolUse ──────────────── Yes: blocks the tool call
├─ PermissionRequest ─────── Yes: denies the permission
├─ UserPromptSubmit ──────── Yes: blocks the prompt
├─ Stop ───────────────────── Yes: prevents Claude from stopping
├─ PostToolBatch ──────────── Yes: stops the agentic loop
├─ PreCompact ─────────────── Yes: blocks compaction
├─ PostToolUse ───────────── No: tool already ran
├─ Notification ───────────── No: side effects only
├─ SessionStart ───────────── No: context injection only
└─ StopFailure ────────────── No: output ignored
Matcher Syntax Quick Reference
The matcher field determines when a hook fires. Its evaluation depends on the characters it contains:
| Pattern | Evaluated As | Example |
|---|---|---|
"*", "", or omitted | Match all | Fires on every occurrence |
Letters, digits, _, spaces, ,, | | Exact string or pipe/comma list | Bash matches only Bash; Edit|Write matches either |
| Any other character | JavaScript regex | ^Notebook matches tools starting with Notebook |
For MCP tools, use the naming pattern mcp__<server>__<tool>. The .* wildcard is required when matching server prefixes:
{
"matcher": "mcp__memory__.*"
}
The if field on individual handlers adds a second filter using permission rule syntax. "Bash(git *)" runs only for git subcommands, and "Edit(*.ts)" runs only for TypeScript files. Leading VAR=value assignments are stripped before matching, and commands inside $() and backticks are also checked.
Async Hooks for Non-Blocking Operations
Set "async": true on command hooks to run them in the background without blocking Claude. This is essential for long-running operations like test suites, deployments, and logging.
{
"type": "command",
"command": "/path/to/run-tests.sh",
"async": true,
"timeout": 300
}
Async hooks have constraints: they cannot block tool calls or return decisions, because the action has already proceeded by the time the hook completes. Output is delivered on the next conversation turn. Use asyncRewake: true instead of async if you need the hook to wake Claude immediately when it exits with code 2, even when the session is idle.
Debugging Hooks
Start Claude Code with --debug-file <path> or --debug to capture hook execution details:
[DEBUG] Executing hooks for PreToolUse:Bash
[DEBUG] Found 2 hook commands to execute
[DEBUG] Executing hook command: block-destructive.sh with timeout 600000ms
[DEBUG] Hook command completed with status 0
Use /hooks inside Claude Code to open a read-only browser showing every configured hook, its matcher, handler type, and source file. This is the fastest way to verify that your configuration loaded correctly.
Quick Start Recommendations
If you have never configured hooks before, start with these three in order:
-
PostCompact -- Re-inject project context after every compaction. Solves the most common invisible failure mode.
-
Stop with agent verification -- Prevent Claude from finishing until tests pass. Builds the auto-verification loop pattern.
-
PreToolUse on Bash -- Block destructive commands. One-time safety net with minimal overhead.
The demo repository by disler at github.com/disler/claude-code-hooks-mastery captures all 27 hook events with their JSON payloads. Use it as a reference for understanding what data each event provides on stdin. For visual configuration, the Claude Code Hooks Configuration Builder at hidekazu-konishi.com/tools/claude_code_hooks_config_builder_tool.html provides an interactive web UI for building and validating hook entries.
Related Resources
- CLAUDE.md configuration: Static project rules go in CLAUDE.md, not in hooks. See the CLAUDE.md Guide for the full reference.
- Agent loop fundamentals: Hooks are the control layer around the agent loop. Review the Agent Loop concept for how hooks fit into the broader architecture.
- Loop engineering in Claude Code: See Loop Engineering in Claude Code for how hooks integrate with model tiering and cost optimization.