Create event-driven hooks for Claude Code automation. Use when the user wants to create hooks, automate tool validation, add pre/post processing, enforce security policies, or configure settings.json hooks. Triggers: create hook, build hook, PreToolUse, PostToolUse, event automation, tool validation, security hook
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/quality-hooks.mdexamples/security-hooks.mdexamples/workflow-hooks.mdreference/best-practices.mdreference/syntax-guide.mdreference/troubleshooting.mdtemplates/basic-hook.mdtemplates/production-hooks.mdtemplates/with-decisions.mdtemplates/with-prompts.mdtemplates/with-scripts.mdA comprehensive guide for creating Claude Code hooks — event-driven automation that monitors and controls Claude's actions.
| 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 | 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 |
~/.claude/settings.json → Personal hooks (all projects)
.claude/settings.json → Project hooks (team, committed)
.claude/settings.local.json → Local overrides (not committed)
| 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) |
/hooks # View active hooks
claude --debug # Enable debug logging
chmod +x script.sh # Make script executable
Use AskUserQuestion to clarify:
What event should trigger this hook?
What should happen when triggered?
Should it block, modify, or just observe?
What are the security implications?
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:
"Write|Edit""*" or omit matchermcp__server__tool patternBash(git:*) patternMatcher 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 |
Choose implementation approach:
Inline command (simple, no external file):
{
"type": "command",
"command": "echo \"$(date) | $tool_name\" >> ~/.claude/audit.log"
}
External script (complex logic, reusable):
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate.sh"
}
Prompt-based (LLM evaluation, intelligent decisions):
{
"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)
CRITICAL: Hooks execute shell commands with YOUR permissions.
Security Checklist:
"$VAR" not $VAR.., normalized)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:
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
{}{"tool_name": "Write"}{"tool_input": {"file_path": "; rm -rf /"}}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
Log without blocking — use PostToolUse or Notification.
{
"hooks": {
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"$(date) | $tool_name\" >> ~/.claude/audit.log"
}]
}]
}
}
Block dangerous actions — use PreToolUse or PermissionRequest.
{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "python3 ~/.claude/hooks/file-protector.py"
}]
}]
}
}
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))
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"}]
}]
}
}
# WRONG - exit 1 doesn't block
echo "Error" >&2
exit 1
# RIGHT - exit 2 blocks Claude
echo "BLOCKED: reason" >&2
exit 2
// WRONG - won't match "Write" tool
"matcher": "write"
// RIGHT - case-sensitive match
"matcher": "Write"
# WRONG - command injection vulnerability
rm $file_path
# RIGHT - properly quoted
rm -- "$file_path"
# WRONG - no shebang, may fail
set -euo pipefail
# RIGHT - explicit interpreter
#!/bin/bash
set -euo pipefail
# Don't forget!
chmod +x ~/.claude/hooks/my-hook.sh
// 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"
# 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; }
# WRONG - may log secrets
echo "Processing: $input" >> /tmp/debug.log
# RIGHT - sanitize before logging
echo "Processing tool: $tool_name" >> /tmp/debug.log
USE hooks for:
DON'T use hooks for:
templates/basic-hook.md — Single event, inline commandtemplates/with-scripts.md — External shell scriptstemplates/with-decisions.md — Permission control, input modificationtemplates/with-prompts.md — LLM-based evaluationtemplates/production-hooks.md — Complete multi-event systemexamples/security-hooks.md — Protection, validation, auditingexamples/quality-hooks.md — Formatting, linting, testingexamples/workflow-hooks.md — Setup, context, notificationsreference/syntax-guide.md — Complete JSON schemas, all eventsreference/best-practices.md — Security, design, team deploymentreference/troubleshooting.md — 10 common issues, testing methodology