intermediatepracticalhookslifecycleeventsautomation

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:

LocationScopeShareable
~/.claude/settings.jsonAll your projectsNo
.claude/settings.jsonSingle projectYes, committed to repo
.claude/settings.local.jsonSingle projectNo, gitignored
Managed policy settingsOrganization-wideAdmin-controlled
Plugin hooks/hooks.jsonWhen plugin enabledBundled 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)

EventFires WhenMatcher ValuesCan Block?
SessionStartSession begins or resumesstartup, resume, clear, compactNo (context only)
Setup--init-only or --init in -p modeinit, maintenanceNo
InstructionsLoadedCLAUDE.md or rules file loadssession_start, nested_traversal, path_glob_match, include, compactNo
SessionEndSession terminatesclear, resume, logout, prompt_input_exit, otherNo

Turn Events (Once Per Turn)

EventFires WhenMatcher ValuesCan Block?
UserPromptSubmitUser submits a promptNo matcher (always fires)Yes
UserPromptExpansionSlash command expands to promptCommand nameYes
StopClaude finishes respondingNo matcherYes
StopFailureTurn ends due to API errorrate_limit, overloaded, server_error, etc.No
NotificationClaude sends a notificationpermission_prompt, idle_prompt, auth_success, etc.No

Agentic Loop Events (Every Tool Call)

EventFires WhenMatcher ValuesCan Block?
PreToolUseBefore tool call executesTool name: Bash, Edit, Write, Read, etc.Yes
PostToolUseAfter tool succeedsTool name (same as PreToolUse)Yes (feedback)
PostToolUseFailureAfter tool failsTool nameNo
PostToolBatchAfter parallel batch resolvesNo matcherYes
PermissionRequestPermission dialog appearsTool nameYes
PermissionDeniedAuto mode denies tool callTool nameNo (retry only)

Subagent and Task Events

EventFires WhenMatcher ValuesCan Block?
SubagentStartSubagent spawnedgeneral-purpose, Explore, Plan, or customNo
SubagentStopSubagent finishesSame as SubagentStartYes
TaskCreatedTask created via TaskCreateNo matcherYes
TaskCompletedTask marked completedNo matcherYes
TeammateIdleAgent teammate about to go idleNo matcherYes

Environment and File Events

EventFires WhenMatcher ValuesCan Block?
ConfigChangeConfig file changesuser_settings, project_settings, local_settings, policy_settings, skillsYes
CwdChangedWorking directory changesNo matcherNo
FileChangedWatched file changes on diskLiteral filenames: `.envrc.env`
PreCompactBefore context compactionmanual, autoYes
PostCompactAfter compaction completesmanual, autoNo
MessageDisplayAssistant text streams to screenNo matcherNo
WorktreeCreateWorktree being createdNo matcherYes (failure)
WorktreeRemoveWorktree being removedNo matcherNo
ElicitationMCP server requests user inputMCP server nameYes
ElicitationResultUser responds to MCP elicitationMCP server nameYes

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:

DecisionEffect
allowSkips the permission prompt, tool executes immediately
denyPrevents the tool call entirely
askPrompts the user to confirm before execution
deferPauses 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):

TypeHow It WorksBest For
commandRuns a shell command, reads JSON from stdinLinting, security checks, logging
httpPOSTs JSON to a URL, reads responseExternal validation services, webhooks
mcp_toolCalls a tool on a connected MCP serverSecurity scanning, compliance checks
promptSends prompt to a Claude model for yes/no decisionComplex evaluations without tool access
agentSpawns 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 CodeMeaningEffect
0SuccessClaude Code parses stdout for JSON output
2Blocking errorAction is blocked (event-dependent)
OtherNon-blocking errorShows 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:

PatternEvaluated AsExample
"*", "", or omittedMatch allFires on every occurrence
Letters, digits, _, spaces, ,, |Exact string or pipe/comma listBash matches only Bash; Edit|Write matches either
Any other characterJavaScript 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:

  1. PostCompact -- Re-inject project context after every compaction. Solves the most common invisible failure mode.

  2. Stop with agent verification -- Prevent Claude from finishing until tests pass. Builds the auto-verification loop pattern.

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

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