- README: add spawn1337 section, update architecture diagram with orchestrator/detector, add spawn API endpoints, update test badge (1426 total) - ralph-wiggum-guide: add spawn-related files to references - All docs: update Last Updated dates to 2026-01-23 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
12 KiB
Claude Code Hooks Reference
Official documentation for Claude Code hooks system, extracted from code.claude.com.
Last Updated: 2026-01-23 Source: Claude Code Hooks Documentation
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"for bash or"prompt"for LLM-based evaluationcommand: Bash command to executeprompt: LLM prompt for evaluation (prompt-based hooks only)timeout: Optional timeout in seconds (default: 60)
Hook Events
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 readingTask- 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"
}
}
Notification
When: When Claude Code sends notifications.
Matchers:
permission_promptidle_promptauth_successelicitation_dialog
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 (Task tool call) finishes responding.
Use Cases: Control nested loops, verify subagent output.
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
For Stop and SubagentStop events, you 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: PreToolUse, PostToolUse, Stop
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