▶ Demo: building your guardrails

Module 03 demo · ~4 minutes · three parts: build the hooks, trip the wires, break one on purpose

Repo structure

client-project/
scripts/
check-command.sh
scan-secrets.sh
audit-log.sh
.claude/
settings.json (the registrations)
logs/
audit.log
Verification tests
rm -rf → BLOCKED, redirected ✓
key in file → BLOCKED, rerouted ✓
audit.log → every call recorded ✓
plain read → untouched (matcher works) ✓
registration removed → no protection!
scripts/check-command.sh (what Claude wrote)
#!/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
.claude/settings.json (all three registrations)
{
  "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" }] }
    ]
  }
}
new session · Part 2 test: the blocker fires
[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?
new session · Part 2 test: the scanner catches a key
[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.
logs/audit.log
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)
new session · Part 2 test: the negative case
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.
new session · Part 3: the unwired doorbell
$ 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.
Module 03 · Demo
Building your guardrails
Every prompt shown here works in your own repo. Three parts:
1 · Build the hooks 2 · Trip the wires 3 · Break one on purpose
Registration is what makes a hook real
Run these prompts in your own repo, in your own words: build the three hooks, trip the first two, read the log, run the negative test, and break one on purpose so you never forget the wiring rule. Then put it back.

On to the Module 03 quiz →

Part 1 · Build the hooks Part 2 · Trip the wires Part 3 · Break it on purpose