feat(hooks): refuse a CAT-UNVERIFIED tag from a turn that ran no check - #468
Conversation
The first commit only logged tags after the fact, on the argument that
blocking the emitting turn would deadlock. That argument was wrong: requiring
an ATTEMPT is not requiring success. cat-mode/SKILL.md:269 asks for the verify
in the same turn, so a tag from a turn with no verification tool is a claim
nobody tried, and that turn is now refused with exit 2.
`stop_hook_active` releases the refusal. Without it the block loops forever,
because the reply being rewritten to satisfy the hook has no tool call either.
Evidence this closes the real case, replaying this session's own turn 419:
A) tag, zero tools -> exit=2, "ran no verification tool. Untried
claim(s): that it widened scope past the one
session I gave it"
B) same tag after Bash -> exit=0
C) stop_hook_active -> exit=0, no loop
D) no tag, no tools -> exit=0
17 tests pass. check_hook_test_coverage.py now demands a positive firing test
for this hook and gets one (test_tag_with_no_attempt_is_blocked); it passes,
36 hooks checked.
Found by reflect on this session: both tags were emitted in the turn directly
after a diu-stop block, and diu-stop's own text at claude_stop_check.py:199-200
offers the tag as an alternative to pasted evidence -- so the tooling taught
the behaviour the prose forbids. Two sibling fixes to diu-stop itself (drop the
tag template from the complaint; do not bill a word-count cut against an
evidence demand in the same message) are follow-ups, not in this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u8S5ct3kFhosinSbybc7W
Change-Id: I356d181c284194e7ee134f6c88ca5436c8e66313
Owner
Author
|
This pull request is part of a Mergify stack:
|
5 tasks
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_249e3121-361d-4dc4-bc1b-5303f0a7786f) |
EdbertChan
deleted the
stack/EdbertChan/reflect/unverified-tag-ledger-20260911/refuse-cat-unverified-tag-turn-ran-check--356d181c
branch
September 12, 2026 06:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A reply that says "I could not check this" now has to show it tried.
If the reply tags a claim and the turn ran no check at all, the turn is refused
and must be written again.
The first slice only logged these claims. The reason given was that refusing
the reply would trap it in a loop.
That reason was wrong. Asking for an attempt is not asking for it to succeed.
Run the check; if it cannot settle the claim, the tag is then honest.
One release valve prevents the loop. A reply already being rewritten because of
this hook is let through, since that rewrite has no check of its own.
Review Claim
A well-formed unverified-claim tag in a turn that used no verification tool
fails that turn, unless the turn is already a rewrite prompted by this hook.
Review Lane
behavior
Review Unit
engine-runtime
Safety Invariant
The refusal needs three things at once: a well-formed tag in the reply, no
verification tool in that same turn, and no
stop_hook_active. Any one of themmissing returns the previous allow, and the hook still touches only its own
ledger. The
stop_hook_activerelease is covered by its own test, so the blockcannot become unescapable without that test failing.
Slice Rationale
The first slice landed the ledger and the parsing; this one changes only the
verdict. Split that way, a reviewer who dislikes the block can drop this commit
and keep the recording. Bundled, rejecting the block would also throw away the
ledger.
Non-goals
diu-stop,hedge-runs-prove-it, orprove-it-ship-gate.Architecture
Before
graph TD A["reply carries a well-formed tag"] --> B["append to session ledger"] B --> C["turn ends, exit 0"] C --> D["reminder on the next prompt"]After
graph TD A["reply carries a well-formed tag"] --> B["append to session ledger"] B --> C{"did this turn run Bash / Read / Grep / Glob?"} C -->|"yes"| D["turn ends, exit 0"] C -->|"no"| E{"stop_hook_active?"} E -->|"yes, a rewrite"| D E -->|"no"| F["refuse the turn, exit 2"] D --> G["reminder on the next prompt"]Test Plan
Test Plan
cd engine/hooks/unverified-tag-ledger && python3 -m unittest discover -s testspython3 engine/skills/make-pr/scripts/preflight.py --base origin/mainpython3 scripts/check_hook_test_coverage.py engine/hooks/unverified-tag-ledgerReplaying the real turn that motivated this, through the hook:
check_hook_test_coverage.pyfailed first, because this hook now blocks and hadno test whose name showed it firing. Renaming the test to
test_tag_with_no_attempt_is_blockedis what made it pass.Fixture vs live: unit and fixture only. The hook is not installed on this
machine, so the refusal has never fired inside a real session:
Revert Plan
Revert Plan
git revert <sha>— leaves the ledger from the first slice in place