Hook Lifecycle
Prompt → PreToolUse → Tool Executes → PostToolUse → Stop
↑ block here ↑ check here ↑ log here
Exit Codes
| Code | Meaning |
0 | Allow (or no opinion) |
2 | Block — tool call cancelled |
| other | Error (treated as allow) |
Hook Events
| Event | Matcher | Use |
| PreToolUse | Bash | Block commands |
| PostToolUse | Edit|Write | Syntax check |
| PostToolUse | (empty) | All tools |
| Stop | (empty) | Session end |
| UserPromptSubmit | — | Validate input |
settings.json Structure
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "~/.claude/hooks/guard.sh"
}]
}]
}
}
Minimal Block Hook
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE 'PATTERN'; then
echo "BLOCKED: reason" >&2
exit 2
fi
exit 0
Auto-Approve Hook
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE '^\s*git\s+(status|log|diff)'; then
jq -n '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}'
fi
exit 0
PostToolUse Syntax Check
#!/bin/bash
FILE=$(cat | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
case "${FILE##*.}" in
py) python3 -m py_compile "$FILE" 2>&1 ;;
sh) bash -n "$FILE" 2>&1 ;;
json) jq empty "$FILE" 2>&1 ;;
js) node --check "$FILE" 2>&1 ;;
esac
exit 0
Modify Input
#!/bin/bash
# Strip comments from bash commands
COMMAND=$(cat | jq -r '.tool_input.command // empty')
CLEAN=$(echo "$COMMAND" | sed '/^#/d; /^$/d')
[ "$CLEAN" = "$COMMAND" ] && exit 0
jq -n --arg cmd "$CLEAN" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": {"command": $cmd}
}
}'
stdin JSON Reference
| Event | Key Fields |
| PreToolUse (Bash) | .tool_input.command |
| PreToolUse (Edit) | .tool_input.file_path |
| PostToolUse | .tool_input.file_path |
| Stop | .stop_reason |
| UserPromptSubmit | .prompt |
Test a Hook
# Manual test
echo '{"tool_input":{"command":"rm -rf /"}}' \
| bash ~/.claude/hooks/guard.sh
echo $? # 2 = blocked
# Auto-test
npx cc-hook-test ~/.claude/hooks/guard.sh
Quick Setup Commands
npx cc-safe-setup | Install 8 hooks |
--create "desc" | Generate hook |
--audit | Safety score |
--lint | Config analysis |
--doctor | Diagnose issues |
--watch | Live dashboard |
--stats | Block statistics |
--verify | Test all hooks |
--export | Share with team |
Common Patterns
| Block | Pattern |
| rm -rf / | rm\s+(-[rf]+\s+)*/ |
| force push | git\s+push.*--force |
| push main | git\s+push.*main |
| .env commit | git\s+add.*\.env |
| git reset | git\s+reset\s+--hard |
| DB wipe | migrate:fresh|DROP\s+DB |