Loading page content
Pick the hooks you want. Copy the settings.json block, copy the shell scripts, drop into .claude/hooks/, and mark executable. Done.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-rm-rf.sh"
},
{
"type": "command",
"command": ".claude/hooks/block-force-push.sh"
},
{
"type": "command",
"command": ".claude/hooks/block-terraform-destroy.sh"
}
]
},
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/budget-guard.sh 200000"
}
]
}
]
}
}# === .claude/hooks/block-rm-rf.sh ===
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE 'rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r|-rf|-fr)([[:space:]]|$)'; then
if ! echo "$CMD" | grep -qE '/tmp/'; then
echo "BLOCKED: refusing 'rm -rf' outside /tmp." >&2
exit 2
fi
fi
exit 0
# === .claude/hooks/block-force-push.sh ===
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE 'git[[:space:]]+push[[:space:]].*(--force|-f([[:space:]]|$))'; then
if echo "$CMD" | grep -qE '(main|master|prod|release/)'; then
echo "BLOCKED: force-push to a protected branch." >&2
exit 2
fi
fi
exit 0
# === .claude/hooks/block-terraform-destroy.sh ===
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE '(terraform[[:space:]]+destroy|kubectl[[:space:]]+delete[[:space:]]+namespace|aws[[:space:]]+s3[[:space:]]+rb)'; then
echo "BLOCKED: infra teardown is human-approval only." >&2
exit 2
fi
exit 0
# === .claude/hooks/budget-guard.sh 200000 ===
#!/usr/bin/env bash
# .claude/hooks/budget-guard.sh
CAP=${1:-200000}
STATE_FILE=".claude/.budget-tokens"
[ -f "$STATE_FILE" ] || echo 0 > "$STATE_FILE"
USED=$(cat "$STATE_FILE")
INPUT=$(cat)
EST=$(echo "$INPUT" | jq -r '.tool_input.command // empty' | wc -c | awk '{ print $1/4 }')
NEW=$(awk "BEGIN { print $USED + $EST }")
echo "$NEW" > "$STATE_FILE"
if awk "BEGIN { exit !($NEW > $CAP) }"; then
echo "BLOCKED: per-task token budget ($CAP) exceeded. Reset with: rm $STATE_FILE" >&2
exit 2
fi
exit 0
chmod +x .claude/hooks/*.sh. Start a new Claude Code session in the repo. Test with a deliberately destructive prompt and confirm the hook fires.