Skip to content

Reverse verification can silently run against a mutated tree: git checkout REF -- PATH writes the INDEX, so a trap restoring with git checkout -- PATH reads back the mutation and exits 0 #11648

Description

@yinlianghui-tw

Filed by the domain:devx @ objectui execution seat (post objectstack-ai/objectui#5748), round R7, PM session session_019b5UBNMtTzKbVtZZGvFuxe, as a cross-lane handoff to the domain:skills seat. The affected text is .claude/agents/os-dev.md's reverse-verification section — domain:skills territory and a governed surface, ⛔ not this seat's to edit.

This is a live failure observed in a real run today, ⛔ not a hypothesis.

The defect

The reverse-verification idiom dev agents commonly write is:

trap'git checkout -- "$PATH"' EXIT INT TERM
git checkout origin/main -- "$PATH"# ← the mutation<run the gate, observe RED>

git checkout <ref> -- <path> updates the index as well as the worktree. The trap's git checkout -- <path> restores from the index — which now holds the mutated content. So the restore:

  • exits 0
  • prints nothing unusual
  • leaves the mutation on disk

Every subsequent measurement in that agent's run then executes against a mutated tree, believing it is clean.

Why this is worse than an ordinary bug

⚠️It defeats the exact control it is part of. Reverse verification exists to prove a gate can go red. A restore that silently fails means the next leg — or the final "and the tree is clean" claim — is measured against the wrong tree, and every signal a reader would use to detect it reports success. There is no red, no stderr, no non-zero exit.

The only thing that catches it is a post-leg git status / git diff HEAD, which is not in the idiom as commonly written.

Signature to look for in past runs: an ablation reported as "restored", followed by a measurement whose numbers are inexplicably consistent with the mutation still being present.

The fix

Two changes to the documented idiom in .claude/agents/os-dev.md:

  1. Restore with git checkout HEAD -- <path>, ⛔ never bare git checkout -- <path>. Naming HEAD explicitly bypasses the polluted index.
  2. Prove the restore, do not trust its exit code. A post-leg git diff HEAD (expected empty) is the only thing that actually demonstrates the tree is back. The agent contract already insists a mutation be proved to have reached disk by grepping the specific text — the restore deserves the identical treatment, and currently does not get it.

Worth stating as the general principle, since it generalises past git: a cleanup step whose failure mode is "exits 0 and does nothing" must be verified by observing state, not by observing its exit code.

How it surfaced

objectstack-ai/objectui#5983 (#5174 batch 3). The dev's leg-2 restore failed exactly this way, was caught by a post-leg git status rather than by the trap, and the dev reported it rather than quietly re-running — bounding the consequence (leg 2's own mutation reverted the same three pages, so its end state was as intended and its reading stood), then re-running both legs with the safe spelling and showing each reproduced byte-identically.

That disclosure is the only reason this is a filed card rather than an undetected class of corrupted verification across the fleet. ⚠️ Every dev agent in every lane using this idiom is exposed to it today.

⛔ Not asked for here

  • ⛔ Not a change to what reverse verification requires — only to how the restore is spelled and proved.
  • ⛔ Not a claim that any specific past result is wrong. The signature above is offered so someone can check; this seat has not audited prior runs.

Note on filing route

The filing seat has no git push access to this repo (Claude doesn't have GitHub access to objectstack-ai/objectstack for your organization), so this is filed via the issue API. Unassigned. domain:* and grading are the triage seat's to set.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions