npx cc-safe-setup to install all of them in 10 seconds.
Claude Code hooks are shell scripts that run before (or after) every tool call. They can inspect the command about to execute and block it if it matches a dangerous pattern. The hook receives JSON on stdin and returns a decision on stdout.
These 5 hooks cover the incidents we see most often in anthropics/claude-code Issues. They're ordered from "prevents data loss" to "saves money."
Claude running rm -rf on directories it shouldn't touch. This is the single most reported destructive action.
rm -rf on a user's project directory. Everything gone.
#!/bin/bash
# prevent-rm-rf.sh
# Trigger: PreToolUse Matcher: Bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$CMD" ] && exit 0
# Block rm -rf targeting protected paths
if echo "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*[rR][a-zA-Z]*\s+)*(\/|~|\$HOME|\.git|\.ssh|\.env|\.gnupg|node_modules\/\.\.)'; then
echo '{"decision":"DENY","reason":"Blocked: rm -rf targeting protected path. Use specific file paths instead."}'
exit 0
fi
# Block rm -rf with wildcard at root-like paths
if echo "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*[rR][a-zA-Z]*\s+)\.\./'; then
echo '{"decision":"DENY","reason":"Blocked: rm -rf with parent directory traversal."}'
exit 0
fi
Once files are deleted with rm -rf, they're gone. No trash can, no undo. The hook blocks the command before it executes, so Claude sees a denial message and adjusts its approach.
Claude running git push --force to main or master, overwriting shared commit history that other people depend on.
#!/bin/bash
# prevent-force-push.sh
# Trigger: PreToolUse Matcher: Bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$CMD" ] && exit 0
# Block force-push to main/master
if echo "$CMD" | grep -qE 'git\s+push\s+.*--force.*\s+(origin\s+)?(main|master)'; then
echo '{"decision":"DENY","reason":"Blocked: force-push to main/master. Use a feature branch or --force-with-lease."}'
exit 0
fi
# Also catch the short form
if echo "$CMD" | grep -qE 'git\s+push\s+-f\s+.*\s*(main|master)'; then
echo '{"decision":"DENY","reason":"Blocked: force-push (-f) to main/master."}'
exit 0
fi
Force-push to a shared branch is one of the few git operations that destroys other people's work. Unlike a bad commit (which can be reverted), a force-push rewrites history. If nobody fetched the old commits, they're gone.
Claude writing API keys, tokens, or credentials into files that end up in version control. This happens more often than you'd expect — Claude copies a key from one file to another "for convenience," or hardcodes a token to make a test pass.
.env to understand config, then hardcodes OPENAI_API_KEY=sk-... directly into a Python file. That file gets committed. The key is now in git history permanently.
#!/bin/bash
# prevent-secret-leak.sh
# Trigger: PreToolUse Matcher: Write|Edit
INPUT=$(cat)
CONTENT=$(echo "$INPUT" | jq -r '
.tool_input.content //
.tool_input.new_string //
empty')
[ -z "$CONTENT" ] && exit 0
# Check for common secret patterns
if echo "$CONTENT" | grep -qE '(sk-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9\-]{20,}|xox[bpsa]-[a-zA-Z0-9\-]+)'; then
echo '{"decision":"DENY","reason":"Blocked: detected what looks like an API key or token. Use environment variables instead."}'
exit 0
fi
# Check for private key headers
if echo "$CONTENT" | grep -qE '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then
echo '{"decision":"DENY","reason":"Blocked: private key detected. Never write private keys into source files."}'
exit 0
fi
# Check for .env-style secrets being written to non-.env files
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" != *.env* ]] && echo "$CONTENT" | grep -qE '^[A-Z_]+=(sk-|AKIA|ghp_|password|secret)'; then
echo '{"decision":"DENY","reason":"Blocked: secret-like values being written outside .env file."}'
exit 0
fi
A leaked API key costs real money. AWS keys have been exploited within minutes of being pushed to public repos. Even in private repos, keys in git history are hard to fully remove. This hook catches the most common key formats before they hit disk.
Auto-compaction entering a death spiral — where each compaction fails to restore context, triggering the next one immediately. Users have lost entire overnight token budgets to this loop.
#!/bin/bash
# compact-circuit-breaker.sh
# Trigger: PreCompact (no matcher needed)
MAX_PER_HOUR="${CC_COMPACT_MAX_PER_HOUR:-3}"
MIN_INTERVAL="${CC_COMPACT_MIN_INTERVAL:-120}"
STATE_DIR="/tmp/.cc-compact-breaker"
STATE_FILE="$STATE_DIR/log"
mkdir -p "$STATE_DIR"
touch "$STATE_FILE"
NOW=$(date +%s)
ONE_HOUR_AGO=$((NOW - 3600))
# Clean entries older than 1 hour
awk -v cutoff="$ONE_HOUR_AGO" '$1 >= cutoff' "$STATE_FILE" > "$STATE_FILE.tmp"
mv "$STATE_FILE.tmp" "$STATE_FILE"
RECENT=$(wc -l < "$STATE_FILE" | tr -d ' ')
# Check last compaction time
LAST=0
[ -s "$STATE_FILE" ] && LAST=$(tail -1 "$STATE_FILE")
ELAPSED=$((NOW - LAST))
if [ "$RECENT" -ge "$MAX_PER_HOUR" ]; then
echo "CIRCUIT BREAKER: $RECENT compactions in the last hour (max $MAX_PER_HOUR). Start a new session." >&2
exit 2
fi
if [ "$ELAPSED" -lt "$MIN_INTERVAL" ] && [ "$LAST" -gt 0 ]; then
echo "COOLDOWN: Last compaction was ${ELAPSED}s ago (min ${MIN_INTERVAL}s)." >&2
exit 2
fi
echo "$NOW" >> "$STATE_FILE"
exit 0
A single compaction costs tokens (it re-summarizes your entire conversation). When compaction enters a loop, each iteration burns tokens with no useful output. Three compactions per hour is generous for normal use; anything beyond that is almost certainly a spiral.
Claude re-reading files that are already in its context window. Every time Claude reads a file, the full content is added to the conversation. Reading the same 500-line file 4 times means paying for 2,000 lines of tokens that add zero new information.
#!/bin/bash
# read-once-guard.sh
# Trigger: PreToolUse Matcher: Read
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE_PATH" ] && exit 0
FILE_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
STATE_DIR="/tmp/cc-read-once"
mkdir -p "$STATE_DIR"
STATE_FILE="$STATE_DIR/reads-$PPID.log"
MAX_READS="${CC_READ_ONCE_MAX:-3}"
# Get current file modification time
MTIME=$(stat -c %Y "$FILE_PATH" 2>/dev/null || echo "0")
# Look up previous reads
COUNT=0
PREV_MTIME="0"
if [ -f "$STATE_FILE" ]; then
while IFS=$'\t' read -r path mtime count; do
[ "$path" = "$FILE_PATH" ] && COUNT=$count && PREV_MTIME=$mtime && break
done < "$STATE_FILE"
fi
# Reset if file was modified
[ "$MTIME" != "$PREV_MTIME" ] && [ "$PREV_MTIME" != "0" ] && COUNT=0
COUNT=$((COUNT + 1))
# Update state
grep -v "^${FILE_PATH} " "$STATE_FILE" 2>/dev/null > "$STATE_FILE.tmp" || true
printf '%s\t%s\t%s\n' "$FILE_PATH" "$MTIME" "$COUNT" >> "$STATE_FILE.tmp"
mv "$STATE_FILE.tmp" "$STATE_FILE"
if [ "$COUNT" -gt "$MAX_READS" ]; then
echo "Token waste: ${FILE_PATH} read ${COUNT} times without changes. Content is already in context." >&2
fi
exit 0
Token costs add up fast on long sessions. This hook doesn't block the read (the default is warn-only), but the warning message reminds Claude to use what it already has. Set CC_READ_ONCE_MAX=2 for stricter sessions, or change exit 0 to exit 2 to hard-block re-reads.
npx cc-safe-setup
This adds 8 default hooks to your ~/.claude/settings.json (the 5 above plus syntax validation, git-reset-hard protection, and large-file-write warnings). No config files to create. No dependencies to install.
After installing, you can verify hooks are active:
cat ~/.claude/settings.json | jq '.hooks'
Claude Code fires events at specific points during operation. A hook is a shell script registered to an event. The two most useful events:
{"decision":"DENY","reason":"..."} to block it.Hooks go in ~/.claude/settings.json under the "hooks" key. Each hook specifies its trigger event, an optional matcher (which tool to intercept), and the command to run.
These 5 hooks handle the most common problems. For deeper coverage:
settings.json for token waste patterns and get specific hook recommendations.Install all 5 hooks now
One command. No configuration. Works with Claude Code v2.x.
npx cc-safe-setup Token CheckupTracking 78+ real incidents from GitHub Issues · Token optimization guide →