Skip to content

[finding] The ablation trap restore EXIT silently did NOT restore — only the byte-identical hash check caught it, and an EMPTY hash must be read as failure #11539

Description

@os-sam

Measured during #11451 (PR #11537), 2026-08-24. Filed by the domain:services execution seat. This is a defect in the ablation protocol itself, which every dev in this lane is briefed to follow, so it costs more than one card if it stays unrecorded.

What happened, in the dev's own words

RESTORE LEG: the trap's git checkout ran from the wrong cwd and DID NOT restore — caught because its own git hash-object echoed EMPTY. Restored by hand from the repo root and verified byte-identical (382ee3d6458e864cc81e8745ccdad57734bc88a5 == the HEAD blob), git status clean. Had I trusted the trap, every later measurement would have been taken on a mutated tree.

The ablation itself was exemplary — mutation confirmed on disk by anchored grep in both directions, direction predicted in writing before running, 85 passed unmutated → 9 failed ablated, exactly the predicted set. The restore is the leg that failed.

Why this is a protocol defect, not one dev's slip

The brief every os-dev in this lane receives says:

restore byte-identically (verify with git hash-object), and use trap restore EXIT INT TERM

Two instructions, and the ordering of trust between them is not stated. The trap reads as the mechanism and the hash check reads as belt-and-braces. It is the other way round. A trap firing proves only that the shell ran a function; it proves nothing about that function's effect. Here the trap fired, git checkout resolved a relative path against a cwd that was no longer the repo root, and the tree stayed mutated.

⚠️ The consequence is not a failed ablation — it is every subsequent measurement in the session silently taken on a mutated tree, reported as clean. That is the same class as the ablation hazards already recorded (green-under-ablation is NOT MEASURED; a zero-hit needs a positive control): an instrument that reports success while measuring the wrong thing.

The specific trap: an EMPTY hash is not a mismatch, it is a missing file

git hash-object <path> on a path that does not resolve produces empty output, not a differing hash. So a verification written as "did the restore command run" passes, and even a naive comparison can be written in a way that treats empty as "nothing to compare". The check must be:

  • compare the post-restore git hash-object against the HEAD blob hash for that path, and
  • treat an empty or absent hash as FAILURE, never as a pass, and
  • confirm git status is clean afterwards.

The dev caught it precisely because its restore function echoed the hash and a human-readable EMPTY was visible. A silent comparison would not have surfaced it.

Proposed hardening (not a ruling — this touches a governed surface)

  1. Absolute paths in the trap. Capture REPO_ROOT="$(git rev-parse --show-toplevel)" before the mutation and restore against "$REPO_ROOT/<path>", so the restore cannot depend on cwd at trap time.
  2. State the trust ordering explicitly in the brief: the trap is a convenience for the crash path; the hash comparison is the proof. A trap that fired is not evidence of restoration.
  3. Name the empty-hash case in the protocol text, since it is the failure mode that looks like success.
  4. Consider having the restore leg exit non-zero and say so loudly when the hash does not match, so a session cannot continue measuring on a mutated tree.

⛔ The natural home for (2) and (3) is .claude/agents/os-dev.md and the pm-dispatch skill text — governed surfaces, maintainer-merge-only. Filing rather than fixing, and this seat will not open a PR against them.

Related, from the same report — worth folding into the same pass

Two more instrument-vs-premise traps the same dev hit and self-corrected:

  • A package typecheck can say nothing about the test files you edited.@objectstack/plugin-security's tsconfig excludes **/*.test.ts — verified with --listFiles (0 hits for the two edited test files). So "typecheck clean" would have been a true statement that did not cover the new code. It reproduced the TEST_DEBT ledger entry directly instead (11 raw errors, matching, none naming a touched file).
  • MODULE_NOT_FOUND is not a red gate. Three gate invocations exited 1 on the dev's own wrong filenames; re-run via the real pnpm check:* commands. An exit 1 that never reached the gate body belongs with exit 99 / queue-timeout / PREREQUISITE NOT MET as NOT MEASURED, not as a failure.

Refs: #11451 / PR #11537 (where this was measured) · #11363 (verify-lock contention, the other standing measurement hazard) · #11463 (the never-park clause, same family: a clause that exists but loses the tiebreak at the moment of decision)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions