- Advanced
- Hooks
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:
| Type | Trigger | Typical usage |
|---|---|---|
SessionStart | When a session starts | Check prerequisites, load config |
PreToolUse | Before a tool executes | Validate parameters, block actions |
PostToolUse | After a tool executes | Auto-format, notifications, logs |
Stop | When Claude ends the session | Session report, cleanup |
Notification | When Claude sends a notification | Slack alerts, emails |
PermissionRequest | Before a permission is requested | Auto-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:
| Variable | Available in | Content |
|---|---|---|
CLAUDE_TOOL_NAME | PreToolUse, PostToolUse | Tool name (Bash, Write, etc.) |
CLAUDE_TOOL_INPUT | PreToolUse | Tool's JSON parameters |
CLAUDE_TOOL_OUTPUT | PostToolUse | Tool result (stdout) |
CLAUDE_TOOL_OUTPUT_FILE | PostToolUse (Write, Edit) | Path of the written or modified file |
CLAUDE_SESSION_ID | All | Unique 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.shTOOL_INPUT=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys, json; d = json.load(sys.stdin); print(d.get('command', ''))")# Only trigger on git commitif echo "$TOOL_INPUT" | grep -q "git commit"; thenCOMMIT_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/nullfiexit 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.shLOG_FILE=~/.claude/sessions/$(date +%Y-%m-%d).logmkdir -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 runningif ! docker info > /dev/null 2>&1; thenecho "WARNING: Docker is not running. Container-related commands will fail." >&2fi# Show project context at startupecho "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.shTOOL=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys, jsond = json.load(sys.stdin)print(d.get('tool_name', ''))" 2>/dev/null)COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys, jsond = json.load(sys.stdin)print(d.get('command', ''))" 2>/dev/null)# Auto-approve read-only Git commandsSAFE_GIT_CMDS="git status|git diff|git log|git branch|git show"if echo "$COMMAND" | grep -qE "$SAFE_GIT_CMDS"; thenexit 0 # Automatically approvedfi# Require confirmation for git push and force operationsif echo "$COMMAND" | grep -qE "git push|git force|git reset --hard"; thenexit 1 # Requires manual confirmationfi# Default: let Claude Code handle itexit 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.shFILE="$CLAUDE_TOOL_OUTPUT_FILE"if [[ "$FILE" =~ \.(ts|tsx|js|jsx|json|css|scss)$ ]]; thennpx prettier --write "$FILE" 2>/dev/nullfiexit 0
Chained hooks: lint + format + test
Run a chain of checks after each file write:
#!/bin/bash# ~/.claude/hooks/quality-chain.shFILE="$CLAUDE_TOOL_OUTPUT_FILE"if [[ -z "$FILE" ]] || [[ ! -f "$FILE" ]]; thenexit 0fi# Step 1: Formatnpx 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)$ ]]; thennpx tsc --noEmit 2>/dev/null || truefiexit 0
Secret validation hook
Block any write containing secret patterns:
#!/bin/bash# ~/.claude/hooks/check-secrets.sh# PreToolUse hook on Write/EditFILE_CONTENT=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys, jsond = 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[@]}"; doif echo "$FILE_CONTENT" | grep -qE "$pattern"; thenecho "ERROR: Secret detected in content (pattern: $pattern)" >&2exit 1fidoneexit 0
Troubleshooting
My hook isn't triggering
- Check the exact matcher name (case-sensitive:
Bash, notbash) - Reload Claude Code: close and reopen the session
- Validate your
settings.jsonJSON:
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 installednpx prettier --write "$FILE"# Good: won't block even on errornpx 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 debuggingexec 2>> ~/.claude/hooks.logset -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
| Event | Description |
|---|---|
Setup | At the very first startup (before SessionStart). Ideal for fetching assets, checking auth |
SessionStart | Start of an interactive session |
SessionEnd | End of a session (normally completed, distinct from Stop) |
Stop | Claude finishes its turn or the session |
ConfigChange | Live changes to settings.json or permissions |
Tools and execution
| Event | Description |
|---|---|
PreToolUse | Before a tool runs (Bash, Write, etc.) |
PostToolUse | After a tool runs |
PermissionRequest | Before a permission request reaches the user |
UserPromptSubmit | User prompt submission (before Claude processes it) |
Notification | Notification emission (desktop push, channels) |
Subagents and orchestration
| Event | Description |
|---|---|
SubagentStart | A subagent starts (via Agent tool) |
SubagentStop | Subagent execution ends |
TeammateIdle | A teammate (in Agent Teams mode) goes idle |
TaskCompleted | A /tasks system task is marked complete |
Compaction and context
| Event | Description |
|---|---|
PreCompact | Before context auto-compaction |
PostCompact | After auto-compaction (summary available in output) |
ContextThreshold | Fill threshold reached (configurable, e.g., 50 %, 80 %) |
Worktrees and files
| Event | Description |
|---|---|
WorktreeCreate | A worktree is created (claude -w or isolation: worktree) |
WorktreeRemove | A worktree is removed |
FileWatcherTrigger | A watched file is modified (in watch mode) |
MCPServerConnect | Connection established with an MCP server |
MCPServerDisconnect | An MCP server disconnects |
Routines and cloud scheduling
| Event | Description |
|---|---|
RoutineStart | A cloud routine starts (/schedule cron) |
RoutineComplete | A cloud routine ends |
LoopIteration | One turn of a local /loop |
Errors and observability
| Event | Description |
|---|---|
ErrorOccurred | Unrecovered error (useful for Sentry) |
RateLimitHit | Token or request limit reached |
CostThreshold | Cost threshold reached (--max-budget-usd) |
Channels (push events)
| Event | Description |
|---|---|
ChannelMessage | Message received from Telegram/Discord/Slack via channels |
ChannelEvent | Non-message event from a channel |
Reserved / experimental
| Event | Description |
|---|---|
Heartbeat | Periodic tick (1/min, experimental) |
BackgroundAgentStart | A Background Agent starts |
BackgroundAgentStop | A 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:
| File | Scope | Versioned? |
|---|---|---|
.claude/hooks-config.json | Team (committed in the repo) | Yes |
.claude/hooks-config.local.json | Personal (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.