
Photo: Zayed Hossain / Pexels
Your Claude Code Safety Rules Are Suggestions Until You Use Hooks
Chris Harper
2 min read
Aug 9, 2026 · 04:07 UTC
CLAUDE.md instructions drift when context grows long — hooks are deterministic shell commands the harness runs on every matching event, unconditionally. That's the difference between a suggestion and a rule.
Your CLAUDE.md can say "never use rm -rf" or "always run lint after editing." The model follows those when it's paying attention. A hook fires whether it's paying attention or not. The two surfaces are complementary: CLAUDE.md sets intent, hooks enforce the constraint.
The two hook types you'll use most
PreToolUse fires before the tool runs. Exit code 2 = block (Claude sees the stderr output and retries without that action). Any other non-zero = warn but proceed. Use for guards.
PostToolUse fires after the tool completes — it can't undo what happened, but it runs unconditionally. Use for formatting, test runs, audit logging.
Configure in .claude/settings.json committed to the project root so every teammate gets it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/block-danger.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/auto-format.sh" }]
}
]
}
}
Three patterns to wire up now
1. Block dangerous Bash (PreToolUse, matcher Bash)
#!/usr/bin/env bash
cmd=$(cat | jq -r '.command')
if echo "$cmd" | grep -qE 'rm -rf|git push --force|DROP TABLE'; then
echo "BLOCKED: $cmd" >&2; exit 2
fi
Exit 2 causes Claude to explain what it tried and propose a safer alternative.
2. Auto-format on every file write (PostToolUse, matcher Edit|Write)
#!/usr/bin/env bash
file=$(cat | jq -r '.path // empty')
[[ -z "$file" ]] && exit 0
[[ "$file" == *.py ]] && { black --quiet "$file"; exit 0; }
[[ "$file" == *.ts || "$file" == *.tsx ]] && npx prettier --write "$file"
3. Run affected tests after edits (PostToolUse, matcher Edit|Write)
#!/usr/bin/env bash
file=$(cat | jq -r '.path // empty')
[[ -z "$file" ]] && exit 0
pnpm test --passWithNoTests --testPathPattern="$(basename "$file")" 2>&1 | tail -5
The model sees hook stdout as context — when block-danger fires, Claude explains what it tried and proposes a safer alternative automatically.
Sources: Claude Code Hooks — Anthropic Docs · Claude Code Settings Reference — code.claude.com · Claude Code Hooks Setup with Examples — kjetilfuras.com