Skip to content

[Bug] "Not decision-eligible" is overloaded, and subagentStop is labelled inconsistently between its schema and hooks.md #146

Description

@rocklambros

What is broken

"Not decision-eligible" means two different things in this specification, and subagentStop carries a different label in its schema than it does in the hook catalogue.

hooks.md uses the phrase for postCompact and then permits a disposition in the same sentence:

Decision: Not decision-eligible — compaction has already occurred. A Guardian MAY return MODIFY (rewrite the summary, e.g. to redact a region the policy can't compact), but MAY NOT return DENY.

It uses the same phrase for turnEnd, where nothing is permitted:

Decision: Not decision-eligible — the turn has already happened.

So the phrase covers both "MODIFY is available, DENY is not" and "no disposition is available." Nothing distinguishes the two readings.

subagentStop then gets one label in each place. Its schema says:

Not decision-eligible — the subagent has already terminated.

hooks.md says:

Decision: Audit only.

Given the postCompact precedent, those are not the same statement. "Not decision-eligible" demonstrably permits MODIFY. "Audit only" does not. Two normative surfaces describe the same hook and a reader cannot tell which governs.

Where

specification/v0.1.0/hooks/subagent-stop.json, docs/spec/instrument/hooks.md, docs/spec/conformance.md

What the specification says, and what happens instead

The label is doing real work, because §6.5 now builds machinery that keys off it.

§6.5 requires a Guardian facing a MODIFY-incapable client to substitute DENY, and then carves out a named exception for the case where DENY is not legal:

Exception — postCompact. postCompact fires after compaction has occurred and DENY is not a legal disposition there, so the substitution above has no legal answer at that hook. For a MODIFY-incapable client the Guardian MUST return ALLOW at postCompact and MUST record an audit event with reason_codes: ["modify_unsupported"] naming the rewrite it could not deliver, so the unmodified summary standing is visible rather than silent.

That exception describes a class: content has already been produced, DENY is meaningless because there is nothing left to stop, MODIFY is the only gate that does anything, and a client that cannot apply it substitutes ALLOW and audits the gap. The class currently has one member.

subagentStop fits the same description in every respect. Both hooks define summary with required: ["value", "provenance"]. Both carry content produced by a process that has finished into a context another process will act on. Both state their rationale the same way, that compaction has already happened and that the subagent has already terminated. postCompact treats that rationale as ruling out DENY alone. subagentStop inherits a label that, read through hooks.md, rules out everything.

The question this issue asks is which of those two outcomes was intended for subagentStop, because the artifacts disagree and the overloaded phrase makes the disagreement easy to miss.

Impact on implementers

A Guardian implementer reading the schema and a Guardian implementer reading hooks.md build different things, and both can defend their reading. That is a conformance-testability problem before it is a security problem, since no test can be written against a requirement two normative documents state differently.

If "Audit only" is the intended answer, the security consequence should be recorded rather than left implicit. A Guardian that enforces output redaction at toolCallResult has no equivalent control on subagent output, so content the redaction gate would catch reaches the parent intact when it arrives through a subagent instead. That is reachable by anyone who can influence what a subagent retrieves, and it does not require compromising the subagent, only feeding it. The monotonicity rule that postCompact states normatively, that no amount of LLM processing launders untrusted-classified data into trusted-classified data, has no attachment point on the subagent return path.

If "Not decision-eligible" in the postCompact sense is the intended answer, then subagentStop belongs in the §6.5 exception alongside postCompact and the fix is small: MODIFY available, DENY not, and a MODIFY-incapable client returns ALLOW with the same audit event. Nothing new goes on the wire.

Proposed, in the order that keeps the two questions separate:

  1. Define "Not decision-eligible" once, or stop using it for two different rules. The cleanest version states eligible dispositions per hook rather than naming a category, which also removes the schema and hooks.md disagreement by construction. Tracked more broadly in the eligibility-declaration issue, since six hooks constrain eligibility today across five different phrasings.
  2. Then settle subagentStop on its merits rather than on an inherited label.

The counterargument to gating, stated so a reviewer does not have to raise it: a MODIFY gate on subagent return adds a round trip on a hot path in coding-agent workloads, paid per subagent return by exactly the deployments that fan out most. That cost is the reason to keep DENY unavailable and to leave MODIFY optional under the SHOULD-support level, not a reason to leave the hook unlabelled.

Written against the tree as PR #21 leaves it. #21 moves final_chain_hash out of subagent-stop.json's required set and adds §6.5 with the postCompact exception quoted above. It does not touch the "Not decision-eligible" sentence, so every quotation here survives that merge. Related: #132 tracks the floor decision #21 implements, and #131 wires subagentStop emission in the adapters, which is the producing side rather than this question.

Current Priority Scope

Feeds conformance evidence

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:needs-triageNot yet triaged. Applied by the issue formstype:bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions