Claude Code hooks

A real run, not a description

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.