
The Guard That Announced Its Own Absence
A bash guard walked every file in the repo on each call, overflowed its own match cap, and printed that its coverage was OFF dozens of times in one session.
View companion repoSixty-one disclosures, zero readers
I was auditing what loads into a Claude Code session when I noticed a hook message repeating. Not once. Sixty-one times in a single session.
The message said the guard was not working:
proofpunk: the pre-command baseline for this Bash call was incomplete (protected-path scan hit its cap), so Bash-authored writes to evidence or test paths cannot be detected for this call. Treat guard coverage as OFF here.
That is an honest message. It states the failure, the mechanism, and the consequence. The guard knew its baseline was incomplete, knew what that meant for detection, and said so in plain language every single time it happened.
Nobody read it. I had been working in that repo across sessions with this printing after Bash calls, and it registered as noise. The disclosure was perfect and its delivery made it invisible.
I wrote the finding as: "The proofpunk bash guard is silently off in this repo — and announces it 61×/session. It walks all 313,041 files on every Bash call (2.4s, twice per call), matches 20,245 against a LIMIT = 4000, overflows, and emits 'Treat guard coverage as OFF here.'"
"Silently off" and "announces it 61×/session" look contradictory in one sentence. They are the same fact. A warning that fires on every operation carries no information, because information is what distinguishes one moment from another. At sixty-one repetitions it had become chrome.
The mechanism
The guard is hooks/bash-write-snapshot.sh, and its job is to notice when a Bash call writes to evidence or test paths. To do that it takes a pre-command baseline: walk the repo, hash the files matching its protected-path patterns, then compare after the command runs. Any file whose hash changed was written by that command.
Reasonable design. It depends on the baseline being complete.
Once I traced it: "LIMIT = 4000 — it walks the entire repo on every Bash call, hashing every file matching evidence/test path patterns."
The cap exists so a pathological repo cannot hang every Bash call. When the match set exceeds LIMIT, the guard cannot build a complete baseline, so it refuses to claim coverage it does not have — and declares itself OFF rather than reporting a comparison against a partial snapshot.
That is the correct failure mode. A guard that silently compared against a truncated baseline would produce false negatives forever and never tell anyone. This one fails loudly and gives up its authority.
In this repo the numbers were 313,041 files walked, 20,245 matched, against a limit of 4,000. Five times over. Not marginally over, not occasionally over: over on every call, deterministically. My first write-up of it recorded the cost precisely — "313,041 files walked, 2.42 s per walk, twice per Bash call."
Two point four two seconds, twice, on every single Bash invocation, to build a baseline that was then discarded as unusable. That is the part that stings. The work was not skipped when the cap was hit; it was performed in full and thrown away. Sixty-one times a session, the guard paid roughly five seconds of wall clock to produce a result it then declared void.
Two bugs supplying the overflow
The cap was not the defect. The match count was. Two path-filter bugs put twenty thousand files in front of a four-thousand-file limit: "SKIP covers .venv but not .venv-mlx (2,604 vendored test_* files), and plans/ + .debug/ false-match its /test_ patterns."
Both are one-character-class problems.
The skip list knew about .venv. The repo also has .venv-mlx. A prefix match would have caught it; an exact match did not, so 2,604 vendored test_* files from a third-party dependency tree entered the protected set. Nothing in .venv-mlx is this project's test surface. It is a library's own tests, vendored, and the guard was hashing them on every Bash call.
The second is the mirror image. The pattern /test_ was meant to catch this project's test files. plans/ and .debug/ contain paths that satisfy that substring without being tests. Too narrow on one side, too broad on the other.
Neither bug is interesting alone. Together they multiplied the match set past the cap, and the cap converted a filter inaccuracy into a total loss of coverage. The guard was not broken. Its inputs were wrong, and its correct response to wrong inputs was to switch itself off.
There is a second-order lesson in how the fix landed later. After the filter bugs were addressed, a residual remained, and I recorded it as: "2.51s/Bash call is driven by 10,073 protected files, mostly historical plans/*/evidence/ from past campaigns (14.8GB). Archiving old run evidence would cut it further — that's your research data, so I left it alone."
Ten thousand of those files are legitimately in scope. They are evidence directories from previous campaigns, exactly the kind of path the guard is meant to protect. The remaining cost is not a bug at all; it is the guard doing its job over a corpus that grew for years. Fixing the two filter defects moved the match count under the cap and restored coverage. It did not make the walk cheap, and no filter change can, because those files genuinely belong to the protected set.
Naming it without fixing it
I did not fix it. The finding is filed under a heading I wrote as "Three findings I did not act on," and in a later pass of the same audit, "Four findings I did not act on."
The reason was scope. The guard lives in a versioned plugin cache, outside the repo I was authorized to change, and my task that session was auditing what loads into context — I had just archived 684 skill directories and cut roughly 116,000 tokens per session. Editing a plugin's internals mid-audit would have meant changing the instrument while measuring with it.
So it got written down with its numbers, its two root causes, and an explicit note that I left it alone. That is a real deliverable. The next person to touch that guard does not have to rediscover why it overflows; they have the file count, the match count, the limit, and both filter bugs.
Reporting a defect you are not fixing is a legitimate output. Silently fixing something outside your scope is how an audit becomes an unreviewed change.
What the repetition taught me
The lesson is not that the guard should be quieter. It is that per-event disclosure is the wrong channel for a persistent structural condition.
A message that fires once, on the first overflow, and then stays silent — while recording the state somewhere a human checks — is strictly more useful than the same message sixty-one times. The first form is an event. The second is weather. Humans, including this one, filter weather.
There is a broader version. The same audit found 222 directories that were .name.bak-TIMESTAMP auto-backups still being indexed as live skills, and a ~/CLAUDE.md where 136,682 of 137,517 bytes were an auto-generated block duplicating what the tool already injected natively. The actual hand-written rules were the remaining 834 bytes.
None of that was hidden. Every one of those files was sitting in a directory, listable, with an obvious name. They accumulated in places where the volume of routine output had trained everyone to stop looking. The guard's sixty-one disclosures per session are the same failure with better manners: the system told the truth, on schedule, in a place where truth had stopped being read.
Continue the series
- 72SeriesThe Env Var That Was Not the SettingREQUIRE_API_KEY=false was set and the endpoint still returned an unauthorized status. A database setting participated in the decision, which is not where I was looking.
- 74SeriesTwo Hundred OK With Five MissingA negative limit returned a success status and ok:true while the advertised count and the payload disagreed. The envelope reported success over a silent drop.
- 71SeriesThe Prompt That Never Arrived: Two Models, Same invalid_requestTwo subagents died before their first tool call. Shrinking the prompt did not help, because the prompt was not what overflowed.
- 75SeriesThe Step That Never RanThe job died installing dependencies, so the CLI check never executed. Two verifiers reported totals 5.1 MB apart, and they were reading two different job logs.