Hook Recipes

Real problems. Copy-paste solutions. Each recipe is a working hook you can install in 10 seconds.

14 recipes

Permission & Approval

Stop permission prompts for read-only commands

Event: PreToolUse · Matcher: Bash · Reduces prompts ~80%
Auto-approve cat, ls, grep, git log, and 50+ other read-only commands.
npx cc-safe-setup --install-example auto-approve-readonly
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
BASE=$(echo "$COMMAND" | sed 's/^[A-Z_]*=[^ ]* //g' | awk '{print $1}' | sed 's|.*/||')
case "$BASE" in
    cat|head|tail|less|wc|grep|rg|find|locate|\
    ls|tree|stat|file|which|type|date|uptime|pwd|df|du)
        echo '{"decision":"approve","reason":"Read-only command"}'
        exit 0 ;;
esac
if echo "$COMMAND" | grep -qE '^\s*git\s+(status|log|diff|show|branch|remote|blame)\b'; then
    echo '{"decision":"approve","reason":"Read-only git"}'
    exit 0
fi
exit 0

Auto-approve test commands

Event: PreToolUse · Matcher: Bash
npm test, pytest, go test, cargo test, jest, vitest — all auto-approved.
npx cc-safe-setup --install-example auto-approve-test
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE '^\s*(npm\s+test|npx\s+(jest|vitest|mocha)|pytest|go\s+test|cargo\s+test)\b'; then
    echo '{"decision":"approve","reason":"Test runner"}'
    exit 0
fi
exit 0

Safety Guards

Block rm -rf, git reset --hard, and other destructive commands

Event: PreToolUse · Matcher: Bash · Prevents data loss
The #1 safety hook. Blocks commands that cause irreversible damage. Built after a user lost C:\Users to rm -rf following NTFS junctions.
npx cc-safe-setup
#!/bin/bash
# Built-in with cc-safe-setup. Blocks:
# rm -rf on /, ~, .., /home, /etc
# git reset --hard, git clean -fd
# chmod -R 777, find -delete on broad paths
# sudo + dangerous commands
# PowerShell Remove-Item -Recurse -Force
# See: https://github.com/yurukusa/cc-safe-setup

Prevent pushes to main/master

Event: PreToolUse · Matcher: Bash
Block direct pushes to main and force-pushes to any branch. Built-in with cc-safe-setup.
npx cc-safe-setup
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE 'git\s+push\s.*(main|master)\b'; then
    echo "BLOCKED: Push to main/master" >&2; exit 2
fi
if echo "$COMMAND" | grep -qE 'git\s+push\s.*--force'; then
    echo "BLOCKED: Force push" >&2; exit 2
fi
exit 0

Prevent committing secrets and .env files

Event: PreToolUse · Matcher: Bash
Block git add .env, credential files, and git add . when .env exists.
npx cc-safe-setup
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE 'git\s+add\s+.*\.env'; then
    echo "BLOCKED: Adding .env to git" >&2; exit 2
fi
if echo "$COMMAND" | grep -qE 'git\s+add\s+(-A|\.)' && [ -f ".env" ]; then
    echo "BLOCKED: git add . with .env present. Use git add " >&2; exit 2
fi
exit 0

Block silent memory file edits

Event: PreToolUse · Matcher: Edit|Write · Fixes Issue #38040
Memory files shape AI behavior across sessions but bypass permission prompts. This hook blocks unreviewed writes.
npx cc-safe-setup --install-example memory-write-guard
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
if [[ "$FILE" == *".claude/"*"/memory/"* ]]; then
    jq -n '{"decision":"block","reason":"Memory file edit requires review"}'
    exit 0
fi
exit 0

Block built-in skills that edit files silently

Event: PreToolUse · Matcher: Skill · Fixes Issue #38040
Built-in skills like update-config and simplify modify files without showing changes. Block them and use Edit tool directly.
npx cc-safe-setup --install-example skill-gate
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
[[ "$TOOL" != "Skill" ]] && exit 0
SKILL=$(echo "$INPUT" | jq -r '.tool_input.skill // empty')
case "$SKILL" in
    update-config|keybindings-help|simplify|statusline-setup)
        jq -n --arg s "$SKILL" '{"decision":"block","reason":"Skill " + $s + " edits files silently"}'
        exit 0 ;;
esac
exit 0

Code Quality

Auto-check syntax after every file edit

Event: PostToolUse · Matcher: Edit|Write
Catches Python, Shell, JSON, YAML, and JS syntax errors immediately after Claude edits a file.
npx cc-safe-setup
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
case "$FILE" in
    *.py) python3 -m py_compile "$FILE" 2>&1 && exit 0 || echo "SYNTAX ERROR in $FILE" >&2 ;;
    *.sh) bash -n "$FILE" 2>&1 && exit 0 || echo "SYNTAX ERROR in $FILE" >&2 ;;
    *.json) python3 -m json.tool "$FILE" >/dev/null 2>&1 && exit 0 || echo "INVALID JSON: $FILE" >&2 ;;
    *.yaml|*.yml) python3 -c "import yaml; yaml.safe_load(open('$FILE'))" 2>&1 && exit 0 || echo "INVALID YAML: $FILE" >&2 ;;
esac
exit 0

Warn about fixup/WIP commits before push

Event: PreToolUse · Matcher: Bash
Catches branches with fixup!, squash!, or WIP commits that should be squashed before pushing.
npx cc-safe-setup --install-example no-commit-fixup
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
echo "$COMMAND" | grep -qE '^\s*git\s+push\b' || exit 0
BASE=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null)
[ -n "$BASE" ] && git log --oneline "$BASE"..HEAD | grep -iE '^[a-f0-9]+ (fixup!|squash!|WIP)' && \
    echo "WARNING: Branch has fixup/WIP commits. Squash before pushing." >&2
exit 0

Verify file edits actually applied

Event: PostToolUse · Matcher: Edit|Write · Fixes Issue #32658
Claude assumes edits succeed. This hook checks: file exists, isn't empty, new_string is present, no conflict markers.
npx cc-safe-setup --install-example edit-verify
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[[ "$TOOL" != "Edit" && "$TOOL" != "Write" ]] && exit 0
[ -z "$FILE" ] || [ ! -f "$FILE" ] && { echo "WARNING: File missing: $FILE" >&2; exit 0; }
SIZE=$(wc -c < "$FILE")
[ "$SIZE" -eq 0 ] && echo "WARNING: File empty after edit: $FILE" >&2
if [ "$TOOL" = "Edit" ]; then
    FIRST=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' | head -1)
    [ -n "$FIRST" ] && ! grep -qF "$FIRST" "$FILE" && echo "WARNING: new_string not found in $FILE" >&2
fi
grep -qE '^(<<<<<<<|=======|>>>>>>>)' "$FILE" && echo "WARNING: Conflict markers in $FILE" >&2
exit 0

Auto-backup files before every edit

Event: PreToolUse · Matcher: Edit|Write · Enables rollback
Creates a timestamped backup in .claude/checkpoints/ before every edit. Keeps last 20 per file.
npx cc-safe-setup --install-example auto-git-checkpoint
#!/bin/bash
FILE=$(cat | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
mkdir -p .claude/checkpoints
cp "$FILE" ".claude/checkpoints/$(basename "$FILE").$(date +%H%M%S).bak"
ls -t ".claude/checkpoints/$(basename "$FILE")".*.bak | tail -n +21 | xargs rm -f
exit 0

Monitoring

Monitor context window usage

Event: PostToolUse · Matcher: (all) · Prevents silent context loss
Graduated warnings at 40%, 25%, 20%, 15% remaining context. Prevents the session from silently losing state.
npx cc-safe-setup
#!/bin/bash
# Built-in with cc-safe-setup
# Monitors context_window_tokens_remaining from PostToolUse events
# Warns at 40%, 25%, 20%, 15% thresholds
# At 15%: suggests compacting or handing off
# See: https://github.com/yurukusa/cc-safe-setup

Troubleshooting

Diagnose why hooks aren't working

CLI command · 13 automated checks
Checks jq, settings.json validity, hook paths, permissions, shebang, and runs test inputs.
npx cc-safe-setup --doctor
# Checks performed:
# 1. jq installed?
# 2. settings.json exists?
# 3. settings.json valid JSON?
# 4. hooks section present?
# 5. Each trigger type registered?
# 6. Each hook script file exists?
# 7. Each script is executable?
# 8. Each script has shebang?
# 9. Each script runs without error on empty input?
# 10. bypassPermissions not set (hooks get skipped)?
# 11. Bash(*) not in allow list?
# 12. hooks directory exists?
# 13. Claude Code version?

Verify a hook is actually firing

Add to any hook · Takes 10 seconds
Add this to the top of any hook to log when it fires. Check /tmp/hook-fire.log after running Claude Code.
Add to any .sh file:
#!/bin/bash
echo "[$(date)] Fired: $0" >> /tmp/hook-fire.log

# ... rest of your hook

Try It: Hook Simulator

Type a command to see which hooks would block, approve, or pass it through.