Hooks Builder
A comprehensive guide for creating Claude Code hooks — event-driven automation that monitors and controls Claude's actions.
Quick Reference
The 10 Hook Events
| Event | When It Fires | Can Block? | Supports Matchers? |
|---|---|---|---|
| PreToolUse | Before tool executes | YES | YES (tool names) |
| PermissionRequest | Permission dialog shown | YES | YES (tool names) |
| PostToolUse | After tool succeeds | No | YES (tool names) |
| Notification | Claude sends notification | No | YES |
| UserPromptSubmit | User submits prompt | YES | No |
| Stop | Claude finishes responding | Can force continue | No |
| SubagentStop | Subagent finishes | Can force continue | No |
| PreCompact | Before context compaction | No | YES (manual/auto) |
| SessionStart | Session begins | No | YES (startup/resume/clear/compact) |
| SessionEnd | Session ends | No | No |
Exit Code Semantics
| Exit Code | Meaning | Effect |
|---|---|---|
| 0 | Success | stdout parsed as JSON for control |
| 2 | Blocking error | VETO — stderr shown to Claude |
| Other | Non-blocking error | stderr logged in debug mode |
Configuration Locations
~/.claude/settings.json → Personal hooks (all projects) .claude/settings.json → Project hooks (team, committed) .claude/settings.local.json → Local overrides (not committed)
Essential Environment Variables
| Variable | Description |
|---|---|
$CLAUDE_PROJECT_DIR | Project root directory |
$CLAUDE_CODE_REMOTE | Remote/local indicator |
$CLAUDE_ENV_FILE | Environment persistence path (SessionStart) |
$CLAUDE_PLUGIN_ROOT | Plugin directory (plugin hooks) |
Key Commands
/hooks # View active hooks claude --debug # Enable debug logging chmod +x script.sh # Make script executable
6-Phase Workflow
Phase 1: Requirements Gathering
Use AskUserQuestion to clarify:
- •
What event should trigger this hook?
- •Tool execution (Pre/Post/Permission) → PreToolUse, PostToolUse, PermissionRequest
- •User input → UserPromptSubmit
- •Response completion → Stop, SubagentStop
- •Session lifecycle → SessionStart, SessionEnd
- •Context management → PreCompact
- •Notifications → Notification
- •
What should happen when triggered?
- •Observe only (logging, metrics)
- •Block/allow based on conditions
- •Modify inputs before execution
- •Add context to prompts
- •Force continuation
- •
Should it block, modify, or just observe?
- •Observer: PostToolUse, Notification, SessionEnd (can't block)
- •Gatekeeper: PreToolUse, PermissionRequest, UserPromptSubmit (can block)
- •Transformer: PreToolUse with updatedInput (can modify)
- •Controller: Stop, SubagentStop (can force continue)
- •
What are the security implications?
- •Will it handle untrusted input?
- •Could it expose sensitive data?
- •Does it need to access external systems?
Phase 2: Event Selection
Match event to use case:
| Use Case | Best Event |
|---|---|
| Block dangerous operations | PreToolUse |
| Auto-format code after writes | PostToolUse |
| Validate user prompts | UserPromptSubmit |
| Setup environment | SessionStart |
| Ensure task completion | Stop |
| Log all tool usage | PostToolUse with "*" matcher |
| Protect sensitive files | PreToolUse for Write/Edit |
| Add project context | UserPromptSubmit |
Determine if matchers are needed:
- •Specific tools? → Use matcher:
"Write|Edit" - •All tools? → Use
"*"or omit matcher - •MCP tools? → Use
mcp__server__toolpattern - •Bash commands? → Use
Bash(git:*)pattern
Phase 3: Matcher Design
Matcher Pattern Syntax:
// Exact match (case-sensitive!) "matcher": "Write" // OR pattern "matcher": "Write|Edit" // Prefix match "matcher": "Notebook.*" // Contains match "matcher": ".*Read.*" // All tools "matcher": "*" // MCP tools "matcher": "mcp__memory__.*" // Bash sub-patterns "matcher": "Bash(git:*)"
Common Matcher Patterns:
| Pattern | Matches |
|---|---|
"Write" | Only Write tool |
"Write|Edit" | Write OR Edit |
"Bash" | All Bash commands |
"Bash(git:*)" | Only git commands |
"Bash(npm:*)" | Only npm commands |
"mcp__.*__.*" | All MCP tools |
".*" or "*" | Everything |
Phase 4: Implementation
Choose implementation approach:
- •
Inline command (simple, no external file):
json{ "type": "command", "command": "echo \"$(date) | $tool_name\" >> ~/.claude/audit.log" } - •
External script (complex logic, reusable):
json{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate.sh" } - •
Prompt-based (LLM evaluation, intelligent decisions):
json{ "type": "prompt", "prompt": "Analyze if all tasks are complete: $ARGUMENTS", "timeout": 30 }
Script Template (Bash):
#!/bin/bash
set -euo pipefail
# Read JSON input from stdin
input=$(cat)
# Parse fields with jq
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Your logic here
if [[ "$file_path" == *".env"* ]]; then
echo "BLOCKED: Cannot modify .env files" >&2
exit 2
fi
# Success - output decision
echo '{"decision": "approve"}'
exit 0
Script Template (Python):
#!/usr/bin/env python3
import sys
import json
# Read JSON input from stdin
data = json.load(sys.stdin)
# Extract fields
tool_name = data.get('tool_name', '')
tool_input = data.get('tool_input', {})
file_path = tool_input.get('file_path', '')
# Your logic here
if '.env' in file_path:
print("BLOCKED: Cannot modify .env files", file=sys.stderr)
sys.exit(2)
# Success - output decision
output = {"decision": "approve"}
print(json.dumps(output))
sys.exit(0)
Phase 5: Security Hardening
CRITICAL: Hooks execute shell commands with YOUR permissions.
Security Checklist:
- • All variables quoted:
"$VAR"not$VAR - • JSON parsed with jq or json.load (not grep/sed)
- • Paths validated (no
.., normalized) - • No sensitive data in logs/output
- • No sudo or privilege escalation
- • Script tested manually first
- • Project hooks audited before running
- • Timeout set appropriately
- • Error handling for all failure modes
Secure Patterns:
# UNSAFE - injection risk rm $file_path # SAFE - quoted, prevents flag injection rm -- "$file_path" # UNSAFE - parsing risk cat "$input" | grep "field" # SAFE - proper JSON parsing echo "$input" | jq -r '.field'
Defense in Depth:
- •Input validation (parse JSON properly)
- •Path sanitization (normalize, check boundaries)
- •Output sanitization (no sensitive data)
- •Fail-safe defaults (block on error, not allow)
- •Timeout protection (prevent infinite loops)
Phase 6: Testing
Step 1: Manual Script Testing
# Create mock input
cat > /tmp/mock-input.json << 'EOF'
{
"session_id": "test-123",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "test content"
}
}
EOF
# Test script
cat /tmp/mock-input.json | ./my-hook.sh
echo "Exit code: $?"
Step 2: Edge Case Testing
- •Empty inputs:
{} - •Missing fields:
{"tool_name": "Write"} - •Malicious inputs:
{"tool_input": {"file_path": "; rm -rf /"}} - •Large inputs: 10KB+ content
- •Unicode: paths with special characters
Step 3: Integration Testing
# Start Claude with debug mode claude --debug # Trigger the tool your hook targets # Watch debug output for hook execution
Step 4: Verification
# Check hooks are registered /hooks # Watch hook execution claude --debug 2>&1 | grep -i hook
Hook Patterns
Observer Pattern
Log without blocking — use PostToolUse or Notification.
{
"hooks": {
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"$(date) | $tool_name\" >> ~/.claude/audit.log"
}]
}]
}
}
Gatekeeper Pattern
Block dangerous actions — use PreToolUse or PermissionRequest.
{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "python3 ~/.claude/hooks/file-protector.py"
}]
}]
}
}
Transformer Pattern
Modify inputs before execution — use PreToolUse with updatedInput.
# In script, output:
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"content": add_license_header(original_content)
}
}
}
print(json.dumps(output))
Orchestrator Pattern
Coordinate multiple events — combine SessionStart + PreToolUse + PostToolUse.
{
"hooks": {
"SessionStart": [{
"matcher": "startup",
"hooks": [{"type": "command", "command": "~/.claude/hooks/setup-env.sh"}]
}],
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{"type": "command", "command": "~/.claude/hooks/validate.sh"}]
}],
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{"type": "command", "command": "~/.claude/hooks/format.sh"}]
}]
}
}
Common Pitfalls
1. Forgetting Exit Code 2 for Blocking
# WRONG - exit 1 doesn't block echo "Error" >&2 exit 1 # RIGHT - exit 2 blocks Claude echo "BLOCKED: reason" >&2 exit 2
2. Case Sensitivity in Matchers
// WRONG - won't match "Write" tool "matcher": "write" // RIGHT - case-sensitive match "matcher": "Write"
3. Unquoted Variables (Injection Risk)
# WRONG - command injection vulnerability rm $file_path # RIGHT - properly quoted rm -- "$file_path"
4. Missing Shebang in Scripts
# WRONG - no shebang, may fail set -euo pipefail # RIGHT - explicit interpreter #!/bin/bash set -euo pipefail
5. Not Making Scripts Executable
# Don't forget! chmod +x ~/.claude/hooks/my-hook.sh
6. Forgetting to Quote Paths in JSON
// WRONG - spaces in path will break "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh" // RIGHT - quoted path "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/script.sh"
7. No Error Handling
# WRONG - silent failures
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_name')
# RIGHT - handle errors
input=$(cat) || { echo "Failed to read input" >&2; exit 1; }
tool=$(echo "$input" | jq -r '.tool_name') || { echo "Failed to parse JSON" >&2; exit 1; }
8. Logging Sensitive Data
# WRONG - may log secrets echo "Processing: $input" >> /tmp/debug.log # RIGHT - sanitize before logging echo "Processing tool: $tool_name" >> /tmp/debug.log
When to Use Hooks
USE hooks for:
- •Security enforcement (block dangerous operations)
- •Code quality automation (format, lint on save)
- •Compliance and auditing (log all actions)
- •Environment setup (consistent configuration)
- •Workflow automation (notifications, integrations)
- •Input validation (prompt checking)
- •Task completion verification
DON'T use hooks for:
- •Adding new capabilities (use Skills)
- •Delegating complex work (use Agents)
- •User-invoked prompts (use Commands)
- •Simple one-off tasks (just ask Claude)
Files in This Skill
Templates (Progressive Complexity)
- •
templates/basic-hook.md— Single event, inline command - •
templates/with-scripts.md— External shell scripts - •
templates/with-decisions.md— Permission control, input modification - •
templates/with-prompts.md— LLM-based evaluation - •
templates/production-hooks.md— Complete multi-event system
Examples (18 Complete Hooks)
- •
examples/security-hooks.md— Protection, validation, auditing - •
examples/quality-hooks.md— Formatting, linting, testing - •
examples/workflow-hooks.md— Setup, context, notifications
Reference
- •
reference/syntax-guide.md— Complete JSON schemas, all events - •
reference/best-practices.md— Security, design, team deployment - •
reference/troubleshooting.md— 10 common issues, testing methodology