[BUG] Stop hook block-cap override is indistinguishable from a legitimate pass — a slow-but-correct policy check gets silently defeated by repetition

Open 💬 0 comments Opened Jul 15, 2026 by ThatDragonOverThere

The problem

A Stop hook can return a blocking decision to prevent a turn from ending until some condition is met — for example, requiring a verification step to run before the session is allowed to stop. That's documented and works as expected for a single block.

What isn't documented, but is real and reproducible: if a Stop hook keeps blocking across repeated turns — because the condition it's checking for genuinely hasn't been satisfied yet, not because the hook is broken — the framework eventually forces the turn to end anyway. I watched this happen directly: a hook enforcing "a verification step must complete before this session stops" blocked nine consecutive turns, then the turn ended regardless of the hook's decision.

That override behavior itself might be reasonable as a safety valve against infinite loops. The actual problem is what happens afterward: there is no signal anywhere — not in the hook's own next invocation, not in any other hook, not in the transcript, not anywhere a later process could read — that distinguishes "the hook passed because its condition was satisfied" from "the hook was still blocking and got force-overridden by hitting an undocumented cap." Both produce the exact same visible outcome: the turn ended.

stop_hook_active exists and is passed back to the hook on a subsequent invocation, but it only tells the hook it is being re-invoked after having blocked before — it doesn't communicate, to the hook or to anything else, that a specific stop was an override rather than a clearance. That's the actual gap: the one field that exists solves a narrower problem (loop prevention within the hook itself) than the one this creates (nothing outside the hook can ever tell the two outcomes apart).

Why this is worse than it looks

Consider a hook enforcing a policy check that takes real time to run correctly — a thorough audit, a slow verification, anything that legitimately can't complete instantly. If that check takes longer than whatever the undocumented cap tolerates, the hook's own legitimate, in-progress verification gets treated identically to "verification never started." The turn ends either way, and every downstream consumer of that fact — logs, other hooks, dashboards, anything built on top of "did this policy get satisfied" — has no way to know which one actually happened.

That inverts the incentive the hook exists to create: a slow-but-correct check is punished exactly as hard as no check at all, and the failure is silent — nothing errors, nothing flags it, the turn just ends and the record looks clean. Over time that's how a real control gets abandoned: not because someone decided to weaken it, but because it keeps getting silently defeated by its own thoroughness and nobody can prove it happened.

What would fix it

Expose whether a given Stop event's resolution was a genuine hook pass or a forced cap-override — as a field in the hook's own next input, and ideally somewhere a process other than the hook itself can read (transcript metadata, a session-level flag, anything queryable after the fact). That would let a hook author build the correct pattern themselves: track when a long-running check was actually dispatched, let the block pass while it's legitimately in flight within a reasonable window, and only escalate to a real failure — a distinguishable one — if that window elapses with nothing to show for it.

Related

Two other findings from the same investigation, same shape (a control that looks like it worked but the aftermath can't prove which outcome actually happened): #77531 and #77667.

View original on GitHub ↗