AI Claude Code Hooks AGENTS.md Reference Automation 2026

Claude Code Hooks: The Complete 2026 Production Reference (32+ Events, 5 Handler Types, Exit Code Semantics)

The Prompt Shelf ·

Claude Code Hooks let you intercept the agent loop at 32+ lifecycle events and inject custom logic — block dangerous commands, notify Slack, run formatters, enforce policies, log every tool call. This page is the complete production reference: every event, every handler type, every exit-code rule, verified against Anthropic’s official Claude Code docs as of May 2026 (v2.1.141+).

If you’re wiring up Hooks in production, this is the page to bookmark.

The 32+ Hook Events (Full Catalog)

Hooks fire at five lifecycle cadences. Pick the right one for your use case.

Session-Level (Once per session)

EventWhen it fires
SessionStartWhen a session begins or resumes
SetupWhen you start Claude Code with --init-only, --init, or --maintenance in -p mode
SessionEndWhen a session terminates

Turn-Level (Once per user prompt)

EventWhen it firesBlocks?
UserPromptSubmitBefore Claude processes the submitted prompt
UserPromptExpansionWhen a typed command expands into a prompt, before it reaches Claude
StopWhen Claude finishes responding
StopFailureWhen the turn ends due to an API error❌ observability only

Agentic Loop (Every tool call)

EventWhen it firesBlocks?
PreToolUseBefore a tool call executes
PostToolUseAfter a tool call succeeds❌ (already ran)
PostToolUseFailureAfter a tool call fails❌ (already failed)
PostToolBatchAfter a full batch of parallel tool calls resolves, before the next model call✅ stops loop
PermissionRequestWhen a permission dialog appears✅ denies
PermissionDeniedWhen a tool call is denied by the auto mode classifier❌ already denied

Agent / Team Events

EventWhen it firesBlocks?
SubagentStartWhen a subagent is spawned
SubagentStopWhen a subagent finishes✅ prevents stop
TeammateIdleWhen an agent team teammate is about to go idle
TaskCreatedWhen a task is being created via TaskCreate✅ rolls back
TaskCompletedWhen a task is being marked as completed✅ prevents completion

File & Environment Events

EventWhen it firesBlocks?
InstructionsLoadedWhen a CLAUDE.md or .claude/rules/*.md file is loaded into context
ConfigChangeWhen a configuration file changes during a session✅ (except policy_settings)
CwdChangedWhen the working directory changes
FileChangedWhen a watched file changes on disk
WorktreeCreateWhen a worktree is being created via --worktree
WorktreeRemoveWhen a worktree is being removed

Context & Notification Events

EventWhen it firesBlocks?
PreCompactBefore context compaction✅ blocks compaction
PostCompactAfter context compaction completes
NotificationWhen Claude Code sends a notification
ElicitationWhen an MCP server requests user input during a tool call
ElicitationResultAfter a user responds to an MCP elicitation

Total: 27 distinct events (the “32+” in the title counts subtypes — for example SessionStart accepts matchers startup, resume, clear, compact, which behave as separate trigger points).

The 5 Handler Types

{
  "type": "command" | "http" | "mcp_tool" | "prompt" | "agent",
  "if": "Bash(git *)",              // Optional: permission rule filter
  "timeout": 600,                   // Seconds before canceling
  "statusMessage": "Checking...",   // Optional: spinner message
  "once": true                      // Optional: run once per session
}

1. command — Shell Script

The workhorse. Spawn a shell command, pipe stdin JSON, read stdout JSON / stderr.

{
  "type": "command",
  "command": "/path/to/secret-guard.sh",
  "args": ["${tool_input.file_path}"],
  "shell": "bash",
  "async": false
}
  • Shell form (no args): full tokenization, pipes, globs available.
  • Exec form (with args): no shell, each arg verbatim, path placeholders substituted.
  • async: true: runs in background, doesn’t block the agent loop.
  • asyncRewake: true: wakes when the async command exits with code 2.

2. http — Webhook

POST JSON to an HTTP endpoint. Useful for centralized policy services or Slack/Discord notifications.

{
  "type": "http",
  "url": "http://localhost:8080/hook",
  "headers": { "Authorization": "Bearer $MY_TOKEN" },
  "allowedEnvVars": ["MY_TOKEN"]
}

Response handling:

  • 2xx empty body: success (= exit 0).
  • 2xx with JSON: parsed as hook output.
  • Non-2xx: non-blocking error, execution continues.

3. mcp_tool — Delegate to MCP Server

Forward the hook decision to a tool exposed by a connected MCP server.

{
  "type": "mcp_tool",
  "server": "my_server",
  "tool": "security_scan",
  "input": { "file_path": "${tool_input.file_path}" }
}

If the server is disconnected or the tool returns isError: true, the hook produces a non-blocking error and execution continues. Don’t rely on mcp_tool for policy enforcement that must hold under network failure.

4. prompt — Single-Turn Claude Decision

{
  "type": "prompt",
  "prompt": "Is this $ARGUMENTS safe? Reply JSON with {decision, reason}.",
  "model": "claude-haiku-4-5"
}

$ARGUMENTS is the hook input JSON. The model returns a JSON object that Claude Code parses as hook output. Use Haiku for speed when the decision is bounded.

5. agent — Spawn a Subagent

Spawn a full subagent with tool access. The heaviest option, but supports multi-step reasoning.

{
  "type": "agent",
  "prompt": "Investigate why this command failed: $ARGUMENTS",
  "model": "claude-opus-4-7"
}

Matcher Patterns

The matcher field filters when a hook fires within an event.

Matcher valueEvaluated asExample
"*", "", omittedMatch allFires on every occurrence
Letters/digits/_/| onlyExact string or |-separated listBash or Edit|Write
Contains other charsJavaScript regex^Notebook or mcp__memory__.*

MCP Tool Matching

MCP tools follow the pattern mcp__<server>__<tool>:

{ "matcher": "mcp__memory__.*" }     // All memory server tools
{ "matcher": "mcp__.*__write.*" }    // Write tools from any server

Per-Event Matcher Targets

Event groupMatcher filtersExample values
PreToolUse/PostToolUse/etcTool nameBash, Edit|Write, mcp__.*
SessionStartSession sourcestartup, resume, clear, compact
SetupCLI flaginit, maintenance
SessionEndExit reasonclear, resume, logout, other
NotificationNotification typepermission_prompt, auth_success
SubagentStart/SubagentStopAgent typegeneral-purpose, Explore, Plan
PreCompact/PostCompactTrigger typemanual, auto
ConfigChangeConfig sourceuser_settings, project_settings, policy_settings
FileChangedLiteral filenames.envrc|.env
StopFailureError typerate_limit, authentication_failed, server_error
InstructionsLoadedLoad reasonsession_start, nested_traversal, path_glob_match

Exit Code Semantics (Critical for Security)

The single most important rule from the official docs:

“For most hook events, only exit code 2 blocks the action. Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use exit 2.

Exit codeMeaningBehavior
0SuccessParse stdout for JSON output. Proceed unless JSON blocks.
2Blocking errorIgnore stdout. Use stderr as error message. Blocks the action for capable events.
Any otherNon-blocking errorIgnore stdout. Show first line of stderr in transcript. Execution continues.

This is the gotcha that breaks half of all hook implementations: developers reach for exit 1 instinctively, the hook fails silently, and the dangerous action proceeds anyway. Always use exit 2 for policy enforcement.

Configuration Locations

LocationScopeShareable
~/.claude/settings.jsonAll projectsNo, local
.claude/settings.jsonSingle projectYes, committed
.claude/settings.local.jsonSingle projectNo, gitignored
Managed policy settingsOrganization-wideYes, admin-controlled
Plugin hooks/hooks.jsonWhen enabledYes, bundled
Skill/agent frontmatterComponent lifetimeYes, in file

Three-Level Nesting Structure

{
  "hooks": {
    "PreToolUse": [                  // Level 1: event
      {
        "matcher": "Bash",            // Level 2: matcher group
        "hooks": [
          {
            "type": "command",        // Level 3: handler
            "command": "/path/to/script.sh"
          }
        ]
      }
    ]
  }
}

JSON Input / Output

Common stdin Input (all events)

{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse"
}

In a subagent context, you additionally get agent_id and agent_type. In tool-use context, you get effort.level (also available as $CLAUDE_EFFORT env var).

JSON stdout Output

{
  "continue": true,                 // false stops Claude entirely
  "stopReason": "Build failed",     // Message when continue=false
  "suppressOutput": false,          // Omit from debug log
  "systemMessage": "Warning",       // User warning
  "terminalSequence": "\033]...",   // Terminal escape (v2.1.141+)
  "decision": "block",              // For most events
  "reason": "Not allowed",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "...",
    "permissionDecision": "deny",
    "permissionDecisionReason": "..."
  }
}

Decision Control Pattern

Different events expect different output schemas:

Most events — top-level decision:

{ "decision": "block", "reason": "Test suite must pass" }

PreToolUse — nested hookSpecificOutput.permissionDecision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes not allowed"
  }
}

PermissionRequesthookSpecificOutput.decision:

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": { "command": "npm run lint" }
    }
  }
}

PreToolUse Tool Input Schemas (Most Used)

Bash

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  }
}

Edit / Write

// Write
{ "tool_input": { "file_path": "...", "content": "..." } }

// Edit
{ "tool_input": {
  "file_path": "...",
  "old_string": "original",
  "new_string": "replacement",
  "replace_all": false
}}

Read / Glob / Grep

// Read
{ "tool_input": { "file_path": "...", "offset": 10, "limit": 50 } }

// Glob
{ "tool_input": { "pattern": "**/*.ts", "path": "/path/to/dir" } }

// Grep
{ "tool_input": {
  "pattern": "TODO.*fix",
  "path": "/path/to/dir",
  "glob": "*.ts",
  "output_mode": "content",
  "-i": true,
  "multiline": false
}}

WebFetch / WebSearch

{ "tool_input": { "url": "https://example.com/api", "prompt": "Extract endpoints" } }
{ "tool_input": { "query": "search terms" } }

Environment Variables

VariableAvailable inPurpose
Parent shell envAll hooksInherited
$CLAUDE_EFFORTAll hooksCurrent effort level
$CLAUDE_CODE_REMOTEAll hooks"true" in web environments
$CLAUDE_ENV_FILESessionStart, Setup, CwdChanged, FileChanged onlyFile path for persisting env vars

Example — set env vars from a SessionStart hook:

echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"

Path Placeholders

Available in all hook types:

  • ${CLAUDE_PROJECT_DIR} — Project root
  • ${CLAUDE_PLUGIN_ROOT} — Plugin installation directory
  • ${CLAUDE_PLUGIN_DATA} — Plugin persistent data directory

Production Patterns

Pattern 1: Dangerous Command Block (PreToolUse + exit 2)

#!/bin/bash
# block-dangerous.sh — PreToolUse hook, matcher: Bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$CMD" | grep -qE 'rm -rf /|git push --force.*main'; then
  echo "Refusing dangerous command: $CMD" >&2
  exit 2  # Critical: NOT exit 1
fi
exit 0

Pattern 2: Format After Edit (PostToolUse)

#!/bin/bash
# format-after-edit.sh — PostToolUse, matcher: Edit|Write
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
case "$FILE" in
  *.ts|*.tsx) npx prettier --write "$FILE" 2>/dev/null ;;
  *.py)       black "$FILE" 2>/dev/null ;;
  *.go)       gofmt -w "$FILE" 2>/dev/null ;;
esac
exit 0

Pattern 3: Secret Guard (PreToolUse Edit)

#!/bin/bash
INPUT=$(cat)
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string')

if echo "$CONTENT" | grep -qE 'sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|AKIA[A-Z0-9]{16}'; then
  echo "Refusing to write potential secret to file" >&2
  exit 2
fi
exit 0

Pattern 4: Telegram Notify on Stop

#!/bin/bash
# telegram-notify.sh — Stop hook, no matcher
MSG="Claude Code session ended at $(date)"
curl -s "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
  -d "chat_id=$TG_CHAT_ID" -d "text=$MSG" > /dev/null
exit 0

Pattern 5: Cost Logger (PostToolUse + PostToolBatch)

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
echo "$(date -u +%FT%TZ) $TOOL" >> ~/.claude/cost-log.tsv
exit 0

Pipe the log into a daily aggregator for per-project tool usage analytics.

Pattern 6: Block Direct Push to Main

#!/bin/bash
# matcher: Bash, "if": "Bash(git push*)"
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)

if echo "$CMD" | grep -qE 'git push.*origin (main|master)' && [ "$BRANCH" = "main" ]; then
  echo "Push to main blocked. Use a feature branch + PR." >&2
  exit 2
fi
exit 0

Hooks vs Subagents vs Skills (When to Use Which)

NeedUse
Lifecycle event (pre-commit, post-edit, on-stop) — fire automaticallyHook
Reusable workflow invoked on demand by nameSkill
Context isolation for a side taskSubagent
Always-loaded instructions at session startCLAUDE.md

For broader configuration-file comparison see AGENTS.md vs CLAUDE.md vs .cursor/rules. For subagent details see Claude Code Subagents: The Complete 2026 Reference.

Common Pitfalls

  1. Using exit 1 for policy enforcement. Claude Code treats it as non-blocking — the dangerous action proceeds. Always exit 2 for blocks.
  2. Mixing decision schemas. Most events use top-level decision, but PreToolUse uses hookSpecificOutput.permissionDecision. Reading the wrong field = your block doesn’t fire.
  3. Forgetting chmod +x. Hook command silently fails (exit code is non-existent). Inspect with a logging $HOOK_LOG write.
  4. Matcher regex case mismatch. Edit|Write|multiEdit (lowercase m) doesn’t match MultiEdit. Use Edit|Write|MultiEdit exactly.
  5. mcp_tool for security enforcement. If the MCP server disconnects, the hook produces a non-blocking error — the action proceeds. Use command hooks for hard policy.
  6. disableAllHooks doesn’t disable managed hooks. Organization-deployed managed policy hooks survive disableAllHooks: true set at user/project/local level.
  7. HTTP hook timeouts. Default timeout is 600 seconds. If your webhook handler is slow, the agent loop stalls. Use async: true for non-critical webhooks.

FAQ

What’s the most important Claude Code hook exit code rule?

exit 2 blocks, every other non-zero is non-blocking. This is the single most common implementation bug. Developers reach for exit 1 (Unix convention for failure), but Claude Code only respects exit 2 for policy enforcement on capable events.

How many hook events does Claude Code support?

27 distinct events as of May 2026 (v2.1.141+): SessionStart, Setup, SessionEnd, UserPromptSubmit, UserPromptExpansion, Stop, StopFailure, PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, SubagentStart, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Notification, Elicitation, ElicitationResult.

What’s the difference between PreToolUse and PostToolUse?

PreToolUse fires before the tool runs and can block with exit 2. PostToolUse fires after the tool succeeds and is observability-only — you can’t undo what already happened, but you can react (format files, log usage, notify).

How do I write a hook that blocks dangerous Bash commands?

Use a PreToolUse hook with matcher: "Bash". Read stdin JSON, extract tool_input.command, check against a deny pattern, and exit 2 with stderr message if matched. Critical: not exit 1. See “Pattern 1: Dangerous Command Block” above for working code.

Can hooks call HTTP endpoints instead of shell scripts?

Yes — type: "http". POST JSON to a URL, parse 2xx response as hook output. Useful for centralized policy services or Slack/Discord notifications. Caveat: non-2xx is non-blocking, so don’t use HTTP hooks for hard policy enforcement under network failure.

What does hookSpecificOutput mean?

Some events expect their decision output in a nested hookSpecificOutput object rather than the top-level decision field. PreToolUse uses hookSpecificOutput.permissionDecision, PermissionRequest uses hookSpecificOutput.decision. Most other events use the flat top-level decision. Read the docs for your specific event.

Are hooks shared across team members?

Depends on location. .claude/settings.json (project, committed) is shared. ~/.claude/settings.json (user, machine-local) is personal. .claude/settings.local.json (gitignored) is personal-per-project. Plugin hooks travel with the plugin. Managed policy hooks are admin-deployed organization-wide and survive disableAllHooks.

Can a hook stop Claude entirely instead of just blocking one action?

Yes — return {"continue": false, "stopReason": "..."} in JSON output. The session stops with the message you provide. Use sparingly — typically reserved for catastrophic states (compromised credentials, broken build, etc.).

How do I debug a hook that isn’t running?

Three checks: (1) chmod +x on the script. (2) Confirm matcher regex matches the actual tool name (case-sensitive, watch for MultiEdit vs multiEdit). (3) Inject a debug log at the top: echo "[$(date)] hook called $0" >> ~/.claude/hook-debug.log. If that log file stays empty, the hook isn’t being invoked — check /hooks menu to confirm registration.

What’s the timeout for an async hook?

Default is 600 seconds (10 min). Override with "timeout": 60 in the handler config. For async hooks (async: true), the timeout applies to total runtime — the agent loop continues without waiting. Use asyncRewake: true if you want the async hook to be able to wake Claude later with exit 2.

External References

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules