Claude Code Hook Builder
Purpose
Guide users through creating effective Claude Code hooks for tool validation, automation, and workflow enhancement. Auto-invokes when users want to create or configure hooks.
When to Use
Auto-invoke when users mention:
- •Creating hooks - "create hook", "make hook", "new hook", "add hook"
- •Hook events - "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SessionStart"
- •Validation - "validate", "check", "prevent", "block", "approve"
- •Automation - "auto-format", "auto-lint", "automatic", "trigger"
- •Hook configuration - "settings.json hooks", "hook matcher", "hook command"
Knowledge Base
- •Official docs:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks.md - •Hook guide:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks-guide.md - •Project guide:
.claude/docs/creating-components.md
Process
1. Gather Requirements
Ask the user:
Let me help you create a Claude Code hook! I need some details: 1. **What should this hook do?** Examples: - Auto-format code after editing files - Validate bash commands before execution - Add context when user submits prompts - Prevent access to sensitive files - Run tests after file changes 2. **When should it trigger?** - PreToolUse (before tool execution) - PostToolUse (after tool execution) - UserPromptSubmit (when user sends message) - Stop (when Claude finishes responding) - SubagentStop (when subagent finishes) - SessionStart (when session begins) - SessionEnd (when session ends) - Notification (when notification sent) - PermissionRequest (when permission requested) 3. **Which tools should it match?** - Specific tool (Write, Edit, Bash, Read, etc.) - Multiple tools (Write|Edit) - All tools (*) - MCP tools (mcp__server__tool) 4. **What should it return?** - Simple exit code (0 = success, 2 = block) - JSON with decision control - Additional context for Claude - Modified tool inputs 5. **Scope:** - User-level (`~/.claude/settings.json`) - Project-level (`.claude/settings.json`) - Local project (`.claude/settings.local.json`)
2. Determine Hook Type
Bash Command Hook:
{
"type": "command",
"command": "/path/to/script.sh"
}
- •Runs a shell command
- •Fast, deterministic
- •Good for validation, formatting
Prompt-based Hook:
{
"type": "prompt",
"prompt": "Evaluate if Claude should stop: $ARGUMENTS"
}
- •Uses LLM for decision
- •Context-aware, intelligent
- •Good for complex decisions (Stop, SubagentStop)
3. Choose Hook Event
PreToolUse
Runs before tool executes.
Use for:
- •Validate inputs
- •Auto-approve safe operations
- •Block dangerous commands
- •Modify tool parameters
JSON Output:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow" | "deny" | "ask",
"permissionDecisionReason": "Why this decision",
"updatedInput": {
"field": "new value"
}
}
}
PostToolUse
Runs after tool completes.
Use for:
- •Auto-format code
- •Run linters
- •Validate outputs
- •Log operations
JSON Output:
{
"decision": "block" | undefined,
"reason": "Why blocking",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Extra info for Claude"
}
}
UserPromptSubmit
Runs when user submits prompt.
Use for:
- •Add context automatically
- •Validate prompts
- •Block sensitive prompts
- •Inject current time/date
JSON Output:
{
"decision": "block" | undefined,
"reason": "Why blocking",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Extra context"
}
}
Stop / SubagentStop
Runs when Claude/subagent finishes.
Use for:
- •Verify tasks completed
- •Continue if work remains
- •Intelligent stoppage control
JSON Output:
{
"decision": "block" | undefined,
"reason": "Why must continue"
}
SessionStart
Runs when session starts.
Use for:
- •Load environment variables
- •Set up development context
- •Install dependencies
- •Inject initial context
JSON Output:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Initial context"
}
}
Special: Can persist environment variables:
#!/bin/bash if [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE" fi
SessionEnd
Runs when session ends.
Use for:
- •Cleanup tasks
- •Save session stats
- •Log session data
4. Create Hook Script
For bash command hooks, create a script:
Template:
#!/usr/bin/env bash # Read JSON input from stdin INPUT=$(cat) # Parse JSON (requires jq) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Your validation logic here if [[ condition ]]; then echo "Error message" >&2 exit 2 # Block operation fi # Success exit 0
Python Template:
#!/usr/bin/env python3
import json
import sys
# Read JSON input
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Your logic here
if condition:
# Block with error
print("Error message", file=sys.stderr)
sys.exit(2)
# Or return JSON for control
output = {
"decision": "approve",
"reason": "Auto-approved"
}
print(json.dumps(output))
sys.exit(0)
5. Configure in settings.json
Add hook configuration:
Basic Hook:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
}
]
}
]
}
}
Multiple Hooks:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh",
"timeout": 30
},
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
}
]
}
]
}
}
No Matcher (events without tools):
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-context.sh"
}
]
}
]
}
}
6. Hook Input Reference
Each event receives JSON on stdin:
Common fields:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/dir",
"permission_mode": "default",
"hook_event_name": "PostToolUse"
}
PreToolUse/PostToolUse:
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
},
"tool_response": { /* PostToolUse only */
"success": true
}
}
UserPromptSubmit:
{
"prompt": "User's submitted message"
}
Stop/SubagentStop:
{
"stop_hook_active": false
}
7. Exit Codes
- •
0: Success
- •stdout shown in verbose mode (Ctrl+O)
- •For UserPromptSubmit/SessionStart: stdout added to context
- •JSON parsed if present
- •
2: Blocking error
- •stderr shown to Claude
- •Operation blocked (behavior varies by event)
- •JSON in stdout ignored
- •
Other: Non-blocking warning
- •stderr shown in verbose mode
- •Execution continues
8. Test the Hook
Test script directly:
# Create test input
echo '{
"tool_name": "Write",
"tool_input": {
"file_path": "test.txt",
"content": "hello"
}
}' | .claude/hooks/your-hook.sh
# Check exit code
echo $?
Test in Claude Code:
1. Add hook to settings.json 2. Restart Claude Code 3. Run /hooks to verify it's loaded 4. Trigger the hook (e.g., write a file) 5. Check verbose mode (Ctrl+O) for output
Debug mode:
claude --debug # Shows hook execution details
9. Provide Configuration
Show the complete configuration:
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh",
"timeout": 60
}
]
}
]
}
}
Hook Examples
Example 1: Auto-Format Python Files
Hook script (.claude/hooks/format-python.sh):
#!/usr/bin/env bash
INPUT=$(cat)
# Get file path
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only process .py files
if [[ "$FILE_PATH" == *.py ]]; then
# Run black formatter
python -m black "$FILE_PATH" 2>&1
if [[ $? -eq 0 ]]; then
echo "Formatted: $FILE_PATH" >&2
fi
fi
exit 0
Configuration:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-python.sh"
}
]
}
]
}
}
Example 2: Validate Bash Commands
Hook script (.claude/hooks/validate-bash.py):
#!/usr/bin/env python3
import json
import sys
import re
# Dangerous patterns
DANGEROUS = [
(r'\brm\s+-rf\s+/', 'Dangerous: rm -rf on root'),
(r'>\s*/dev/sd[a-z]', 'Dangerous: writing to block device'),
(r'\bcurl\s+.*\|\s*bash', 'Dangerous: piping curl to bash'),
]
try:
data = json.load(sys.stdin)
except:
sys.exit(1)
if data.get('tool_name') != 'Bash':
sys.exit(0)
command = data.get('tool_input', {}).get('command', '')
# Check for dangerous patterns
for pattern, message in DANGEROUS:
if re.search(pattern, command):
print(f"⚠️ {message}", file=sys.stderr)
print(f"Command: {command}", file=sys.stderr)
sys.exit(2) # Block
sys.exit(0) # Allow
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
}
]
}
]
}
}
Example 3: Add Timestamp to Prompts
Hook script (.claude/hooks/add-timestamp.sh):
#!/usr/bin/env bash # Output current timestamp echo "Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')" exit 0
Configuration:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-timestamp.sh"
}
]
}
]
}
}
Example 4: Auto-Approve Documentation Reads
Hook script (.claude/hooks/auto-approve-docs.py):
#!/usr/bin/env python3
import json
import sys
data = json.load(sys.stdin)
if data.get('tool_name') != 'Read':
sys.exit(0)
file_path = data.get('tool_input', {}).get('file_path', '')
# Auto-approve docs
if any(file_path.endswith(ext) for ext in ['.md', '.txt', '.json', '.yaml']):
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Documentation file auto-approved"
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-approve-docs.py"
}
]
}
]
}
}
Example 5: Prevent Sensitive File Access
Hook script (.claude/hooks/block-secrets.sh):
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Block sensitive files
if [[ "$FILE_PATH" =~ \.env ||
"$FILE_PATH" =~ secrets/ ||
"$FILE_PATH" =~ \.aws/ ]]; then
echo "⛔ Access to sensitive file blocked: $FILE_PATH" >&2
exit 2
fi
exit 0
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Write|Edit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.sh"
}
]
}
]
}
}
Example 6: Intelligent Stop Hook (Prompt-based)
Configuration:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate whether Claude should stop. Context: $ARGUMENTS\n\nCheck if:\n1. All tasks are complete\n2. Tests are passing\n3. No errors need addressing\n\nRespond with JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}",
"timeout": 30
}
]
}
]
}
}
Example 7: Session Setup Hook
Hook script (.claude/hooks/session-setup.sh):
#!/usr/bin/env bash # Set up environment for session if [ -n "$CLAUDE_ENV_FILE" ]; then # Load nvm source ~/.nvm/nvm.sh nvm use 20 # Capture environment changes export -p >> "$CLAUDE_ENV_FILE" # Add custom variables echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE" fi # Add context echo "Development environment initialized" echo "Node version: $(node --version)" exit 0
Configuration:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-setup.sh"
}
]
}
]
}
}
Matcher Patterns
Exact match:
"matcher": "Write"
Multiple tools (regex):
"matcher": "Write|Edit|NotebookEdit"
All tools:
"matcher": "*"
Or:
"matcher": ""
MCP tools:
"matcher": "mcp__github__.*" "matcher": "mcp__.*__write.*"
Event-specific matchers:
Notification:
"matcher": "permission_prompt" "matcher": "idle_prompt"
PreCompact:
"matcher": "manual" "matcher": "auto"
SessionStart:
"matcher": "startup" "matcher": "resume" "matcher": "clear"
Environment Variables
Available in hook scripts:
- •
$CLAUDE_PROJECT_DIR- Absolute path to project root - •
$CLAUDE_CODE_REMOTE- "true" if remote/web, empty if local - •
$CLAUDE_ENV_FILE- (SessionStart only) File to persist env vars - •Standard environment variables
Best Practices
DO:
✅ Keep hooks fast (<100ms recommended)
✅ Provide clear error messages
✅ Use appropriate exit codes
✅ Quote variables in bash: "$VAR"
✅ Validate inputs before processing
✅ Test thoroughly before deploying
✅ Use $CLAUDE_PROJECT_DIR for portability
✅ Document what your hook does
DON'T:
❌ Run slow operations (full test suites) ❌ Block legitimate operations unnecessarily ❌ Use hooks for everything (be selective) ❌ Forget to handle errors ❌ Skip input validation ❌ Hardcode absolute paths ❌ Leave debug output in production
Security Considerations
⚠️ USE AT YOUR OWN RISK
Hooks execute arbitrary commands:
- •Can modify/delete any files
- •Can access sensitive data
- •Can cause data loss
- •Anthropic provides no warranty
Best practices:
- •Validate and sanitize inputs
- •Quote all variables
- •Block path traversal (
..) - •Use absolute paths
- •Skip sensitive files
- •Test in safe environment first
Troubleshooting
Hook Not Running
Check:
- •Hook is in
settings.jsoncorrectly - •Matcher pattern is correct (case-sensitive)
- •Script has execute permissions:
chmod +x script.sh - •Script shebang is correct:
#!/usr/bin/env bash - •Restart Claude Code after config changes
Debug:
# Run with debug mode claude --debug # Check hook execution in output # Shows: "Executing hooks for PostToolUse:Write"
Hook Errors
Check:
- •Script runs standalone:
echo '{}' | ./script.sh - •Exit code is correct:
echo $? - •JSON output is valid:
./script.sh | jq . - •Timeout is sufficient (default: 60s)
View errors:
- •Verbose mode: Ctrl+O
- •Debug mode:
claude --debug - •Check stderr output
Permissions Issues
Check:
# Make script executable chmod +x .claude/hooks/script.sh # Verify permissions ls -la .claude/hooks/
JSON Parse Errors
Validate JSON:
# Test JSON output
echo '{}' | ./script.sh | jq .
# Common issues:
# - Missing quotes
# - Trailing commas
# - Single quotes instead of double
Resources
- •Official Hook Docs:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks.md - •Hook Guide:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_hooks-guide.md - •Settings Reference:
.claude/skills/ai/claude-code/docs/code_claude_com/docs_en_settings.md - •Project Guide:
.claude/docs/creating-components.md