Hooks System

Master Claude Code Hooks — 17 lifecycle events, 3 handler types, 10+ practical recipes and enterprise configuration

Updated 2026-09-03
On This Page

Hooks let you insert your own logic at key points in the Claude Code lifecycle — format code automatically, block dangerous commands, send notifications, write audit logs. Hooks are one of Claude Code's most powerful extension mechanisms.


1. Core Concepts

Every hook has three parts:

Event (When)   →  Matcher (Which)     →  Handler (What)
PreToolUse        matcher: "Bash"        command: "check_safety.sh"
  • Event: when it fires (for example PreToolUse — before a tool runs)
  • Matcher: an optional tool-name filter (only Bash, or only Write)
  • Handler: a shell command, a prompt injection, or a subagent

2. Complete Event List

Core events

Event Fires Can block Typical use
PreToolUse Before a tool runs Yes (exit 2) Security interception, argument validation
PostToolUse After a tool runs No Auto formatting, logging
Notification Claude sends a notification No Slack / Feishu / DingTalk alerts
Stop Claude finishes a response No Quality checks, auto commit
UserPromptSubmit User submits a prompt Yes Context injection, policy checks
SessionStart Session starts No Environment setup, welcome message

Extended events (new in 2026)

Event Fires Can block Typical use
SubagentStop A subagent finishes No Collect subagent results
SubagentToolUse A subagent uses a tool Yes Restrict subagent permissions
FileChanged A file is modified No Auto lint, trigger a build
CwdChanged Working directory changes No Load directory-specific config
ModelChange Model is switched No Track model usage
CompactComplete /compact finishes No Post-compaction processing
ToolError A tool errors out No Error collection, retry logic

3. Handler Types

Type 1: Command (shell command)

The most common type — runs a shell command:

{
  "type": "command",
  "command": "npx prettier --write $FILEPATH"
}

Environment variables: | Variable | Meaning | Available in | |------|------|---------| | $FILEPATH | Path of the file being operated on | PreToolUse/PostToolUse | | $TOOL_INPUT | Tool input as JSON | PreToolUse | | $TOOL_NAME | Tool name | PreToolUse/PostToolUse | | $SESSION_ID | Session id | All | | $NOTIFICATION_MESSAGE | Notification body | Notification |

stdin input: a hook script also receives a JSON payload on stdin containing session_id, tool_name and tool_input. Read it when you need structured data — it is more reliable than parsing the environment variables.

Exit codes:

  • 0: continue
  • 2: block the operation (PreToolUse/UserPromptSubmit only)
  • anything else: treated as an error but does not block

Type 2: Prompt (prompt injection)

Injects text into Claude's context:

{
  "type": "prompt",
  "prompt": "Remember: every database operation must use a transaction"
}

Useful for: injecting extra project rules at SessionStart.

Type 3: Subagent

Spawns a subagent to handle the event:

{
  "type": "subagent",
  "prompt": "Review the code that was just modified and check for security vulnerabilities"
}

Useful for: automatic code-quality review after PostToolUse.


4. Configuration

Configure in .claude/settings.json (project level) or ~/.claude/settings.json (user level):

{
  "hooks": {
    "EventName": [
      {
        "matcher": "tool name (optional, | separates several)",
        "hooks": [
          {
            "type": "command|prompt|subagent",
            "command": "..."
          }
        ]
      }
    ]
  }
}

5. Practical Recipes

Recipe 1: format automatically after a file is saved

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $FILEPATH 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

Recipe 2: block dangerous shell commands

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo $TOOL_INPUT | grep -qE 'rm -rf /|sudo rm|git push --force|DROP TABLE|DROP DATABASE' && echo 'dangerous command blocked' && exit 2 || exit 0"
          }
        ]
      }
    ]
  }
}

Recipe 3: protect sensitive files

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo $TOOL_INPUT | grep -qE '\\.env|\\.env\\.|credentials|secrets?\\.ya?ml|private.key' && echo 'access to a sensitive file blocked' && exit 2 || exit 0"
          }
        ]
      }
    ]
  }
}

Recipe 4: system notification when a task completes

macOS:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"$NOTIFICATION_MESSAGE\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Linux:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' \"$NOTIFICATION_MESSAGE\""
          }
        ]
      }
    ]
  }
}

Recipe 5: Slack notification

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST https://hooks.slack.com/services/xxx/yyy/zzz -H 'Content-Type: application/json' -d '{\"text\": \"Claude Code task finished\"}'"
          }
        ]
      }
    ]
  }
}

Recipe 6: ESLint auto-fix

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix $FILEPATH 2>/dev/null; npx prettier --write $FILEPATH 2>/dev/null; exit 0"
          }
        ]
      }
    ]
  }
}

Recipe 7: audit log

{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) | session=$SESSION_ID | tool=$TOOL_NAME | file=$FILEPATH\" >> ~/.claude/audit.log"
          }
        ]
      }
    ]
  }
}

Recipe 8: run tests automatically

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo $FILEPATH | grep -qE '\\.(ts|tsx|js|jsx)$' && echo $FILEPATH | grep -qvE '\\.test\\.|\\.spec\\.' && npx vitest related $FILEPATH --run 2>/dev/null || exit 0"
          }
        ]
      }
    ]
  }
}

Recipe 9: protect migration files

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo $FILEPATH | grep -qE 'migrations/|alembic/versions/' && echo 'editing an existing migration is not allowed, create a new one' && exit 2 || exit 0"
          }
        ]
      }
    ]
  }
}

Recipe 10: inject context at session start

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Important: this project is mid-way through a v3.0 rewrite. All new code must use React Server Components; the pages/ directory is no longer supported."
          }
        ]
      }
    ]
  }
}

6. Enterprise Hooks

managed-settings.d/ configuration

Enterprise administrators can enforce an organisation-wide security policy through the managed-settings.d/ directory:

# Create a policy file in the enterprise management directory
mkdir -p /etc/claude-code/managed-settings.d/
// /etc/claude-code/managed-settings.d/security.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/opt/claude-policy/check_command.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/opt/claude-policy/audit_log.sh"
          }
        ]
      }
    ]
  }
}

An enterprise policy cannot be overridden by user- or project-level configuration.


7. Debugging

Verbose mode

Press Ctrl+O to turn on verbose mode, which shows:

  • when each hook fires
  • the stdout/stderr of the hook script
  • the hook's exit code

Common problems

Problem Cause Fix
The hook never fires Typo in the matcher Check tool-name casing: Bash, Write, Edit, Read
The hook blocks normal work Exit-code logic is wrong Make sure the normal path is exit 0 and only block with exit 2
The hook is slow The script takes too long A hook should finish in 1–2 seconds; run long jobs in the background
An environment variable is empty That event does not provide it Check the table above to see which variables the event supplies

Test the hook script

Test it by hand in a terminal before putting it in the configuration:

# Simulate the PreToolUse environment variables
FILEPATH="src/main.ts" TOOL_INPUT="rm -rf /" bash -c 'echo $TOOL_INPUT | grep -qE "rm -rf" && echo "blocked" && exit 2 || exit 0'

8. Compared with Codex Hooks

Claude Code Hooks Codex Hooks
Number of events 17 Fewer
Configuration settings.json config.toml
Handler types command/prompt/subagent command
Enterprise management managed-settings.d/ None
Blocking exit code 2 Limited

Next Steps

Related Documents

Security Best Practices
Fully mastering Claude Code's security mechanisms — permission control, sensitive file protection, command interception, API key management
Getting Started with Claude Agent SDK
Learn how to build AI Agent applications using the Claude Agent SDK, connected via QCode.cc API
Automation & CI/CD
Master Claude Code headless mode — full flag reference, five CI/CD recipes, Docker isolation, session resumption and a Codex comparison
🚀
Get Started with QCode — Claude Code & Codex
One plan for both Claude Code and Codex, Asia-Pacific low latency
View Pricing Plans → Create Account
Team of 3+?
Enterprise: dedicated domain + sub-key management + ban protection, from ¥250/person/mo
Learn Enterprise →