Context
check routes the agent to run agents-shipgate verify; the Stop hook then runs an identical verify again at turn end. Measured on a small fixture that is ~1.2s of pure duplicate work per turn (0.9s vs 2.1s), and it grows with repo size.
A first attempt shipped in #294 and was reverted in the same PR after review found two defects. Recording both so the next attempt does not repeat them:
- It trusted a workspace-writable artifact. The hook read
agents-shipgate-reports/verifier.json, which the agent can write. A forged (or interrupted) artifact with control.state=complete skipped verification entirely, and an agent_action_required artifact could inject an arbitrary "exact" command into the hook's block reason.
- The identity binding was insufficient. Comparing the recorded
verification-input.diff to the hook's own snapshot diff is not a binding to the current verification request: committing an uncommitted change reproduces the same diff text, which was used to reproduce an outcome-changing stale pass — an old passed result reused while a fresh verify returned review_required for permissions.scopes: ["*"].
Also noted: on a feature branch with worktree edits, the hook hashes only the worktree diff while the verifier evaluates base...HEAD plus that diff, so the optimization would silently miss anyway.
What a correct design needs
- Binding to the verifier's own content-addressed request identity (
request_id / input_set_id / the terminal receipt), not to a re-derived diff. A hook script cannot compute those, so the producer has to hand the consumer something checkable.
- A trust story for the handoff. Any path the agent can write is not evidence; the plausible shapes are the verify CLI recording a consumable marker outside the workspace, or the hook invoking a cheap "is this result current?" CLI subcommand that re-derives identity in the engine.
- Correct behavior on a feature branch, where the hook's snapshot and the verifier's evaluated range differ.
Non-negotiable
No design may let a reused result be more trusted than a fresh one. The Stop hook is advisory, but "advisory" must not mean "skippable by writing a file".
Context
checkroutes the agent to runagents-shipgate verify; the Stop hook then runs an identical verify again at turn end. Measured on a small fixture that is ~1.2s of pure duplicate work per turn (0.9s vs 2.1s), and it grows with repo size.A first attempt shipped in #294 and was reverted in the same PR after review found two defects. Recording both so the next attempt does not repeat them:
agents-shipgate-reports/verifier.json, which the agent can write. A forged (or interrupted) artifact withcontrol.state=completeskipped verification entirely, and anagent_action_requiredartifact could inject an arbitrary "exact" command into the hook's block reason.verification-input.diffto the hook's own snapshot diff is not a binding to the current verification request: committing an uncommitted change reproduces the same diff text, which was used to reproduce an outcome-changing stale pass — an oldpassedresult reused while a fresh verify returnedreview_requiredforpermissions.scopes: ["*"].Also noted: on a feature branch with worktree edits, the hook hashes only the worktree diff while the verifier evaluates
base...HEADplus that diff, so the optimization would silently miss anyway.What a correct design needs
request_id/input_set_id/ the terminal receipt), not to a re-derived diff. A hook script cannot compute those, so the producer has to hand the consumer something checkable.Non-negotiable
No design may let a reused result be more trusted than a fresh one. The Stop hook is advisory, but "advisory" must not mean "skippable by writing a file".