▶ The hook that said no

Module 03 walkthrough · ~3 minutes · three parts: anatomy, the trap, and two more locks

Repo structure & events

client-project/
.claude/
settings.json (the registration)
scripts/
check-command.sh
scan-secrets.sh
logs/
audit.log
Event timeline
Claude proposes: rm -rf deliverables/old
PreToolUse hook fires → check-command.sh
BLOCKED (exit 2), message sent to Claude
Claude proposes: move to archive/ instead ✓
Claude proposes: write settings.py (has key)
BLOCKED by secret scanner
PostToolUse → audit.log, one line per call
.claude/settings.json (the registration)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "./scripts/check-command.sh" }
        ]
      }
    ]
  }
}
scripts/check-command.sh (the script)
#!/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
session · what Claude sees and says
[hook] Blocked: destructive command.
       Archive instead of deleting.

Claude: That delete was blocked by a project
guardrail. Instead, I'll move the old files
to archive/2026-pre-june/ so nothing is
lost. Want me to go ahead?
session · the secret scanner catches a write
[hook] Blocked: this write contains something
       that looks like a credential.

Claude: The config had the API key pasted in
directly. I'll reference it from an
environment variable instead, so the actual
key never lands in the repo.
logs/audit.log
2026-08-21 14:02:11  Bash   git status
2026-08-21 14:02:40  Bash   BLOCKED rm -rf deliverables/old
2026-08-21 14:03:05  Bash   mv deliverables/old archive/
2026-08-21 14:09:18  Write  BLOCKED settings.py (credential)
2026-08-21 14:09:52  Write  settings.py (env reference)
2026-08-21 14:11:30  Edit   deliverables/2026-08-21-brief.md
.claude/settings.json (committed, all three hooks)
{
  "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" }] }
    ]
  }
}
Module 03 · Walkthrough
The hook that said no
A rule that enforces itself, watched live. Three parts:
1 · Anatomy of a hook 2 · The trap springs 3 · Two more locks
Preferences go in prose. Invariants go in hooks.
The closing demo builds all three of these hooks prompt by prompt, so you can run the same sequence in your own repo.

Continue → Section 2: When hooks fire

Part 1 · Anatomy Part 2 · The trap springs Part 3 · Two more locks