Repository navigation
feat(hook): make a host's event set a declared capability, not an assumption - #266
Conversation
…umption Refs: CLOUD-45
CLOUD-45 Treat Claude-only events as optional capabilities
Why Acceptance
Refinement — Ready (host events are declared capabilities, never baseline assumptions) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Re-scope (evidence: Harness capability matrix (M1)): The capability table must be host × capability, not a list of Claude-only events — the asymmetry runs both ways. Gemini CLI's Constraints the table must encode:
|
|
|
/fast-forward |



Implements CLOUD-45, in the shape its own Re-scope section specifies rather
than the shape its title does.
The title encodes a false premise, and the issue says so
"Claude-only events" reads as Claude has more, others degrade. The M1 survey
(CLOUD-209) measured the asymmetry running both ways: Gemini's
BeforeModel/AfterModel/BeforeToolSelectionrewrite model traffic andconstrain tool selection; Cursor's
beforeReadFilesees full file contentsbefore a read. Claude Code can do none of those.
So the table is host × capability, as the Re-scope requires — not a list of
things other hosts lack.
What the table encodes
Capabilitiesis a row per host, every field a survey finding with a citation inthe code:
events— which lifecycle events the host emits.EventgainsTaskCompletedandConfigChange, the two genuinely Claude-only ones.ask— absent on Gemini (allow/deny only) and not in effect on Codex (itsschema lists it, its docs mark it "parsed but not supported yet" — advertised
is not available). A policy wanting human confirmation must hard-deny
there; degrading
askto allow would turn "check with a human" into "goahead", which is the one direction that must never be a fallback.
stop_vetoes_completion—falseon every surveyed host, Claude included.The field exists to make a uniform fact checkable rather than remembered:
behaviour keyed on "Stop blocks" is wrong everywhere, not degraded somewhere,
and a table omitting the column would let someone assume the opposite.
admin-deployed policy hooks), Cursor needs
failClosedper hook, Gemini readsunparseable stdout on exit 0 as Allow. These are host behaviours Batten cannot
change and must not forget, so they are capabilities too.
Degradation
Capabilities::degrademapsTaskCompletedto the Stop family on hosts thatlack it — the one substitution the survey endorses, since every host has a
Stop-family event.
ConfigChangedegrades to nothing: no honest stand-inexists for "a settings edit is happening", and substituting an unrelated event
would be worse than admitting the gap.
Degrading is explicitly not equivalence — the stand-in cannot veto either — so
a caller still has to read
stop_vetoes_completionbefore assuming it can block.What the substitution buys is observing the moment, not refusing it.
The dispatcher consults it before keying on anything
An event the host does not declare allows and fires nothing — never an error,
never a deny. An absent capability is a fact about the host, and refusing would
make Batten the reason a session cannot proceed on a host that simply offers
less. The note rides the ladder above
normal, because where it is reachable itis the ordinary state rather than news.
Two clippy lints allowed, with reasons
struct_excessive_boolswants the row folded into an enum or bitflags. Itspremise — several bools are usually one state machine in disguise — is false
here: these are mutually independent measured facts about third parties, found on
different pages of different vendors' docs. An enum would enumerate 2^n
meaningless combinations; bitflags would trade a named field for a bit position.
match_same_armswould collapse the rows that agree today. Each arm carries thecitation for why its values are what they are, and collapsing would delete those
reasons and make a future divergence a structural edit rather than a one-value
one.
Tests
Unit: table totality across every host × every event; no host vetoes completion
from Stop;
TaskCompletedis Claude-only and degrades to Stop everywhere else;ConfigChangedegrades toNone;askis absent exactly where the survey foundit absent; the fail-open edges are set where measured and not set elsewhere.
E2E: the same
TaskCompletedpayload under a host that declares it and four thatdo not — all allow, none emit a decision document, and a default run stays quiet;
asking for detail produces the note naming the fallback. The payload carries a
command the fixture policy denies, so an accidental adjudication of an undeclared
event would surface as a deny rather than passing silently.
The event census gains rows for both new variants — its own totality gate is what
caught them missing.
Refs: CLOUD-45