14 KiB
Claude Code Hooks Reference
Official documentation for Claude Code hooks system, extracted from code.claude.com.
Last Updated: 2026-07-25 Source: Claude Code Hooks Documentation
This is a maintained summary, not an exhaustive copy of the upstream reference. Check the source link for event-specific schemas before adding a new hook.
Overview
Hooks are automated scripts that execute at specific events during your Claude Code session. They allow you to:
- Validate, modify, or block tool usage
- Add context to prompts
- Implement custom workflows
- Control agent behavior
Configuration
Hooks are configured in settings files:
| File | Scope |
|---|---|
~/.claude/settings.json |
User (global) |
.claude/settings.json |
Project |
.claude/settings.local.json |
Local project (gitignored) |
| Plugin hook files | Plugin-specific |
Basic Structure
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
]
}
}
Key Fields:
matcher: Pattern to match tool names (case-sensitive, supports regex likeEdit|Writeor*for all)type:"command","http","mcp_tool","prompt", or"agent"where the event supports itcommand: Bash command to executeprompt: LLM prompt for evaluation (prompt-based hooks only)timeout: Optional timeout in seconds (default: 60)
Hook Events
Claude Code's current event surface is broader than the detailed subset below. In
particular, TeammateIdle and TaskCompleted are supported lifecycle events used
by Codeman; they are not stale or plugin-defined event names.
PreToolUse
When: After Claude creates tool parameters, before processing the tool call.
Use Cases: Approval, denial, or modification of tool calls.
Common Matchers:
Bash- Shell commandsWrite- File writingEdit- File editingRead- File readingAgent- Subagent tasksWebFetch,WebSearch- Web operationsmcp__<server>__<tool>- MCP tools
Output Control:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "string",
"updatedInput": {
"field_to_modify": "new value"
},
"additionalContext": "Context for Claude"
}
}
PermissionRequest
When: When the user is shown a permission dialog.
Use Cases: Auto-approve or deny permissions.
Output Control:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": {},
"message": "deny reason",
"interrupt": false
}
}
}
PostToolUse
When: Immediately after a tool completes successfully.
Use Cases: Provide feedback, run formatters/linters, log operations.
Output Control:
{
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Additional information"
}
}
Asynchronous Rewake
Command hooks can set "asyncRewake": true to run asynchronously and wake an
idle Claude turn when the hook exits with code 2. The hook's stderr is delivered
to Claude as a system reminder. This implies "async": true; ordinary async
hooks do not wake an idle turn, and their output waits for the next interaction.
Codeman uses this on PostToolUse(Bash): a self-contained Node helper extracts
the background task ID from the Bash result, watches the session transcript for
the matching completion notification, and exits 2. It does not send terminal
input, so it cannot submit a user's partially written prompt.
Notification
When: When Claude Code sends notifications.
Matchers:
permission_promptidle_promptauth_successelicitation_dialogelicitation_completeelicitation_response
UserPromptSubmit
When: When the user submits a prompt, before Claude processes it.
Use Cases: Add context, validate, or block prompts.
Output Control:
{
"decision": "block",
"reason": "Explanation",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "My additional context"
}
}
Stop
When: When the main Claude Code agent finishes responding.
Important: Does NOT run on user interrupt.
Use Cases: Ralph Wiggum loops - block exit and refeed prompt.
Output Control:
{
"decision": "block",
"reason": "Must provide when blocking"
}
Or to allow exit:
{
"continue": true,
"stopReason": "optional message"
}
Note: For Stop events, "continue": false takes precedence over "decision": "block".
SubagentStop
When: When a subagent (Agent tool call) finishes responding.
Use Cases: Control nested loops, verify subagent output.
TeammateIdle
When: When an agent-team teammate is about to go idle.
Use Cases: Reassign work, continue a teammate loop, or notify an orchestrator.
Matcher Support: None. The hook fires for every occurrence.
TaskCompleted
When: When a task is about to be marked completed.
Use Cases: Validate completion or forward team progress to an external UI.
Matcher Support: None. The hook fires for every occurrence.
PreCompact
When: Before a compact operation.
Matchers:
manual- Invoked from/compactauto- Invoked from auto-compact
SessionStart
When: When Claude Code starts or resumes a session.
Matchers:
startup- Fresh startresume- From--resume,--continue, or/resumeclear- From/clearcompact- From auto or manual compact
Use Cases: Load development context, set environment variables.
Persisting Environment Variables:
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export API_KEY=your-api-key' >> "$CLAUDE_ENV_FILE"
fi
exit 0
Output Control:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Context to load"
}
}
SessionEnd
When: When a session ends.
Reason Values:
clearlogoutprompt_input_exitother
Use Cases: Cleanup tasks, logging.
Hook Input
Hooks receive JSON via stdin with common fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {},
"tool_use_id": "toolu_01ABC123..."
}
Tool-Specific Input
Bash:
{
"tool_name": "Bash",
"tool_input": {
"command": "psql -c 'SELECT * FROM users'",
"description": "Query the users table",
"timeout": 120000
}
}
Write:
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
}
}
Edit:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.txt",
"old_string": "original text",
"new_string": "replacement text"
}
}
Hook Output
Exit Codes
| Code | Behavior |
|---|---|
| 0 | Success. stdout processed (shown in verbose or added as context) |
| 2 | Blocking error. Only stderr used. Blocks tool/prompt based on event |
| Other | Non-blocking error. stderr shown in verbose, execution continues |
JSON Output (Exit Code 0)
{
"continue": true,
"stopReason": "optional message",
"suppressOutput": true,
"systemMessage": "optional warning"
}
Prompt-Based Hooks
Prompt and agent handlers are supported by decision-oriented events including
PreToolUse, PermissionRequest, PostToolUse, PostToolUseFailure,
PostToolBatch, UserPromptSubmit, Stop, SubagentStop, TaskCreated, and
TaskCompleted. Check the upstream reference before choosing a handler type.
For example, a Stop event can use LLM-based evaluation:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Should Claude stop? Context: $ARGUMENTS\n\nCheck if all tasks are complete.",
"timeout": 30
}
]
}
]
}
}
LLM Response Format:
{
"ok": true,
"reason": "Explanation when ok is false"
}
Component-Scoped Hooks
Hooks can be defined in Skills, Agents, and Slash Commands using frontmatter:
---
name: secure-operations
hooks:
PreToolUse:
- matcher: 'Bash'
hooks:
- type: command
command: './scripts/security-check.sh'
---
These hooks:
- Are scoped to the component's lifecycle
- Only run when that component is active
- Support all hook events; a subagent-scoped
Stopis converted toSubagentStop
MCP Tools
MCP tools follow the pattern mcp__<server>__<tool>:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Memory operation' >> ~/mcp.log"
}
]
},
{
"matcher": "mcp__.*__write.*",
"hooks": [
{
"type": "command",
"command": "/home/user/scripts/validate-mcp-write.py"
}
]
}
]
}
}
Examples
Bash Command Validation
#!/usr/bin/env python3
import json
import re
import sys
VALIDATION_RULES = [
(r"\bgrep\b(?!.*\|)", "Use 'rg' instead of 'grep'"),
(r"\bfind\s+\S+\s+-name\b", "Use 'rg --files' instead of 'find -name'"),
]
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
command = tool_input.get("command", "")
if tool_name != "Bash" or not command:
sys.exit(1)
issues = []
for pattern, message in VALIDATION_RULES:
if re.search(pattern, command):
issues.append(message)
if issues:
for message in issues:
print(f"- {message}", file=sys.stderr)
sys.exit(2)
Auto-Approve Documentation Reads
#!/usr/bin/env python3
import json
import sys
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name == "Read":
file_path = tool_input.get("file_path", "")
if file_path.endswith((".md", ".mdx", ".txt", ".json")):
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Documentation file auto-approved"
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
Post-Write Formatter
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
}
Ralph Wiggum Stop Hook
#!/bin/bash
# ralph-stop-hook.sh
STATE_FILE=".claude/ralph-loop.local.md"
# Check if state file exists
if [ ! -f "$STATE_FILE" ]; then
exit 0 # No active loop, allow exit
fi
# Read state from YAML frontmatter
ENABLED=$(grep -m1 "^enabled:" "$STATE_FILE" | cut -d' ' -f2)
ITERATION=$(grep -m1 "^iteration:" "$STATE_FILE" | cut -d' ' -f2)
MAX_ITER=$(grep -m1 "^max-iterations:" "$STATE_FILE" | cut -d' ' -f2)
PROMISE=$(grep -m1 "^completion-promise:" "$STATE_FILE" | cut -d' ' -f2-)
# Check if disabled
if [ "$ENABLED" = "false" ]; then
exit 0
fi
# Check max iterations
if [ -n "$MAX_ITER" ] && [ "$ITERATION" -ge "$MAX_ITER" ]; then
exit 0
fi
# Check for completion promise in output
if [ -n "$PROMISE" ]; then
if echo "$CLAUDE_OUTPUT" | grep -q "<promise>$PROMISE</promise>"; then
exit 0
fi
fi
# Block exit, increment iteration
NEW_ITER=$((ITERATION + 1))
sed -i "s/^iteration:.*/iteration: $NEW_ITER/" "$STATE_FILE"
# Output block decision
echo '{"decision": "block", "reason": "Completion promise not found. Iteration '"$NEW_ITER"'."}'
exit 0
Environment Variables
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Project root directory |
CLAUDE_CODE_REMOTE |
"true" for web, empty for CLI |
CLAUDE_ENV_FILE |
Path to write persistent env vars (SessionStart) |
Debugging
Use claude --debug to see detailed hook execution:
[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Executing hook command: <command> with timeout 60000ms
[DEBUG] Hook command completed with status 0: <stdout>
Use /hooks command to view registered hooks and make changes.
Execution Details
- Timeout: 60-second default per hook, configurable
- Parallelization: All matching hooks run in parallel
- Deduplication: Identical commands deduplicated automatically
- Matchers: Only apply to tool-based hooks (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest)
Security Best Practices
- Validate and sanitize inputs - Never trust input data blindly
- Always quote shell variables - Use
"$VAR"not$VAR - Block path traversal - Check for
..in file paths - Use absolute paths - Specify full paths for scripts (use
$CLAUDE_PROJECT_DIR) - Skip sensitive files - Avoid
.env,.git/, keys, etc.
Source: Claude Code Hooks Documentation