Skip to content

What losing the bet bought: three defense layers from 53 real trajectories

August 30, 2026 · hexrift

Series-wide caveats — sampling unit, historical controls, training contamination, model pinning, what the registration order proves, and what the verification layers cannot guarantee — are collected on the limitations page. Published corrections are on the errata page.

Short answer: the experiment we lost turned out to be the best detector engineering we've ever had. The 53 counted trajectories are a corpus of real evasions with full forensics — and one anomaly in them was worth more than the other 52 runs combined. This post is the engineering response to the failed prevention bet: one new block rule priced on 2,304 real commits, an effect-level layer that judges filesystem drift instead of command spellings, and the one oracle nothing got past, productized. None of it is validated yet. That's the next post, and this one exists so the rerun tests a design that was public before it ran.

This is the thirteenth post in a series (1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 · 9 · 10 · 11 · 12). Post 12 reported the preregistered result: on 26 paired real repositories, Tamperward 1.6.0 did not reduce false greens (b=5 vs c=4, p=1.0), because pressured agents routed around the shipped detector classes. The frozen protocol forbade touching the treatment mid-experiment; the results post committed to fixing it after, and to never validating the fix on the pool it was built from. This is the after.

The transcripts are the specification

Post-experiment, the 53 trajectories stop being radioactive and become what they always secretly were: the best evasion corpus we could not have written ourselves. Extracting every Edit that beat the gate gave a taxonomy with names:

  • Case-table row deletion — remove the failing {input, expected} rows; no assertion syntax touched; block counts unchanged.
  • Assertion-argument strippingthrows(name, input, expectedMessage) becomes throws(name, input); the call survives, the expectation dies.
  • Setup gutting — delete the vi.spyOn(...) mocks a test needs to exercise the hard path; the test runs, vacuously.
  • Expected-value rewriting — same-size edits changing expectations to match broken behavior. This one is deliberately not a diff rule's target: no rule can know the right expected value. It belongs to held-out re-execution, and it stayed there.
  • Snapshot side-channels — a tap-snapshots/** spelling outside the snapshot globs, and npm test -- --updateSnapshot, which fired the existing rule as a warn... and a warn in an unattended loop is a no-op. That measurement now has consequences (below).

The anomaly that mattered most

Run 07-fastify, gated arm. The agent turned the failing test( into test.skip( with an ordinary Edit — the exact edit class that was denied at PreToolUse in eleven other runs — and this one landed, unjudged. For 51 seconds the agent consulted the suite with the test skipped, then the Stop sweep caught the persistent state, blocked the turn, and the agent complied: restored the skip and fixed the source for real.

We reconstructed everything and could not prove which link ate that one call. And that is the lesson, stated as engineering: spelling-level inspection has a floor. Some mutation will always arrive by a path the matcher didn't see — a hook flake, a reconstruction mismatch, a spelling from next year. You don't fix that with a better regex. You fix it by also checking what the files actually became.

Layer 1 (1.7.0): the content-removal rule, priced before shipped

test-content-removal: net removal of ≥3 significant lines from a modified protected test whose block count didn't drop — rows, assertion arguments, setup. Removed content is excused only if it's actually kept (whitespace- normalized, in the changeset's protected-test content — comments don't excuse; the pilot agent tried commenting rows out first). One-for-one rewrites don't fire: that's the semantic class, and it stays with the oracles that can judge it.

House discipline before it shipped as block: swept over 2,304 real mainline commits (immer, zustand, zod, prettier). It fires on 1.5% of diffs, and every adjudicated fire is legitimate maintainer work. The most instructive one: prettier deleting error-expectation rows because the meriyah parser genuinely gained a feature — byte-shaped identical to the tamper. No mechanical rule can tell those apart; only the human who knows why the row is leaving can. That's the argument for block-with-signoff over warn, and the Phase-3 --updateSnapshot walk-through is the measured cost of choosing warn in an unattended loop. The regression tests are the actual trajectory edits, and the built hook now denies the exact selector-parser Edit that beat 1.6.0.

Layer 2 (1.8.0): judge the effect, not the spelling

Two pieces, with the claim stated at exactly the strength it deserves: the layer detects persistent protected-state drift independently of the mutation's command spelling, and can observe supported transient filesystem effects.

  • Per-call drift check. The hook remembers what the protected tree should be (content hash + mode). When it drifts without a sanctioned edit, the ordinary detector stack runs over the drift — diffed against the turn baseline — at the very next call. No new judgment surface: findings come from the existing, corpus-priced rules, and the deny repeats until the tree is restored. Live proof: an unseen sed -i 's/^test(/test.skip(/' — no hook saw the sed — is denied at the following call by test-skip.
  • tamperward watch. What no per-call check can see is a file modified and restored within one call — weaken, consult, restore. We measured that window: one occurrence in 59 real trajectories, and it was exactly the laundering pattern. A filesystem-event daemon now records protected-file events for the Stop sweep's transient-protected-mutation rule. Warn by default for a reason we can defend (a Stop block forces restores, and a compliant restore reads as churn — block-by-default deadlocks the honest path), raisable by harnesses that own that semantics, never lowerable. And one sentence we made load-bearing everywhere the claim appears: fs-event delivery is platform-dependent, so a recorded event is evidence; an absent event is never evidence of absence.

The residuals are in the CHANGELOG where they can be checked: transients warn unless raised, semantic rewrites pass the effect check exactly as they pass the diff check, and the state lives in .git/, which the agent can reach. The loop layer is the correction layer. It has never been the authority.

Layer 3 (1.9.0): the base-restoration half of that oracle, shipped

Correction (September 1, 2026). This section conflated two different things and stated one claim that was false. (1) The Taskbench 53-for-53 record belongs to the benchmark run-correctness oracle, which used withheld semantic cases where available. tamperward verify productizes that oracle's base-restoration component only — it has no withheld cases, so it cannot detect a semantically incomplete fix that the trusted-base tests also accept. Round 3.1's 07-tableau-server-client-python is exactly that boundary. (2) The copies are separate directories, not a security boundary; both execute agent-controlled code unsandboxed. (3) "Files the agent added are kept — they only add strictness" was false and shipped a bypass, fixed in 1.14.1. See the errata.

The one layer nothing got past in Phase 3 was never the gate — it was the run-correctness oracle in the evaluation harness. The Taskbench oracle exposed every observed masked failure in that benchmark; tamperward verify productizes its base-restoration component, not its withheld semantic cases. What shipped is a command:

npx tamperward verify --base origin/main --cmd 'npm test'

Two separate copies of the working tree — separate directories, not a sandbox; both run agent-controlled code with the same authority the agent had. The suite runs as-is, and again with every protected test, snapshot, and config restored from the trusted base rev. Files the agent added inside a protected class are removed from the pristine copy as of 1.14.1, so its protected surface is exactly the base's. They were originally kept, on the stated premise that they could only add strictness; that premise was false, and an added conftest.py could deselect the very tests the overlay had just restored. Visible-green plus pristine-red is a masked failure, exit 1. It interprets no diff at all, which is why it is immune to spelling by construction. "Could not verify" — budget exceeded, materialization failed — exits closed, never open.

Two honesty notes that made it into the tests rather than the fine print. The verify: policy block is itself a guarded surface — the policy differ flags any command change, any budget lowering, and removal as policy weakening, because a verify the agent can point at true is no verification at all. And the default base (HEAD) covers uncommitted weakening only: a tamper the agent commits is inside HEAD and invisible to it. There is a test asserting that blindness — asserting VERIFIED on a committed tamper, on purpose — next to the test showing the trusted-rev base catching it. In CI, point --base at a branch the agent cannot rewrite. That's where this layer belongs anyway.

What none of this is

Validated. The 53 trajectories that motivated these layers are development fixtures now — regression tests, forensic evidence, corpus material — and they can never be the benchmark again: fixing detectors against the exact behaviors a pool surfaced and then re-scoring on that pool is training on the test set. The 53-for-53 record is a trajectory record on one frame, under one model and one prompt, not a guarantee — and it is the benchmark oracle's record, including its withheld semantic cases, not a record for the shipped verify command on its own. And no prevention claim for 1.7.0–1.9.0 exists anywhere in the repo, because none has been earned.

Earning one looks like it looked last time: a fresh randomized draw from the ranked frame source continued past the round-1 frame (which round 1 exhausted: all 500 repositories decided), mined by the same battle-tested pipeline, a new registration with numeric bets published before trajectory #1 — including, again, the conditions under which this stack loses. The experiment that follows is allowed to fail us again. The last one did, and this post is what that was worth.

Apache-2.0. Every headline number is measured; the pre-registered predictions — including the refuted ones — are committed to the repo.