Skip to content

Claude Code Hooks Reference

Hooks are user-defined shell commands that execute automatically at specific points in Claude Code's lifecycle. They provide guaranteed automation that doesn't rely on Claude "remembering" to do something.

Table of Contents


Overview

Hooks complement the "should-do" suggestions in CLAUDE.md with "must-do" deterministic rules. They're perfect for:

  • Quality gates: Block commits with secrets, enforce linting
  • Automation: Auto-format on save, run tests on stop
  • Integration: Slack notifications, CI/CD triggers
  • Context loading: Initialize project environment at session start

Hook vs CLAUDE.md

AspectCLAUDE.mdHooks
ExecutionSuggestions Claude may followGuaranteed execution
FlexibilityContext-dependentDeterministic
Use caseGuidelines, preferencesEnforcement, automation

Hook Events

Claude Code supports 25 hook event types:

EventWhen it firesMatcher support
PreToolUseBefore tool executionYes (tool names)
PostToolUseAfter tool completesYes (tool names)
PermissionRequestWhen permission dialog showsYes (tool names)
UserPromptSubmitWhen user submits promptNo
StopWhen main agent finishesNo
SubagentStopWhen subagent finishesNo
NotificationWhen notification is sentYes (notification type)
PreCompactBefore context compactionYes (manual, auto)
PostCompactAfter context compaction completesNo
SessionStartWhen session starts/resumesYes (startup, resume, clear, compact)
SessionEndWhen session terminatesNo
SetupWhen --init, --init-only, --maintenance runNo
MessageDisplayBefore assistant message is rendered to user (v2.1.157+)No
TeammateIdleWhen a teammate agent goes idle (v2.1.33+)Yes (teammate name)
TaskCompletedWhen a task is marked completed (v2.1.33+)Yes (task ID)
ConfigChangeWhen configuration files change (v2.1.49+)Yes (config key)
WorktreeCreateWhen a git worktree is created (v2.1.50+)Yes (worktree path)
WorktreeRemoveWhen a git worktree is removed (v2.1.50+)Yes (worktree path)
StopFailureWhen a stop/termination operation fails (v2.1.78+)No
ElicitationWhen MCP tool requests user input (v2.1.76+)No
ElicitationResultAfter user responds to elicitation (v2.1.76+)No
TaskCreatedWhen a new task is created (v2.1.84+)No
CwdChangedWhen the working directory changes (v2.1.83+)No
FileChangedWhen a watched file changes (v2.1.83+)No
PermissionDeniedAfter auto mode classifier denies a tool (v2.1.89+)No

Event Details

PreToolUse

Fires after Claude creates tool parameters, before execution. Can:

  • Block the tool call (exit code 2)
  • Modify tool inputs (since v2.0.10)
  • Add context for Claude

PostToolUse

Fires immediately after tool completes successfully. Use for:

  • Post-processing (linting, formatting)
  • Logging and metrics
  • Validation of results

Stop / SubagentStop

Fires when Claude finishes responding. Perfect for:

  • Quality gates (run tests before accepting)
  • Summary generation
  • Cleanup tasks

SessionStart

Fires when session starts or resumes. Use for:

  • Loading project context
  • Setting environment variables
  • Initializing tools

Setup (v2.1.20+)

Fires when running initialization/maintenance commands:

  • --init: First-time project setup
  • --init-only: Run setup without starting session
  • --maintenance: Run maintenance tasks

Use for:

  • Project initialization scripts
  • First-time environment configuration
  • Database migrations on setup

TeammateIdle (v2.1.33+)

Fires when a teammate agent becomes idle after completing its turn. Use for:

  • Auto-assigning next available task
  • Collecting teammate status updates
  • Triggering coordination logic

TaskCompleted (v2.1.33+)

Fires when a task is marked as completed via TaskUpdate. Use for:

  • Triggering dependent workflows
  • Running validation checks
  • Updating external systems (Jira, Slack)

ConfigChange (v2.1.49+)

Fires when a configuration file (settings.json, .claude/CLAUDE.md, etc.) is modified. Use for:

  • Reloading configuration automatically
  • Validating settings changes
  • Syncing configuration across tools

WorktreeCreate (v2.1.50+)

Fires when a git worktree is created via --worktree flag or git worktree add. Use for:

  • Initializing worktree-specific settings
  • Setting up environment for parallel sessions
  • Notifying team members of new work branches

WorktreeRemove (v2.1.50+)

Fires when a git worktree is removed. Use for:

  • Cleaning up worktree-specific resources
  • Archiving session logs
  • Updating project tracking

MessageDisplay (v2.1.157+)

Fires before each assistant message is rendered to the user. Can transform or suppress displayed text without affecting Claude's reasoning. Use for:

  • RTK-style output filtering (compress/trim verbose output on the response side)
  • Injecting syntax-highlighted formatting
  • Masking sensitive data patterns before display
  • Custom rendering pipelines

Note: This hook affects the display layer only. It does not modify the actual assistant response stored in context.

Hook Conditional Execution (v2.1.85+)

Hooks support an if field to conditionally execute based on permission rule syntax:

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "if": "tool.input matches 'rm -rf'",
      "command": "echo 'BLOCKED: dangerous delete' && exit 1"
    }]
  }
}

Deferred Permission Decision (v2.1.89+)

PreToolUse hooks can return a "defer" decision for headless sessions, pausing tool execution until the session is resumed with --resume:

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "command": "echo '{\"decision\": \"defer\", \"reason\": \"Requires human review\"}'"
    }]
  }
}

PreCompact Blocking (v2.1.105+)

PreCompact hooks can now block compaction by returning exit code 2:

Exit CodeBehavior
0Continue with compaction
1Log error but continue with compaction
2Block compaction, prevent it from proceeding

Use cases:

  • Preserve critical context during active work
  • Wait for background processes to complete
  • Prevent compaction during specific workflow phases
  • Maintain conversation state for multi-step operations

Example bash script:

bash
#!/bin/bash
# .claude/hooks/pre-compact-check.sh

# Block compaction if critical context marker exists
if [ -f .claude/no-compact ]; then
  echo "Critical context in use, blocking compaction" >&2
  exit 2
fi

# Block if background task is running
if pgrep -f "my-long-task" > /dev/null; then
  echo "Background task in progress, deferring compaction" >&2
  exit 2
fi

# Allow compaction
exit 0

Configuration:

json
{
  "hooks": {
    "PreCompact": [{
      "matcher": "auto",
      "hooks": [{
        "type": "command",
        "command": ".claude/hooks/pre-compact-check.sh"
      }]
    }]
  }
}

Configuration

Settings File Locations

LocationPathScope
User~/.claude/settings.jsonAll projects
Project.claude/settings.jsonShared with team
Local.claude/settings.local.jsonPersonal, not committed

Basic Structure

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git commit:*)",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/pre-commit-check.sh",
            "timeout": 30
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint:fix"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/quality-gate.sh"
          }
        ]
      }
    ]
  }
}

Hook Types

Command Hooks

Execute a shell command:

json
{
  "type": "command",
  "command": "/path/to/script.sh",
  "timeout": 60
}

Hook Configuration Options

OptionTypeDescription
typestring"command" or "prompt"
commandstringShell command to execute
timeoutnumberMax execution time in seconds (default: 60)
oncebooleanRun hook only once per session (v2.1.0+)

Example with once: true:

json
{
  "type": "command",
  "command": ".claude/hooks/expensive-check.sh",
  "once": true
}

Prompt Hooks

Use AI to evaluate (Stop, SubagentStop, UserPromptSubmit, PreToolUse, PermissionRequest):

json
{
  "type": "prompt",
  "prompt": "Evaluate if this code change is safe: $ARGUMENTS",
  "timeout": 30
}

Matchers

Matchers are case-sensitive patterns for tool names.

Pattern Types

PatternExampleMatches
ExactWriteOnly Write tool
Regex ORWrite|EditWrite or Edit
WildcardNotebook.*Notebook and variants
All tools* or ""Everything

Common Tool Names

  • File ops: Read, Write, Edit, Glob, Grep
  • Shell: Bash
  • Subagents: Task
  • Web: WebFetch, WebSearch
  • MCP tools: mcp__<server>__<tool>

Bash Command Matching

json
"matcher": "Bash(git commit:*)"    // Any git commit
"matcher": "Bash(npm run:*)"       // Any npm script
"matcher": "Bash(rm:*)"            // Any rm command

Notification Types

json
"matcher": "permission_prompt"     // Permission requests
"matcher": "idle_prompt"           // Idle > 60 seconds
"matcher": "auth_success"          // Authentication success

Exit Codes

CodeMeaningBehavior
0SuccessContinue, stdout shown in verbose mode
2BlockStop action, stderr shown to Claude/user
OtherNon-blocking errorContinue, stderr shown in verbose mode

Exit Code 2 Behavior by Event

EventEffect
PreToolUseBlocks tool, shows stderr to Claude
PermissionRequestDenies permission
PostToolUseShows stderr to Claude (tool already ran)
UserPromptSubmitBlocks prompt, erases it, shows stderr to user
Stop/SubagentStopBlocks stop, shows stderr to Claude

Input/Output Format

Input (via stdin)

All hooks receive JSON on stdin:

json
{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse"
}

PreToolUse Additional Fields

json
{
  "tool_name": "Write",
  "tool_input": { "file_path": "/path/file.txt", "content": "..." },
  "tool_use_id": "toolu_01ABC..."
}

PostToolUse Additional Fields

json
{
  "tool_name": "Write",
  "tool_input": { ... },
  "tool_response": { "filePath": "/path/file.txt", "success": true },
  "tool_use_id": "toolu_01ABC..."
}

Output (JSON on stdout)

Common Fields

json
{
  "continue": true,
  "stopReason": "Message when continue=false",
  "suppressOutput": true,
  "systemMessage": "Warning to user"
}

PreToolUse Output

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "Reason",
    "updatedInput": { "field": "new_value" },
    "additionalContext": "Context message for Claude (v2.1.21+)"
  }
}

The additionalContext field (v2.1.21+) allows hooks to inject context that Claude will see when processing the tool result. Use for warnings, hints, or additional information.


Examples

1. Block Commits with Secrets

.claude/hooks/block-secrets.sh:

bash
#!/bin/bash
set -e

# Read JSON input
INPUT=$(cat)
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Check for sensitive patterns
if echo "$TOOL_INPUT" | grep -qE "(password|api_key|secret|token)="; then
  echo "BLOCKED: Potential secret in commit" >&2
  exit 2
fi

exit 0

settings.json:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git commit:*)",
        "hooks": [{
          "type": "command",
          "command": ".claude/hooks/block-secrets.sh"
        }]
      }
    ]
  }
}

2. Auto-Lint on File Save

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": "npm run lint:fix --silent"
        }]
      }
    ]
  }
}

3. Quality Gate on Stop

.claude/hooks/quality-gate.sh:

bash
#!/bin/bash

# Run tests
if ! npm test --silent 2>/dev/null; then
  echo '{"decision": "block", "reason": "Tests are failing. Please fix before completing."}'
  exit 0
fi

echo '{"continue": true}'
exit 0
json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [{
          "type": "command",
          "command": ".claude/hooks/quality-gate.sh"
        }]
      }
    ]
  }
}

4. Slack Notification on Completion

json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [{
          "type": "command",
          "command": "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code task completed!\"}' $SLACK_WEBHOOK_URL"
        }]
      }
    ]
  }
}

5. Load Project Context on Session Start

.claude/hooks/session-init.sh:

bash
#!/bin/bash

# Set environment variables
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
  echo 'export DATABASE_URL=postgres://...' >> "$CLAUDE_ENV_FILE"
fi

# Output context for Claude
echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "Project uses Symfony 7.2, PHP 8.3, PostgreSQL 16"}}'
exit 0
json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [{
          "type": "command",
          "command": ".claude/hooks/session-init.sh"
        }]
      }
    ]
  }
}

Best Practices

Security

  1. Validate all inputs - Never trust hook input blindly
  2. Quote shell variables - Always use "$VAR" not $VAR
  3. Block path traversal - Check for .. in file paths
  4. Skip sensitive files - Avoid .env, .git/, keys

Performance

  1. Set appropriate timeouts - Default is 60s
  2. Use fast scripts - Hooks run synchronously
  3. Cache when possible - Avoid repeated expensive operations

Reliability

  1. Use absolute paths - Specify full script paths
  2. Handle errors gracefully - Exit codes matter
  3. Test thoroughly - Test in safe environment first
  4. Log actions - For debugging

Organization

.claude/
├── hooks/
│   ├── pre-commit-check.sh
│   ├── post-edit-lint.sh
│   ├── quality-gate.sh
│   └── session-init.sh
└── settings.json

Troubleshooting

Hook Not Executing

  1. Check matcher pattern (case-sensitive)
  2. Verify script is executable (chmod +x)
  3. Check file path is correct
  4. Enable verbose mode (ctrl+o) to see output

Hook Blocking Unexpectedly

  1. Check exit code (should be 0 for success)
  2. Verify stderr is empty for non-blocking
  3. Test script independently

Environment Variables Not Set

For SessionStart, use $CLAUDE_ENV_FILE:

bash
echo 'export VAR=value' >> "$CLAUDE_ENV_FILE"

Common Issues

IssueSolution
Script not foundUse absolute path
Permission deniedchmod +x script.sh
JSON parse errorValidate JSON output
TimeoutIncrease timeout or optimize script

Interactive Hook Management

/hooks Command (v2.1.105+)

The /hooks command provides interactive hook management and debugging capabilities:

bash
/hooks                    # View all active hooks with execution statistics
/hooks enable <name>      # Enable a specific hook
/hooks disable <name>     # Disable a hook temporarily
/hooks test <name>        # Test a hook with sample input
/hooks debug              # Show detailed hook execution logs

Features:

  • View statistics: See execution counts, success/failure rates, average execution time
  • Toggle hooks: Enable/disable hooks without editing JSON files
  • Test hooks: Run hooks with sample data to verify behavior
  • Debug mode: Detailed logging for troubleshooting

Example output:

Active Hooks:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PreToolUse
  ├─ block-secrets.sh         ✓  Executed: 23  Success: 100%  Avg: 45ms
  └─ lint-check.sh            ✓  Executed: 45  Success: 98%   Avg: 120ms

PostToolUse
  └─ auto-format.sh           ✓  Executed: 67  Success: 100%  Avg: 230ms

Stop
  └─ quality-gate.sh          ✓  Executed: 5   Success: 80%   Avg: 1.2s

SessionStart
  └─ init-context.sh          ✓  Executed: 1   Success: 100%  Avg: 350ms


Sources