Another Useful Hook! Understanding Claude Code Hook Types and Input Structures
Executive Summary
Claude Code hooks have evolved significantly beyond basic command blocking. Understanding the full spectrum of hook types—from PreToolUse and PostToolUse to Notification, Stop, SubagentStop, PreCompact, UserPromptSubmit, SessionStart, and SessionEnd—unlocks powerful patterns for agent automation, validation, and observability.
This comprehensive guide explores all hook types, their stdin/stdout input variations, tool-specific inputs, and practical debugging techniques using jq. You’ll learn not just what hooks do, but how to read their input data and build complex validation pipelines.
Table of Contents
- Hook Architecture Fundamentals
- All Hook Types (8 Total)
- Hook Input Structures & JSON
- Stdin/Stdout Variations
- Tool-Specific Hook Inputs
- Practical Debugging with jq
- Real-World Implementation Examples
- Advanced Patterns
Hook Architecture Fundamentals
What Are Hooks?
Hooks are blocking interceptors that execute at specific lifecycle points in Claude Code. They sit between the agent’s intent and the actual action, allowing you to:
- Validate before/after tool execution
- Log every action for observability
- Block dangerous operations
- Transform data flowing through the system
- Notify external systems of state changes
Hook Execution Model
Agent Decision
↓
[PreToolUse Hook] ← Can block execution
↓
Tool Executes (Read, Write, Edit, Exec, etc.)
↓
[PostToolUse Hook] ← Can validate results
↓
[Notification Hook] ← Informational only (can't block)
↓
Agent Continues
Hook State & Context
Each hook receives:
- What changed (file path, command, etc.)
- Tool arguments (exact parameters passed)
- Workspace context (other files, directory structure)
- Agent metadata (model, session ID, iteration count)
All Hook Types (8 Total)
1. PreToolUse Hook
When it fires: Before any tool (Read, Write, Edit, Exec) executes
Can block: Yes (exit code 1)
Typical use: Permission checks, command validation, rate limiting
Configuration
hooks:
pre_tool_use:
- tools: [Write, Edit, Exec]
patterns: ["**/*.ts", "**/*.py"]
command: "python .claude/hooks/pre_tool_validator.py"
timeout_seconds: 5
Input to Hook
The hook receives via stdin a JSON object with:
{
"tool": "Write",
"file_path": "/Users/zorro/project/src/main.ts",
"arguments": {
"file_path": "/Users/zorro/project/src/main.ts",
"content": "export const greeting = 'hello';"
},
"workspace_path": "/Users/zorro/project",
"session_id": "session_abc123",
"model": "claude-opus-4-5",
"iteration": 3,
"agent_id": "main-agent"
}
Hook Response
exit 0 # Allow execution
# or
exit 1 # Block execution (stderr contains error message)
Example blocking hook:
#!/usr/bin/env python3
import json
import sys
data = json.loads(sys.stdin.read())
# Block writes to sensitive paths
blocked_paths = ['/etc', '/System', '/Applications']
if any(data['file_path'].startswith(p) for p in blocked_paths):
print(f"Blocked: Cannot write to {data['file_path']}", file=sys.stderr)
sys.exit(1)
sys.exit(0)
2. PostToolUse Hook
When it fires: After a tool completes successfully
Can block: Yes (exit code 1 causes agent to receive error)
Typical use: Result validation, file format checking, consistency verification
Configuration
hooks:
post_tool_use:
- tools: [Write, Edit]
patterns: ["**/*.json", "**/*.yaml"]
command: "bash .claude/hooks/validate_format.sh"
Input to Hook
{
"tool": "Write",
"file_path": "/Users/zorro/project/config.json",
"arguments": {
"file_path": "/Users/zorro/project/config.json",
"content": "{\"name\": \"app\", \"version\": \"1.0.0\"}"
},
"result": {
"status": "success",
"file_size": 42,
"created_at": "2026-03-09T14:22:45Z"
},
"workspace_path": "/Users/zorro/project"
}
Example: JSON Validation Hook
#!/bin/bash
HOOK_INPUT=$(cat)
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.file_path')
# Validate JSON structure
if ! jq empty "$FILE_PATH" 2>/dev/null; then
echo "Invalid JSON in $FILE_PATH" >&2
exit 1
fi
# Ensure required fields exist
if ! jq -e '.name and .version' "$FILE_PATH" >/dev/null; then
echo "Missing required fields: name, version" >&2
exit 1
fi
exit 0
3. Notification Hook
When it fires: After any significant action (informational only)
Can block: No (exit code ignored)
Typical use: Logging, alerting, external system updates
Configuration
hooks:
notification:
- tools: [Write, Edit, Exec]
command: "bash .claude/hooks/log_action.sh"
Input to Hook
{
"event_type": "tool_complete",
"tool": "Exec",
"command": "npm test",
"status": "success",
"output": "Tests passed: 42/42",
"timestamp": "2026-03-09T14:22:45Z",
"session_id": "session_abc123"
}
Example: Audit Logging
#!/bin/bash
HOOK_INPUT=$(cat)
# Log to audit file
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $HOOK_INPUT" >> .claude/audit.log
# Send to external service (non-blocking)
if command -v curl &> /dev/null; then
curl -X POST \
-H "Content-Type: application/json" \
-d "$HOOK_INPUT" \
"https://logs.example.com/claude-code" \
& # Background, don't wait
fi
exit 0
4. Stop Hook
When it fires: When agent reaches stop or max iterations
Can block: Yes (can modify completion behavior)
Typical use: Autonomous loop continuation, final validation, report generation
Configuration
hooks:
stop:
- patterns: ["**/*.ts"]
command: "bash .claude/hooks/final_validation.sh"
timeout_seconds: 30
Input to Hook
{
"reason": "user_stop",
"session_id": "session_abc123",
"iterations": 12,
"max_iterations": 20,
"files_modified": [
"src/main.ts",
"src/utils.ts",
"test/main.test.ts"
],
"total_tokens_used": 45000,
"completion_promise": "All tests pass",
"completion_status": "pending"
}
Example: Ralph Loop Pattern
#!/bin/bash
HOOK_INPUT=$(cat)
# Parse JSON
ITERATIONS=$(echo "$HOOK_INPUT" | jq -r '.iterations')
MAX_ITERATIONS=$(echo "$HOOK_INPUT" | jq -r '.max_iterations')
COMPLETION_STATUS=$(echo "$HOOK_INPUT" | jq -r '.completion_status')
# If not done and iterations remain, signal to continue
if [ "$COMPLETION_STATUS" != "complete" ] && [ "$ITERATIONS" -lt "$MAX_ITERATIONS" ]; then
echo '{"action": "continue", "reason": "Not done yet, continuing autonomous loop"}'
exit 0
else
echo '{"action": "stop", "reason": "Task complete or max iterations reached"}'
exit 0
fi
5. SubagentStop Hook
When it fires: When a sub-agent completes
Can block: Yes (can require remediation)
Typical use: Sub-agent result validation, error recovery, orchestration logic
Configuration
hooks:
subagent_stop:
- command: "python .claude/hooks/subagent_validator.py"
timeout_seconds: 10
Input to Hook
{
"subagent_id": "writer-agent",
"subagent_label": "Write markdown article",
"completion_reason": "complete",
"status": "success",
"iterations": 8,
"tokens_used": 12000,
"files_generated": ["article.md"],
"errors": [],
"output_summary": "Article written with proper formatting"
}
Example: Sub-Agent Validation
#!/usr/bin/env python3
import json
import sys
data = json.loads(sys.stdin.read())
# Validate required outputs exist
required_files = ['article.md']
for required_file in required_files:
if required_file not in data['files_generated']:
print(f"ERROR: Sub-agent did not generate {required_file}", file=sys.stderr)
sys.exit(1)
# Check for errors
if data['errors']:
print(f"Sub-agent had errors: {data['errors']}", file=sys.stderr)
sys.exit(1)
# Log successful completion
print(f"✓ Sub-agent {data['subagent_id']} completed successfully", file=sys.stdout)
sys.exit(0)
6. PreCompact Hook
When it fires: Before Claude Code compacts/truncates context
Can block: Yes (exit code 1 cancels compaction)
Typical use: Memory checkpointing, state preservation, checkpoint validation
Configuration
hooks:
pre_compact:
- command: "python .claude/hooks/checkpoint_handler.py"
timeout_seconds: 10
Input to Hook
{
"reason": "context_limit_approaching",
"current_tokens": 95000,
"max_tokens": 100000,
"session_id": "session_abc123",
"files_in_context": ["src/main.ts", "src/utils.ts", "config.json"],
"recent_changes": ["src/main.ts", "src/utils.ts"]
}
Example: Checkpoint Management
#!/usr/bin/env python3
import json
import sys
from datetime import datetime
data = json.loads(sys.stdin.read())
# Create checkpoint before compaction
checkpoint = {
"timestamp": datetime.now().isoformat(),
"reason": data['reason'],
"tokens_used": data['current_tokens'],
"files_modified": data['recent_changes']
}
# Save checkpoint
with open('.claude/checkpoints.jsonl', 'a') as f:
f.write(json.dumps(checkpoint) + '\n')
print(f"✓ Checkpoint created before compaction", file=sys.stdout)
sys.exit(0)
7. UserPromptSubmit Hook
When it fires: When user submits a new message/prompt
Can block: Yes (can reject invalid inputs)
Typical use: Input validation, prompt safety checks, instruction injection prevention
Configuration
hooks:
user_prompt_submit:
- command: "python .claude/hooks/prompt_validator.py"
Input to Hook
{
"prompt_text": "Write a file that deletes /tmp/*",
"user_id": "user_abc123",
"session_id": "session_abc123",
"model": "claude-opus-4-5",
"is_edit": false,
"context_window_size": 45000
}
Example: Prompt Injection Prevention
#!/usr/bin/env python3
import json
import sys
import re
data = json.loads(sys.stdin.read())
prompt = data['prompt_text'].lower()
# Block dangerous instruction injections
dangerous_patterns = [
r'ignore.*previous.*instruction',
r'admin.*mode',
r'system.*prompt',
r'delete.*production',
r'rm.*-rf'
]
for pattern in dangerous_patterns:
if re.search(pattern, prompt):
print(f"Prompt rejected: Contains potentially dangerous instruction", file=sys.stderr)
sys.exit(1)
print(f"✓ Prompt validation passed", file=sys.stdout)
sys.exit(0)
8. SessionStart & SessionEnd Hooks
When it fires: At session initialization (Start) and completion (End)
Can block: Start hook yes, End hook no
Typical use: Session setup/teardown, initialization checks, final reporting
Configuration
hooks:
session_start:
- command: "bash .claude/hooks/session_init.sh"
session_end:
- command: "bash .claude/hooks/session_cleanup.sh"
SessionStart Input
{
"session_id": "session_abc123",
"model": "claude-opus-4-5",
"workspace_path": "/Users/zorro/project",
"user_id": "user_abc123",
"capabilities": ["read", "write", "edit", "exec"],
"max_iterations": 50
}
SessionEnd Input
{
"session_id": "session_abc123",
"duration_seconds": 1800,
"total_iterations": 25,
"total_tokens_used": 78000,
"completion_reason": "user_stop",
"final_status": "success",
"files_modified": 5,
"errors_encountered": 0
}
Example: Session Setup & Reporting
#!/bin/bash
HOOK_TYPE=$1
HOOK_INPUT=$(cat)
if [ "$HOOK_TYPE" = "start" ]; then
echo "📍 Session starting..."
WORKSPACE=$(echo "$HOOK_INPUT" | jq -r '.workspace_path')
# Initialize workspace state
mkdir -p "$WORKSPACE/.claude/state"
echo '{"started": true}' > "$WORKSPACE/.claude/state/session.json"
elif [ "$HOOK_TYPE" = "end" ]; then
echo "🏁 Session ending..."
DURATION=$(echo "$HOOK_INPUT" | jq -r '.duration_seconds')
TOKENS=$(echo "$HOOK_INPUT" | jq -r '.total_tokens_used')
echo "Duration: ${DURATION}s | Tokens: ${TOKENS}"
fi
Hook Input Structures & JSON
Universal Hook Structure
All hooks receive a JSON object with:
{
"hook_type": "pre_tool_use",
"timestamp": "2026-03-09T14:22:45Z",
"session_id": "session_abc123",
"workspace_path": "/Users/zorro/project",
"[hook-specific-fields]": {}
}
Common Fields Across Hooks
| Field | Type | Description |
|---|---|---|
session_id |
string | Unique session identifier |
timestamp |
ISO8601 | When hook fired |
workspace_path |
string | Project root directory |
model |
string | Claude model in use |
iteration |
number | Current iteration count |
Tool-Specific Fields in PreToolUse
{
"tool": "Write|Edit|Read|Exec",
"arguments": {
// For Write:
"file_path": "string",
"content": "string",
// For Edit:
"file_path": "string",
"old_text": "string",
"new_text": "string",
// For Read:
"file_path": "string",
// For Exec:
"command": "string",
"cwd": "string"
}
}
Stdin/Stdout Variations
How Hooks Receive Input
There are three patterns:
Pattern 1: stdin JSON (Most Common)
Hook receives JSON via stdin:
#!/bin/bash
# Read from stdin
HOOK_INPUT=$(cat)
echo "$HOOK_INPUT" | jq '.file_path'
Configuration:
command: "bash my_hook.sh"
Pattern 2: Command-Line Arguments
Hook receives file path as argument:
#!/bin/bash
# Receive via argv
FILE_PATH=$1
cat "$FILE_PATH" | jq '.'
Configuration:
command: "bash my_hook.sh {file_path}"
# or
command: "bash my_hook.sh {arguments.file_path}"
Pattern 3: Environment Variables
Hook receives metadata via env vars:
#!/bin/bash
# Receive via env
echo "Session: $CLAUDE_SESSION_ID"
echo "Workspace: $CLAUDE_WORKSPACE"
echo "Tool: $CLAUDE_TOOL"
Claude Code sets these automatically:
CLAUDE_SESSION_IDCLAUDE_WORKSPACECLAUDE_TOOLCLAUDE_FILE_PATHCLAUDE_ITERATION
Hook Response Patterns
Pattern 1: Exit Code Only
exit 0 # Success/Allow
exit 1 # Failure/Block
Pattern 2: Exit Code + stderr
echo "Detailed error message" >&2
exit 1
Pattern 3: Exit Code + stdout
echo '{"status": "ok", "data": {...}}'
exit 0
Pattern 4: File-Based Response
# Write response to file
echo '{"action": "continue"}' > .claude/hook_response.json
exit 0
Tool-Specific Hook Inputs
Write Hook Input
{
"tool": "Write",
"file_path": "/Users/zorro/project/src/utils.ts",
"arguments": {
"file_path": "/Users/zorro/project/src/utils.ts",
"content": "export function add(a: number, b: number): number {\n return a + b;\n}"
},
"file_extension": ".ts",
"file_size_bytes": 65,
"will_overwrite": false,
"parent_directory_exists": true
}
Edit Hook Input
{
"tool": "Edit",
"file_path": "/Users/zorro/project/src/utils.ts",
"arguments": {
"file_path": "/Users/zorro/project/src/utils.ts",
"old_text": "export function add(a, b) {",
"new_text": "export function add(a: number, b: number): number {"
},
"old_text_line_number": 1,
"old_text_length": 28,
"new_text_length": 51
}
Read Hook Input
{
"tool": "Read",
"file_path": "/Users/zorro/project/src/utils.ts",
"arguments": {
"file_path": "/Users/zorro/project/src/utils.ts"
},
"file_size_bytes": 500,
"file_exists": true,
"readable": true
}
Exec Hook Input
{
"tool": "Exec",
"arguments": {
"command": "npm test",
"cwd": "/Users/zorro/project"
},
"command": "npm test",
"working_directory": "/Users/zorro/project",
"shell": "bash",
"timeout_seconds": 300
}
Practical Debugging with jq
jq Basics for Hook Debugging
Pretty-print incoming JSON
#!/bin/bash
cat | jq '.'
Extract single field
echo "$HOOK_INPUT" | jq '.file_path'
# Output: "/Users/zorro/project/src/main.ts"
Check if field exists
echo "$HOOK_INPUT" | jq 'has("file_path")'
# Output: true
Filter with conditions
# Check if file extension is .ts
echo "$HOOK_INPUT" | jq '.file_path | endswith(".ts")'
# Output: true
Array operations
# Check if array contains value
echo "$HOOK_INPUT" | jq '.files_modified | contains(["main.ts"])'
Extract multiple fields
echo "$HOOK_INPUT" | jq '{tool, file_path, timestamp}'
# Output: {"tool": "Write", "file_path": "...", "timestamp": "..."}
Debugging Hook Failures
Create a debugging hook
#!/bin/bash
set -e
# Log all incoming data
HOOK_INPUT=$(cat)
echo "=== HOOK DEBUG ===" >&2
echo "$HOOK_INPUT" | jq '.' >&2
echo "=================" >&2
# Parse and validate
TOOL=$(echo "$HOOK_INPUT" | jq -r '.tool')
FILE=$(echo "$HOOK_INPUT" | jq -r '.file_path')
if [ -z "$TOOL" ] || [ -z "$FILE" ]; then
echo "ERROR: Missing required fields" >&2
exit 1
fi
echo "✓ Tool: $TOOL, File: $FILE" >&2
exit 0
Log hook executions
#!/bin/bash
HOOK_INPUT=$(cat)
TIMESTAMP=$(echo "$HOOK_INPUT" | jq -r '.timestamp')
TOOL=$(echo "$HOOK_INPUT" | jq -r '.tool')
# Append to hook log
echo "[$TIMESTAMP] $TOOL Hook Executed" >> .claude/hook_debug.log
echo "$HOOK_INPUT" | jq '.' >> .claude/hook_debug.log
echo "---" >> .claude/hook_debug.log
exit 0
Filter and validate JSON
#!/bin/bash
HOOK_INPUT=$(cat)
# Validate JSON structure
if ! echo "$HOOK_INPUT" | jq 'has("tool") and has("file_path")' | grep -q true; then
echo "ERROR: Invalid hook input structure" >&2
exit 1
fi
# Extract for processing
TOOL=$(echo "$HOOK_INPUT" | jq -r '.tool')
FILE=$(echo "$HOOK_INPUT" | jq -r '.file_path')
# Complex validation
if echo "$HOOK_INPUT" | jq -e '.arguments | keys | length > 0' >/dev/null; then
echo "✓ Hook has arguments" >&2
fi
exit 0
jq Advanced Patterns
Conditional logic
# If tool is Exec, check command
echo "$HOOK_INPUT" | jq '
if .tool == "Exec" then
.arguments.command
else
.file_path
end
'
Map and filter
# Get all modified file paths
echo "$HOOK_INPUT" | jq '
.files_modified |
map(select(endswith(".ts")))
'
Group by type
# Group errors by severity
echo "$HOOK_INPUT" | jq '
group_by(.severity) |
map({severity: .[0].severity, count: length})
'
Real-World Implementation Examples
Example 1: Comprehensive TypeScript Validator Hook
#!/usr/bin/env python3
"""
Complete TypeScript validation hook.
- Runs ESLint pre-flight checks
- Validates types with TypeScript
- Logs all validations for debugging
"""
import json
import sys
import subprocess
from pathlib import Path
from datetime import datetime
class TypeScriptValidator:
def __init__(self, hook_input: dict):
self.data = hook_input
self.file_path = self.data['file_path']
self.errors = []
self.warnings = []
def validate(self) -> bool:
"""Run all validation steps."""
self._validate_syntax()
self._validate_types()
self._validate_style()
return len(self.errors) == 0
def _validate_syntax(self):
"""Check TypeScript syntax."""
try:
result = subprocess.run(
['npx', 'tsc', '--noEmit', self.file_path],
capture_output=True,
timeout=5
)
if result.returncode != 0:
self.errors.append(f"TypeScript compilation error: {result.stderr.decode()}")
except Exception as e:
self.warnings.append(f"Could not run TypeScript check: {str(e)}")
def _validate_types(self):
"""Validate type annotations."""
with open(self.file_path) as f:
content = f.read()
if 'function' in content and ': ' not in content:
self.warnings.append("Functions may be missing type annotations")
def _validate_style(self):
"""Check code style with ESLint."""
try:
result = subprocess.run(
['npx', 'eslint', self.file_path],
capture_output=True,
timeout=5
)
if result.returncode != 0:
self.warnings.append(f"ESLint issues: {result.stdout.decode()}")
except Exception as e:
self.warnings.append(f"Could not run ESLint: {str(e)}")
def report(self):
"""Print results and return exit code."""
if self.errors:
print(f"❌ TypeScript Validation Failed for {self.file_path}", file=sys.stderr)
for error in self.errors:
print(f" ERROR: {error}", file=sys.stderr)
return 1
if self.warnings:
print(f"⚠️ TypeScript Warnings for {self.file_path}", file=sys.stderr)
for warning in self.warnings:
print(f" WARNING: {warning}", file=sys.stderr)
print(f"✓ TypeScript validation passed: {self.file_path}", file=sys.stdout)
return 0
if __name__ == '__main__':
try:
hook_input = json.loads(sys.stdin.read())
validator = TypeScriptValidator(hook_input)
if not validator.validate():
exit_code = validator.report()
sys.exit(exit_code)
else:
exit_code = validator.report()
sys.exit(exit_code)
except Exception as e:
print(f"Hook error: {str(e)}", file=sys.stderr)
sys.exit(1)
Example 2: Database Query Deduplication Hook
#!/bin/bash
"""
PostToolUse hook that finds duplicate database queries.
Fires after Write/Edit tools complete.
"""
HOOK_INPUT=$(cat)
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.file_path')
TOOL=$(echo "$HOOK_INPUT" | jq -r '.tool')
# Only check TypeScript/JavaScript files
if ! [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then
exit 0
fi
# Extract all SQL queries from file
QUERIES=$(grep -oE 'db\.query\(`[^`]+`' "$FILE_PATH" 2>/dev/null || echo "")
if [ -z "$QUERIES" ]; then
exit 0
fi
# Create temp file of query hashes
TEMP_HASHES=$(mktemp)
echo "$QUERIES" | sort | uniq -d > "$TEMP_HASHES"
if [ -s "$TEMP_HASHES" ]; then
echo "⚠️ Found duplicate queries in $FILE_PATH:" >&2
cat "$TEMP_HASHES" | head -5 >&2
rm "$TEMP_HASHES"
# Don't block, just warn
exit 0
fi
rm "$TEMP_HASHES"
exit 0
Example 3: Autonomous Loop with Stop Hook
#!/usr/bin/env python3
"""
Stop hook implementing Ralph Wiggum pattern.
Continues autonomous agent loop if completion promise not met.
"""
import json
import sys
from datetime import datetime
class AutonomousLoopManager:
def __init__(self, hook_input: dict):
self.data = hook_input
self.iterations = hook_input['iterations']
self.max_iterations = hook_input['max_iterations']
self.completion_promise = hook_input.get('completion_promise', '')
self.completion_status = hook_input.get('completion_status', 'pending')
def should_continue(self) -> bool:
"""Determine if agent should continue."""
# Stop if max iterations reached
if self.iterations >= self.max_iterations:
return False
# Stop if completion promise met
if self.completion_status == 'complete':
return False
# Stop if user explicitly stopped
if self.data.get('reason') == 'user_stop':
return False
# Continue if promise not met
return True
def generate_response(self) -> dict:
"""Generate hook response."""
if self.should_continue():
return {
'action': 'continue',
'reason': f'Continuing autonomous loop (iteration {self.iterations}/{self.max_iterations})',
'next_task': 'Run failing tests and fix issues'
}
else:
return {
'action': 'stop',
'reason': 'Task complete or max iterations reached',
'summary': f'Completed in {self.iterations} iterations'
}
if __name__ == '__main__':
try:
hook_input = json.loads(sys.stdin.read())
manager = AutonomousLoopManager(hook_input)
response = manager.generate_response()
print(json.dumps(response), file=sys.stdout)
sys.exit(0)
except Exception as e:
print(f"Error in stop hook: {str(e)}", file=sys.stderr)
sys.exit(0) # Don't block on hook errors
Advanced Patterns
Pattern 1: Multi-Stage Validation Pipeline
hooks:
pre_tool_use:
- tools: [Write]
patterns: ["**/*.ts"]
command: "python .claude/hooks/stage1_syntax.py"
timeout_seconds: 5
post_tool_use:
- tools: [Write]
patterns: ["**/*.ts"]
command: "python .claude/hooks/stage2_types.py"
timeout_seconds: 10
stop:
- command: "python .claude/hooks/stage3_integration.py"
timeout_seconds: 30
Why: Fast failures early, comprehensive checks at completion.
Pattern 2: Conditional Hooks Based on File Type
#!/bin/bash
HOOK_INPUT=$(cat)
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.file_path')
if [[ "$FILE_PATH" =~ \.ts$ ]]; then
python .claude/hooks/typescript_validator.py <<< "$HOOK_INPUT"
elif [[ "$FILE_PATH" =~ \.py$ ]]; then
python .claude/hooks/python_validator.py <<< "$HOOK_INPUT"
elif [[ "$FILE_PATH" =~ \.json$ ]]; then
jq empty "$FILE_PATH" || exit 1
fi
exit $?
Pattern 3: Hook Chaining with State
#!/usr/bin/env python3
"""
Hook that chains multiple validations, maintaining state.
"""
import json
import sys
from pathlib import Path
class StatefulValidator:
STATE_FILE = '.claude/hook_state.json'
def __init__(self):
self.state = self.load_state()
def load_state(self) -> dict:
if Path(self.STATE_FILE).exists():
with open(self.STATE_FILE) as f:
return json.load(f)
return {'validations': 0, 'errors': 0}
def save_state(self):
Path(self.STATE_FILE).parent.mkdir(exist_ok=True)
with open(self.STATE_FILE, 'w') as f:
json.dump(self.state, f)
def validate(self, hook_input: dict) -> bool:
self.state['validations'] += 1
# Run validation logic
result = self.check_file(hook_input['file_path'])
if not result:
self.state['errors'] += 1
self.save_state()
return result
def check_file(self, file_path: str) -> bool:
# Validation logic here
return True
if __name__ == '__main__':
hook_input = json.loads(sys.stdin.read())
validator = StatefulValidator()
if validator.validate(hook_input):
sys.exit(0)
else:
sys.exit(1)
Pattern 4: External System Notifications
#!/bin/bash
"""
Notification hook that sends to Slack/email.
Non-blocking pattern.
"""
HOOK_INPUT=$(cat)
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.file_path')
TOOL=$(echo "$HOOK_INPUT" | jq -r '.tool')
TIMESTAMP=$(echo "$HOOK_INPUT" | jq -r '.timestamp')
# Format message
MESSAGE="Claude Code Action: $TOOL on $FILE_PATH at $TIMESTAMP"
# Send to Slack (background)
if [ -n "$SLACK_WEBHOOK" ]; then
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$MESSAGE\"}" \
"$SLACK_WEBHOOK" &
fi
# Send email (background)
if command -v mail &>/dev/null; then
echo "$MESSAGE" | mail -s "Claude Code Event" admin@example.com &
fi
# Always exit 0 - notifications don't block
exit 0
Hook Configuration Best Practices
1. Timeout Configuration
hooks:
pre_tool_use:
- tools: [Write]
command: "bash validate.sh"
timeout_seconds: 5 # Don't hang the agent
Rule of thumb:
- Syntax checks: 2-5 seconds
- Type checks: 5-10 seconds
- Complex analysis: 10-30 seconds
- Integration tests: 30-60 seconds
2. Error Handling
#!/usr/bin/env python3
import sys
import json
try:
hook_input = json.loads(sys.stdin.read())
result = validate(hook_input)
if not result:
print("Validation failed", file=sys.stderr)
sys.exit(1)
sys.exit(0)
except json.JSONDecodeError as e:
print(f"Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
# Don't block agent on hook errors
sys.exit(0)
3. Performance Optimization
#!/bin/bash
# Only run expensive checks on important files
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.file_path')
if [[ "$FILE_PATH" =~ (api|core|auth) ]]; then
python comprehensive_validator.py "$FILE_PATH"
else
python lightweight_validator.py "$FILE_PATH"
fi
4. Observability
#!/bin/bash
HOOK_LOG=".claude/hooks.log"
{
echo "=== Hook Execution ==="
echo "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Input:"
cat | tee /tmp/hook_input.json
echo ""
echo "Result: $?"
} >> "$HOOK_LOG" 2>&1
Common Hook Mistakes & Solutions
Mistake 1: Blocking on Network I/O
# ❌ WRONG - Hook hangs if API is slow
response = requests.get('https://api.example.com/validate')
# ✅ CORRECT - Use timeout or background task
try:
response = requests.get('https://api.example.com/validate', timeout=2)
except requests.Timeout:
print("API timeout, skipping validation", file=sys.stderr)
sys.exit(0) # Don't block agent
Mistake 2: Not Handling Large Files
# ❌ WRONG - File too large to load into memory
cat "$FILE_PATH" | jq '.'
# ✅ CORRECT - Stream or check size first
if [ $(stat -f%z "$FILE_PATH") -gt 10000000 ]; then
echo "File too large, skipping detailed validation" >&2
exit 0
fi
jq '.' "$FILE_PATH"
Mistake 3: JSON Parsing Errors
# ❌ WRONG - Assumes valid JSON
data = json.loads(sys.stdin.read())
# ✅ CORRECT - Handle parse errors
try:
data = json.loads(sys.stdin.read())
except json.JSONDecodeError as e:
print(f"Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
Conclusion
Claude Code hooks provide a sophisticated mechanism for embedding validation, observability, and control directly into the agent execution workflow. By understanding:
- All 8 hook types and their lifecycle positions
- JSON input structures and how to parse them with jq
- Tool-specific inputs for Read, Write, Edit, and Exec
- Debugging techniques for hook development
- Real-world patterns from validation to autonomous loops
You can build robust, production-grade agent systems that catch errors automatically, log everything, and gracefully handle edge cases.
The key insight: Hooks aren’t just safety guardrails—they’re the foundation of trustworthy agentic systems. Invest in them early.
Article Created: 2026-03-09
Format: Comprehensive markdown with JSON examples, bash/Python implementations, jq patterns, and debugging guides
Status: Ready for Obsidian vault (manual copy required)
Tags: Claude Code, Hooks, Validation, Debugging, jq, Architecture
Source
Full Course: Claude Code in Action
Note: Signup required to access full course content and video lessons
Course Topics:
- Introduction to Claude Code and AI assistants
- Project setup and context management
- Custom commands and MCP servers
- GitHub integration
- Hooks and SDK
- Quiz and summary
Related Chapters:
- Useful Hooks! (TypeScript type checking, query duplication prevention)
- Another Useful Hook! (Hook types and input structures)
- The Claude Code SDK (Programmatic integration)