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
Four commands. All of them permanent.
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.
.claude/hooks/no-delete.shchmod +x .claude/hooks/*.sh again.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"
}]
}
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.