Real problems. Copy-paste solutions. Each recipe is a working hook you can install in 10 seconds.
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
# 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?
#!/bin/bash
echo "[$(date)] Fired: $0" >> /tmp/hook-fire.log
# ... rest of your hook
Type a command to see which hooks would block, approve, or pass it through.