Loading...
Loading...
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
npx skill4agent add mike-coulbourn/claude-vibes hooks-builder| 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 |
|---|---|
| Project root directory |
| Remote/local indicator |
| Environment persistence path (SessionStart) |
| Plugin directory (plugin hooks) |
/hooks # View active hooks
claude --debug # Enable debug logging
chmod +x script.sh # Make script executable| 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 |
| Protect sensitive files | PreToolUse for Write/Edit |
| Add project context | UserPromptSubmit |
"Write|Edit""*"mcp__server__toolBash(git:*)// 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:*)"| Pattern | Matches |
|---|---|
| Only Write tool |
| Write OR Edit |
| All Bash commands |
| Only git commands |
| Only npm commands |
| All MCP tools |
| Everything |
{
"type": "command",
"command": "echo \"$(date) | $tool_name\" >> ~/.claude/audit.log"
}{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate.sh"
}{
"type": "prompt",
"prompt": "Analyze if all tasks are complete: $ARGUMENTS",
"timeout": 30
}#!/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#!/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)"$VAR"$VAR..# 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'# 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: $?"{}{"tool_name": "Write"}{"tool_input": {"file_path": "; rm -rf /"}}# Start Claude with debug mode
claude --debug
# Trigger the tool your hook targets
# Watch debug output for hook execution# Check hooks are registered
/hooks
# Watch hook execution
claude --debug 2>&1 | grep -i hook{
"hooks": {
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo \"$(date) | $tool_name\" >> ~/.claude/audit.log"
}]
}]
}
}{
"hooks": {
"PreToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "python3 ~/.claude/hooks/file-protector.py"
}]
}]
}
}# In script, output:
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"content": add_license_header(original_content)
}
}
}
print(json.dumps(output)){
"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.logtemplates/basic-hook.mdtemplates/with-scripts.mdtemplates/with-decisions.mdtemplates/with-prompts.mdtemplates/production-hooks.mdexamples/security-hooks.mdexamples/quality-hooks.mdexamples/workflow-hooks.mdreference/syntax-guide.mdreference/best-practices.mdreference/troubleshooting.md