Stop it deleting things

You'll finish with: rm -rf, DROP TABLE and force pushes blocked at the door.

The other half. This one watches commands rather than files, and stops the ones you can't undo.

What gets stopped

🗑️rm -rfRecursive delete
🧨DROP TABLEWipes a database
↩️reset --hardThrows away work
⬆️push --forceOverwrites history

Four commands. All of them permanent.

Archive beats delete

The rule worth teaching your whole setup: nothing gets deleted, things get moved to _archive. Disk space is cheap. A client folder that vanished on a Friday is not.

Do this now
  1. Save the script below as .claude/hooks/no-delete.sh
  2. Run chmod +x .claude/hooks/*.sh again.
  3. Add the Bash matcher block to your settings.json alongside the first one.
#!/bin/bash
# Blocks destructive commands. FAILS CLOSED.
INPUT=$(cat)

VALUE=""
PARSED=0
for PY in python3 python py; do
  command -v "$PY" >/dev/null 2>&1 || continue
  OUT=$(printf '%s' "$INPUT" | "$PY" -c "
import sys,json
try: print(json.load(sys.stdin).get('tool_input',{}).get('command','') or '')
except Exception: sys.exit(9)" 2>/dev/null) || continue
  VALUE="$OUT"
  PARSED=1
  break
done

if [ "$PARSED" -ne 1 ]; then
  echo "Blocked: this hook could not read its input, so it cannot tell whether" >&2
  echo "the action is safe. It needs a working python3 (or python) on PATH." >&2
  echo "Install Python, then try again. Blocking is deliberate — see the Gates course." >&2
  exit 2
fi

CMD="$VALUE"
[ -z "$CMD" ] && exit 0
if printf '%s' "$CMD" | grep -qE '(^|[[:space:];&|(])rm[[:space:]]+-[a-zA-Z]*[rf]'; then
  echo "Blocked: recursive delete. Move it to _archive instead." >&2; exit 2; fi
if printf '%s' "$CMD" | grep -qiE '\b(DROP|TRUNCATE)[[:space:]]+(TABLE|DATABASE)\b'; then
  echo "Blocked: destructive database command." >&2; exit 2; fi
if printf '%s' "$CMD" | grep -qE 'git[[:space:]]+push[^|;&]*--force|git[[:space:]]+reset[[:space:]]+--hard'; then
  echo "Blocked: destructive git command." >&2; exit 2; fi
exit 0
{
  "matcher": "Bash",
  "hooks": [{
    "type": "command",
    "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/no-delete.sh"
  }]
}
How to know it worked

Ask Claude to run rm -rf test-folder. Blocked. Then ask it to run ls. Allowed. Both matter — a hook that blocks everything is as useless as one that blocks nothing.

If rm -rf goes through, the Bash matcher block probably isn't in settings.json, or the file has a JSON error and Claude is ignoring the lot — one missing comma does it silently. If ls is also blocked, your hook is failing closed because it can't read its input; that's the right behaviour for a broken hook, and Gates chapter 04 shows you how to confirm it.

All courses · Start here · Privacy