Module 03 demo · ~4 minutes · three parts: build the hooks, trip the wires, break one on purpose
#!/bin/bash
# refuse destructive shell commands
input=$(cat)
if echo "$input" | grep -qE "rm -rf|--force|push.*main"
then
echo "Blocked: destructive command." >&2
echo "Archive instead of deleting." >&2
exit 2 # 2 = block. 0 = proceed.
fi
exit 0
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command",
"command": "./scripts/check-command.sh" }] },
{ "matcher": "Write|Edit",
"hooks": [{ "type": "command",
"command": "./scripts/scan-secrets.sh" }] }
],
"PostToolUse": [
{ "hooks": [{ "type": "command",
"command": "./scripts/audit-log.sh" }] }
]
}
}
[hook] Blocked: destructive command.
Archive instead of deleting.
Claude: That delete was blocked by a project
guardrail. I'll move the folder to
archive/2026/ instead so nothing is lost.
Want me to go ahead?
[hook] Blocked: this write contains something
that looks like a credential.
Claude: The test config had the key pasted in
directly. I'll reference it from an
environment variable instead, so the actual
value never lands in the repo.
2026-08-22 09:14:02 Bash git status
2026-08-22 09:15:11 Bash BLOCKED rm -rf archive-old
2026-08-22 09:15:40 Bash mv archive-old archive/2026
2026-08-22 09:17:03 Write BLOCKED test-config.py (credential)
2026-08-22 09:17:31 Write test-config.py (env reference)
Claude reads STAKEHOLDERS.md and answers
immediately. No hook ran.
The Bash blocker's matcher scopes it to shell
commands, and the secret scanner only watches
writes. A plain read touches neither. Hooks
are working when they stay out of the way.
$ rm -rf archive-old ← ran. no block.
The script still exists in scripts/.
The registration is gone from settings.json.
Nothing points to the script, so nothing
runs it. Protection: zero.