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
- Hook Events
- Configuration
- Matchers
- Exit Codes
- Input/Output Format
- Examples
- Best Practices
- Troubleshooting
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
| Aspect | CLAUDE.md | Hooks |
|---|---|---|
| Execution | Suggestions Claude may follow | Guaranteed execution |
| Flexibility | Context-dependent | Deterministic |
| Use case | Guidelines, preferences | Enforcement, automation |
Hook Events
Claude Code supports 13 hook event types:
| Event | When it fires | Matcher support |
|---|---|---|
PreToolUse | Before tool execution | Yes (tool names) |
PostToolUse | After tool completes | Yes (tool names) |
PermissionRequest | When permission dialog shows | Yes (tool names) |
UserPromptSubmit | When user submits prompt | No |
Stop | When main agent finishes | No |
SubagentStop | When subagent finishes | No |
Notification | When notification is sent | Yes (notification type) |
PreCompact | Before context compaction | Yes (manual, auto) |
SessionStart | When session starts/resumes | Yes (startup, resume, clear, compact) |
SessionEnd | When session terminates | No |
Setup | When --init, --init-only, --maintenance run | No |
TeammateIdle | When a teammate agent goes idle | Yes (teammate name) |
TaskCompleted | When a task is marked completed | Yes (task ID) |
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)
Configuration
Settings File Locations
| Location | Path | Scope |
|---|---|---|
| User | ~/.claude/settings.json | All projects |
| Project | .claude/settings.json | Shared with team |
| Local | .claude/settings.local.json | Personal, not committed |
Basic Structure
{
"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:
{
"type": "command",
"command": "/path/to/script.sh",
"timeout": 60
}Hook Configuration Options
| Option | Type | Description |
|---|---|---|
type | string | "command" or "prompt" |
command | string | Shell command to execute |
timeout | number | Max execution time in seconds (default: 60) |
once | boolean | Run hook only once per session (v2.1.0+) |
Example with once: true:
{
"type": "command",
"command": ".claude/hooks/expensive-check.sh",
"once": true
}Prompt Hooks
Use AI to evaluate (Stop, SubagentStop, UserPromptSubmit, PreToolUse, PermissionRequest):
{
"type": "prompt",
"prompt": "Evaluate if this code change is safe: $ARGUMENTS",
"timeout": 30
}Matchers
Matchers are case-sensitive patterns for tool names.
Pattern Types
| Pattern | Example | Matches |
|---|---|---|
| Exact | Write | Only Write tool |
| Regex OR | Write|Edit | Write or Edit |
| Wildcard | Notebook.* | 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
"matcher": "Bash(git commit:*)" // Any git commit
"matcher": "Bash(npm run:*)" // Any npm script
"matcher": "Bash(rm:*)" // Any rm commandNotification Types
"matcher": "permission_prompt" // Permission requests
"matcher": "idle_prompt" // Idle > 60 seconds
"matcher": "auth_success" // Authentication successExit Codes
| Code | Meaning | Behavior |
|---|---|---|
| 0 | Success | Continue, stdout shown in verbose mode |
| 2 | Block | Stop action, stderr shown to Claude/user |
| Other | Non-blocking error | Continue, stderr shown in verbose mode |
Exit Code 2 Behavior by Event
| Event | Effect |
|---|---|
| PreToolUse | Blocks tool, shows stderr to Claude |
| PermissionRequest | Denies permission |
| PostToolUse | Shows stderr to Claude (tool already ran) |
| UserPromptSubmit | Blocks prompt, erases it, shows stderr to user |
| Stop/SubagentStop | Blocks stop, shows stderr to Claude |
Input/Output Format
Input (via stdin)
All hooks receive JSON on stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse"
}PreToolUse Additional Fields
{
"tool_name": "Write",
"tool_input": { "file_path": "/path/file.txt", "content": "..." },
"tool_use_id": "toolu_01ABC..."
}PostToolUse Additional Fields
{
"tool_name": "Write",
"tool_input": { ... },
"tool_response": { "filePath": "/path/file.txt", "success": true },
"tool_use_id": "toolu_01ABC..."
}Output (JSON on stdout)
Common Fields
{
"continue": true,
"stopReason": "Message when continue=false",
"suppressOutput": true,
"systemMessage": "Warning to user"
}PreToolUse Output
{
"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:
#!/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 0settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(git commit:*)",
"hooks": [{
"type": "command",
"command": ".claude/hooks/block-secrets.sh"
}]
}
]
}
}2. Auto-Lint on File Save
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "npm run lint:fix --silent"
}]
}
]
}
}3. Quality Gate on Stop
.claude/hooks/quality-gate.sh:
#!/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{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [{
"type": "command",
"command": ".claude/hooks/quality-gate.sh"
}]
}
]
}
}4. Slack Notification on Completion
{
"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:
#!/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{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": ".claude/hooks/session-init.sh"
}]
}
]
}
}Best Practices
Security
- Validate all inputs - Never trust hook input blindly
- Quote shell variables - Always use
"$VAR"not$VAR - Block path traversal - Check for
..in file paths - Skip sensitive files - Avoid
.env,.git/, keys
Performance
- Set appropriate timeouts - Default is 60s
- Use fast scripts - Hooks run synchronously
- Cache when possible - Avoid repeated expensive operations
Reliability
- Use absolute paths - Specify full script paths
- Handle errors gracefully - Exit codes matter
- Test thoroughly - Test in safe environment first
- Log actions - For debugging
Organization
.claude/
├── hooks/
│ ├── pre-commit-check.sh
│ ├── post-edit-lint.sh
│ ├── quality-gate.sh
│ └── session-init.sh
└── settings.jsonTroubleshooting
Hook Not Executing
- Check matcher pattern (case-sensitive)
- Verify script is executable (
chmod +x) - Check file path is correct
- Enable verbose mode (
ctrl+o) to see output
Hook Blocking Unexpectedly
- Check exit code (should be 0 for success)
- Verify stderr is empty for non-blocking
- Test script independently
Environment Variables Not Set
For SessionStart, use $CLAUDE_ENV_FILE:
echo 'export VAR=value' >> "$CLAUDE_ENV_FILE"Common Issues
| Issue | Solution |
|---|---|
| Script not found | Use absolute path |
| Permission denied | chmod +x script.sh |
| JSON parse error | Validate JSON output |
| Timeout | Increase timeout or optimize script |
Related Documentation
- Skills - Model-invoked capabilities
- MCP - External tool integration
- Settings - Configuration options
- Commands - Slash commands
