← cc-safe-setup

5 Hooks Every Claude Code User Should Install Today

April 21, 2026 · 8 min read · by yurukusa

TL;DR: These 5 hooks address the most common ways Claude Code causes real damage — file deletion, force-push, secret leaks, runaway compaction, and token waste. Each one is backed by a GitHub Issue. Run 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."

1 rm -rf Blocker PreToolUse · Bash

What it prevents

Claude running rm -rf on directories it shouldn't touch. This is the single most reported destructive action.

#49129 — 50 GB / 1,500 files permanently deleted. Claude ran rm -rf on a user's project directory. Everything gone.
#46058 — 3,467 files / ~7 GB deleted. Claude decided to "clean up" and wiped a project with a single command.

The hook

#!/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

Why it matters

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.

2 Force-Push Guard PreToolUse · Bash

What it prevents

Claude running git push --force to main or master, overwriting shared commit history that other people depend on.

#45893 — Force-push to main wiped team's work. Claude force-pushed to resolve a merge conflict. The team's commits from that day were gone from remote.

The hook

#!/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

Why it matters

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.

3 Secret Leak Prevention PreToolUse · Write / Edit

What it prevents

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.

Common pattern — .env values copied into source code. Claude reads .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.

The hook

#!/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

Why it matters

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.

4 Auto-Compact Circuit Breaker PreCompact

What it prevents

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.

#51088 — 15+ compactions in a single overnight session. Each compaction consumed tokens but failed to restore working context. The session made zero forward progress while burning through the budget.

The hook

#!/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

Why it matters

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.

5 Read-Once Guard PreToolUse · Read

What it prevents

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.

Common pattern — 30%+ token waste from re-reads. In long sessions, Claude frequently re-reads files it already has in context. It forgets it already read them, or reads them "just to be sure." Each re-read costs tokens.

The hook

#!/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

Why it matters

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.

Install All 5 in One Command

All 5 hooks ship with cc-safe-setup:
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'

How Hooks Work (30-Second Primer)

Claude Code fires events at specific points during operation. A hook is a shell script registered to an event. The two most useful events:

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.

Going Further

These 5 hooks handle the most common problems. For deeper coverage:

Install all 5 hooks now

One command. No configuration. Works with Claude Code v2.x.

npx cc-safe-setup Token Checkup

Tracking 78+ real incidents from GitHub Issues · Token optimization guide →