Skip to main content
Advanced

Hooks system

Automate your Claude Code workflows with the 32 hook events (PreToolUse, PostToolUse, SubagentStart, PermissionRequest, WorktreeCreate, etc.). Auto-format, notifications, session reports, advanced patterns.

What is a hook?

Hooks are shell commands triggered automatically by Claude Code at specific points in the execution cycle. Think of them as event listeners at the terminal level: Claude performs an action, your hook reacts.

Unlike MCPs (which extend Claude's capabilities) or Skills (which define behaviors), hooks intercept the execution flow to inject external logic.

The hook types

Claude Code exposes a rich lifecycle with 32 hook events. The first six cover 90 % of common usage:

TypeTriggerTypical usage
SessionStartWhen a session startsCheck prerequisites, load config
PreToolUseBefore a tool executesValidate parameters, block actions
PostToolUseAfter a tool executesAuto-format, notifications, logs
StopWhen Claude ends the sessionSession report, cleanup
NotificationWhen Claude sends a notificationSlack alerts, emails
PermissionRequestBefore a permission is requestedAuto-approve certain actions, route to a model

The exhaustive reference of all 32 events (useful for power users: subagents, worktrees, compaction, channels) is documented further down in the Complete reference section.

Configuration in settings.json

Hooks are declared in your settings.json file (~/.claude/settings.json for global, or .claude/settings.json for a project):

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'Validating before bash...' && exit 0"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$CLAUDE_TOOL_OUTPUT_FILE\" 2>/dev/null || true"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "echo 'Session ended' >> ~/.claude/session.log"
}
]
}
]
}
}

Hook structure

hooks:
  <HookType>:           # PreToolUse | PostToolUse | Stop | Notification
    - matcher: "<tool>"  # Tool name to intercept (optional for Stop/Notification)
      hooks:
        - type: "command" # Only type currently available
          command: "..."  # Shell command to execute

The matcher accepts the exact name of a Claude Code tool: Bash, Write, Edit, Read, WebFetch, etc. It's omitted for Stop, Notification, and SessionStart.

Available environment variables

Your hook commands receive several environment variables injected by Claude Code:

VariableAvailable inContent
CLAUDE_TOOL_NAMEPreToolUse, PostToolUseTool name (Bash, Write, etc.)
CLAUDE_TOOL_INPUTPreToolUseTool's JSON parameters
CLAUDE_TOOL_OUTPUTPostToolUseTool result (stdout)
CLAUDE_TOOL_OUTPUT_FILEPostToolUse (Write, Edit)Path of the written or modified file
CLAUDE_SESSION_IDAllUnique session identifier

Practical examples

PreToolUse: validate shell command parameters

This hook blocks any rm -rf command on dangerous paths:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo $CLAUDE_TOOL_INPUT | python3 -c \"import sys, json; cmd = json.load(sys.stdin).get('command', ''); exit(1 if 'rm -rf /' in cmd else 0)\""
}
]
}
]
}
}

PostToolUse: auto-format with Prettier

Automatically format every file written by Claude Code:

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

PostToolUse: Slack notification after a commit

First create the notification script:

#!/bin/bash
# ~/.claude/hooks/notify-slack.sh
TOOL_INPUT=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys, json; d = json.load(sys.stdin); print(d.get('command', ''))")
# Only trigger on git commit
if echo "$TOOL_INPUT" | grep -q "git commit"; then
COMMIT_MSG=$(git log --oneline -1 2>/dev/null || echo "Commit created")
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-type: application/json' \
-d "{\"text\": \":white_check_mark: Claude Code created a commit: \`$COMMIT_MSG\`\"}" > /dev/null
fi
exit 0

Then configure the hook in settings.json:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/notify-slack.sh"
}
]
}
]
}
}

Stop: generate a session report

Create the report script:

#!/bin/bash
# ~/.claude/hooks/session-report.sh
LOG_FILE=~/.claude/sessions/$(date +%Y-%m-%d).log
mkdir -p ~/.claude/sessions
{
echo "=== Session $(date '+%Y-%m-%d %H:%M:%S') ==="
echo "Session ID: $CLAUDE_SESSION_ID"
echo "Directory: $(pwd)"
echo "Git branch: $(git branch --show-current 2>/dev/null || echo 'N/A')"
echo ""
} >> "$LOG_FILE"
exit 0

Then in settings.json:

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/session-report.sh"
}
]
}
]
}
}

SessionStart: check prerequisites at startup

This hook verifies that Docker is running before starting a development session. If Docker is stopped, Claude knows about it from the start rather than discovering it mid-task.

#!/bin/bash
# ~/.claude/hooks/check-prerequisites.sh
# Check that Docker is running
if ! docker info > /dev/null 2>&1; then
echo "WARNING: Docker is not running. Container-related commands will fail." >&2
fi
# Show project context at startup
echo "Project: $(basename $(pwd))"
echo "Branch: $(git branch --show-current 2>/dev/null || echo 'N/A')"
echo "Node: $(node --version 2>/dev/null || echo 'Not installed')"
exit 0

Then in settings.json:

{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/check-prerequisites.sh"
}
]
}
]
}
}

PermissionRequest: manage approvals automatically

The PermissionRequest hook runs when Claude asks the user for permission. An exit code of 0 auto-approves, while 1 forces manual confirmation.

This script auto-approves read-only Git commands (safe) and blocks network write commands to require confirmation:

#!/bin/bash
# ~/.claude/hooks/smart-permissions.sh
TOOL=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('tool_name', ''))
" 2>/dev/null)
COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('command', ''))
" 2>/dev/null)
# Auto-approve read-only Git commands
SAFE_GIT_CMDS="git status|git diff|git log|git branch|git show"
if echo "$COMMAND" | grep -qE "$SAFE_GIT_CMDS"; then
exit 0 # Automatically approved
fi
# Require confirmation for git push and force operations
if echo "$COMMAND" | grep -qE "git push|git force|git reset --hard"; then
exit 1 # Requires manual confirmation
fi
# Default: let Claude Code handle it
exit 0
{
"hooks": {
"PermissionRequest": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/smart-permissions.sh"
}
]
}
]
}
}

Advanced patterns

Conditional hooks by file type

Only apply Prettier to TypeScript and JavaScript files:

#!/bin/bash
# ~/.claude/hooks/format-if-ts.sh
FILE="$CLAUDE_TOOL_OUTPUT_FILE"
if [[ "$FILE" =~ \.(ts|tsx|js|jsx|json|css|scss)$ ]]; then
npx prettier --write "$FILE" 2>/dev/null
fi
exit 0

Chained hooks: lint + format + test

Run a chain of checks after each file write:

#!/bin/bash
# ~/.claude/hooks/quality-chain.sh
FILE="$CLAUDE_TOOL_OUTPUT_FILE"
if [[ -z "$FILE" ]] || [[ ! -f "$FILE" ]]; then
exit 0
fi
# Step 1: Format
npx prettier --write "$FILE" 2>/dev/null || true
# Step 2: Lint (non-blocking)
npx eslint --fix "$FILE" 2>/dev/null || true
# Step 3: TypeScript check (non-blocking)
if [[ "$FILE" =~ \.(ts|tsx)$ ]]; then
npx tsc --noEmit 2>/dev/null || true
fi
exit 0

Secret validation hook

Block any write containing secret patterns:

#!/bin/bash
# ~/.claude/hooks/check-secrets.sh
# PreToolUse hook on Write/Edit
FILE_CONTENT=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('content', '') + d.get('new_string', ''))
" 2>/dev/null)
# Patterns to block (common API keys)
PATTERNS=(
"sk-[a-zA-Z0-9]{48}"
"AKIA[0-9A-Z]{16}"
"AIza[0-9A-Za-z-_]{35}"
"ghp_[a-zA-Z0-9]{36}"
)
for pattern in "${PATTERNS[@]}"; do
if echo "$FILE_CONTENT" | grep -qE "$pattern"; then
echo "ERROR: Secret detected in content (pattern: $pattern)" >&2
exit 1
fi
done
exit 0

Troubleshooting

My hook isn't triggering

  1. Check the exact matcher name (case-sensitive: Bash, not bash)
  2. Reload Claude Code: close and reopen the session
  3. Validate your settings.json JSON:
cat ~/.claude/settings.json | python3 -m json.tool

My hook blocks all actions

An unexpected exit 1 can block Claude. Add || true to non-critical commands:

# Bad: can block if npx isn't installed
npx prettier --write "$FILE"
# Good: won't block even on error
npx prettier --write "$FILE" 2>/dev/null || true

Debugging a hook

Log your hook's output to analyze its behavior:

#!/bin/bash
# Add these lines at the start of your script for debugging
exec 2>> ~/.claude/hooks.log
set -x # Enable trace mode (every command is logged)
# Your hook code...

The hook is too slow

Hooks block Claude Code's execution. For long operations, use background execution:

#!/bin/bash
# Launch the notification asynchronously
(curl -s -X POST "$SLACK_WEBHOOK_URL" -d '{"text":"Done!"}' > /dev/null 2>&1) &
exit 0 # Return immediately

Complete reference of the 32 events

The exhaustive list of hook events grouped by lifecycle phase. Most events share the same configuration format (matcher + command) and receive the same base environment variables (CLAUDE_SESSION_ID).

Session lifecycle

EventDescription
SetupAt the very first startup (before SessionStart). Ideal for fetching assets, checking auth
SessionStartStart of an interactive session
SessionEndEnd of a session (normally completed, distinct from Stop)
StopClaude finishes its turn or the session
ConfigChangeLive changes to settings.json or permissions

Tools and execution

EventDescription
PreToolUseBefore a tool runs (Bash, Write, etc.)
PostToolUseAfter a tool runs
PermissionRequestBefore a permission request reaches the user
UserPromptSubmitUser prompt submission (before Claude processes it)
NotificationNotification emission (desktop push, channels)

Subagents and orchestration

EventDescription
SubagentStartA subagent starts (via Agent tool)
SubagentStopSubagent execution ends
TeammateIdleA teammate (in Agent Teams mode) goes idle
TaskCompletedA /tasks system task is marked complete

Compaction and context

EventDescription
PreCompactBefore context auto-compaction
PostCompactAfter auto-compaction (summary available in output)
ContextThresholdFill threshold reached (configurable, e.g., 50 %, 80 %)

Worktrees and files

EventDescription
WorktreeCreateA worktree is created (claude -w or isolation: worktree)
WorktreeRemoveA worktree is removed
FileWatcherTriggerA watched file is modified (in watch mode)
MCPServerConnectConnection established with an MCP server
MCPServerDisconnectAn MCP server disconnects

Routines and cloud scheduling

EventDescription
RoutineStartA cloud routine starts (/schedule cron)
RoutineCompleteA cloud routine ends
LoopIterationOne turn of a local /loop

Errors and observability

EventDescription
ErrorOccurredUnrecovered error (useful for Sentry)
RateLimitHitToken or request limit reached
CostThresholdCost threshold reached (--max-budget-usd)

Channels (push events)

EventDescription
ChannelMessageMessage received from Telegram/Discord/Slack via channels
ChannelEventNon-message event from a channel

Reserved / experimental

EventDescription
HeartbeatPeriodic tick (1/min, experimental)
BackgroundAgentStartA Background Agent starts
BackgroundAgentStopA Background Agent ends

Team vs personal configuration

With this many events, the team/personal split becomes critical. Claude Code reads two hook files in this order:

FileScopeVersioned?
.claude/hooks-config.jsonTeam (committed in the repo)Yes
.claude/hooks-config.local.jsonPersonal (gitignored)No

Personal hooks (TTS, sounds, local audit) go in .local. Team hooks (lint enforcement, secret scan, centralized audit log) go in the versioned file.

# .gitignore
.claude/hooks-config.local.json

Next steps

Hooks are powerful on their own, but reach their full potential when combined with headless mode for CI/CD pipelines.