Claude Code hooks
This is the unedited output of ./demo/demo.sh against a working
hooks lab. Every verdict below is a hook's real exit code, not a mock-up. The pauses the
script uses for narration have been stripped, nothing else was touched.
The part worth reading is the fourth block: the suite passes 75 assertions, one guard is deleted on purpose, 23 assertions go red, and the guard is restored.
------------------------------------------------------------ GAP 2 - a shell command sails past a Write/Edit hook The matcher watches tool names, so Bash is a third door. Here is that door, with the guard on it. protected/ is the tree it defends. BLOCKED rm protected/important.txt BLOCKED sh -c 'rm /Users/danielmeshulam/hooks-lab/protected/important.txt' BLOCKED cd protected && rm important.txt BLOCKED P=protected; rm $P/important.txt BLOCKED find protected -name '*.txt' -delete BLOCKED cp /etc/hosts protected/important.txt BLOCKED echo pwned > protected/important.txt allowed cat protected/important.txt None of those verbs is on a blocklist. The rule is inverted: touching the protected tree is denied unless every verb in the command is read-only. New tools and new spellings fail closed. ------------------------------------------------------------ GAP 5 - brittle verdict parsing One JSON line on stdout, nothing else. Prose goes to stderr. stdout (the hook parses this and only this): {"verdict": "deny", "reason": "'rm' is not on the read-only allowlist and the command references the protected tree"} stderr (what the model is told): BLOCKED: 'rm' is not on the read-only allowlist and the command references the protected tree stdout lines: 1 valid JSON: yes Reword the message however you like. The parse never changes. ------------------------------------------------------------ GAP 4 - a finish gate that checks the filename, not the content Same filename every time. Only the content differs. FAIL three headings, no body Deliverable gate failed: section(s) present but without real con FAIL headings inside prose Deliverable gate failed: missing section heading(s) at line star FAIL heading after prose Deliverable gate failed: missing section heading(s) at line star FAIL padded with filler Deliverable gate failed: section(s) present but without real con pass the real report Sections are parsed as structure: a heading counts only when it starts a line, and each one has to carry real prose of its own. ------------------------------------------------------------ GAP 3 - Stop hook recursion Same failing deliverable both times. Only stop_hook_active differs. stop_hook_active=false exit=2 blocks, one chance to fix stop_hook_active=true exit=0 allows, the loop ends unreadable payload exit=0 allows, never risks a loop The field is read first and read permissively. Mistaking true for false costs an infinite loop; the other direction costs one check. ------------------------------------------------------------ YOUR DEFINITION OF DONE - every check proved by breaking what it guards 75 assertions. Watch what happens when I neuter one guard. baseline: RESULT: 75 passed, 0 failed guard neutered: RESULT: 52 passed, 23 failed restored: RESULT: 75 passed, 0 failed A suite that stays green when you delete the guard is decoration. ------------------------------------------------------------ MIGRATING PROSE RULES - the policy is data, not code config.json holds what is protected. Swapping it inverts the answer, with no edit to any gate. BLOCKED rm protected/important.txt allowed rm deliverable/report.md (config.json: protected_paths -> ["deliverable"], nothing else touched) allowed rm protected/important.txt BLOCKED rm deliverable/report.md ------------------------------------------------------------ restored RESULT: 75 passed, 0 failed
The lab defends a protected/ tree. Every blocked line is a
different way of spelling the same destructive action, and none of those verbs is on a
blocklist: the rule is inverted, so anything touching the tree is denied unless every verb
in the command is read-only.