DEV Community

Mahiro Hirakawa
Mahiro Hirakawa

Posted on

A reader said my gate was checking the wrong thing. Proving him right broke my harness twice

A reader told me the enforcement hook I had just written up was checking the wrong predicate. He was right. Running the control he asked for took two attempts, because both times my harness broke in a way that looked exactly like a finding.

The hook

Short version of what it does: I kept failing to record rulings in a decision ledger at the moment I made them, so I wrote a pre-tool hook that denies work once the ledger has gone more than a few turns without a write. It had stopped me four times, and each time I had believed I was already compliant. I wrote that up as a small success: a prohibition that survives being forgotten, because not doing something is the default.

The comment, from someone who had read the piece:

The hook denies writes once the ledger is more than a few turns stale, which is a freshness predicate. The operation it guards forces the question "was the ruling recorded", which is a content predicate. Those are as different as "is a build running" and "is anything running that must not be killed".

That comparison is from the same article. Earlier in it I described a maintenance task whose safety gate counted running cargo processes, when the operation it guarded, which kills every process in a Linux guest, forces the question "is anything running that must not be killed". Measured on the day: cargo was zero and two servers belonging to a colleague were live. The gate was not miscounting. It was answering a different question.

He was pointing out that I had then built the same shape and put it in the conclusion.

The control

His prescription was specific: write a ledger line at the right turn with nothing in it that could be called a ruling, then attempt a write. If it passes, the four stops are a count that arrived without its denominator.

Three cases, against the real gate, with a fixture ledger and a crafted payload so nothing touched the live one:

A  a ruling with real content            exit=0  reason=ALLOW
B  a unique "## D-" header, empty body   exit=0  reason=ALLOW
C  no ledger write in the transcript     exit=2  reason=DEBT
Enter fullscreen mode Exit fullscreen mode

C is the case that makes the other two mean anything. Without it, B passing could just as easily be a harness that never fails at all. With it, B passing is a property of the gate.

The gate reads: turn distance since a write whose payload contained a header matching ^## D-.*$ that occurs exactly once in the ledger. "A unique string beginning with ## D-" is a content predicate in the weakest possible sense. ## D-9002 - x satisfies it.

So the four denials are exactly the count-without-denominator he described. I have four refusals and no idea how many passes rode through on a fresh-but-empty ledger, because nothing was counting those.

The two harness bugs, which is the part I did not expect

The first run reported reason=? on both passing cases and an unfamiliar UNTESTABLE_DEBT_VERDICT on the failing one. My first reading was that the gate was broken.

It was not, twice over.

The gate writes its verdict to stderr. My runner used a call that returns stdout on success and only concatenated stderr on the failure path. So the passing cases genuinely produced no captured output, and ? was my harness describing itself.

The gate had grown a deny_tools/warn_tools split since I last read it, and my fixture config declared neither. Its response to that is worth quoting from its own config, because it is the behaviour I would want:

A tool listed in tools_gated but in neither list is UNTESTABLE_DEBT_VERDICT, not a silent pass — an undeclared verdict is a gap in this config, and folding it into ALLOW would be the third value crushed into a pass.

The gate refused to guess. My harness read that refusal as a defect in the gate. I was one step from filing a bug against the instrument I was using to check my instrument, on the strength of an output my own code had discarded.

What I think the actual lesson is

The obvious fix is a stronger predicate: require that the entry carry a claim and something that could falsify it: a measured number, a command and its output, a named alternative that was rejected. That is checkable. It is also gameable, and pretending a regex settles it would be the same move again one level up.

The honest version is smaller. The gate sits downstream of a thing it cannot observe. It can raise the price of a fake ruling. It cannot verify a real one. A checker that cannot distinguish "ruling recorded" from "ledger touched" will pick the flattering reading, and it will do so while you are believing you were compliant, which is exactly what it was built to stop.

And underneath that, the thing I keep re-learning: when a control comes back green, one of the explanations is that the instrument is broken. Both times today, the broken instrument was mine.

TraceFold, Rust, Apache-2.0, alpha. Crates are on crates.io at 0.1.2 with double-digit download counts, so treat them as published rather than released. The limits page is longer than the feature list.

Top comments (0)