Implement the tighten-hook-protocol-contract RFC (moved to implemented/):
- HookDialect narrows to 'claude' | 'codex': the 'native' variant had zero
producers (native plugins on the seams write no hook/* provenance), and the
dialect is defined as the bridge that ran the hook.
- HookOutput.suppressOutput is gone: the codec parsed it and every path
discarded it with no warn and no deferral — hook stdout never enters a
transcript, so there is nothing to suppress.
- hook/result.durationMs is gone: durable timing telemetry with no reader
that the snapshot normalizer had to scrub as replay noise. With no duration
to measure, runHook loses its injected now clock and the single-field
RunHookResult wrapper — it returns the HookOutput directly. The committed
hook fixtures had the field stripped mechanically (field-only diff); the
stdout goldens never carried it.
- The bridges' double-defaulted defaultTimeoutMs config knob is replaced by
one reference-default constant, DEFAULT_HOOK_TIMEOUT_MS, exported from the
lib's runner and applied inside runHook; per-hook timeoutSec stays the
override surface.
- The hook/result semantics move into the lib that declares the event:
HookResultRecord now carries the decoded HookOutput and appendHookResult
derives the decision string (decision ?? stop-on-continue:false ?? pass)
and the 500-char stderrSummary truncation; both bridges delete their
byte-identical private copies. The snapshot suite passes against the
existing goldens, proving the derived values are unchanged.
- Rider: BLOCKING_EXIT_CODE is codec-internal again (zero importers).
Amend the hook-protocol-lib and hook-snapshot-matrix RFCs to the new facts,
update the lib/bridge READMEs and the session.md event tables, and retarget
the affected unit tests (including new lib-level coverage of the derivation
rules).
Address review on the hook-protocol PR: the event-scope guard only rejected a
`hookSpecificOutput` block whose `hookEventName` NAMED a different event than
the firing one. A block with NO `hookEventName` slipped through and applied its
event-scoped permission fields to whatever event was firing. Under the keyed
Claude Code schema (where `hookEventName` is part of the block) a missing
discriminator is as malformed as a mismatched one — a Stop/UserPromptSubmit
hook emitting a bare `{ permissionDecision: 'deny' }` could deny the current
point.
Drop the `eventName !== undefined` clause so the guard fires on both a
mismatch and an omission when the caller passes `expectedEventName`; the
opt-out (no expectedEventName) still applies a discriminator-less block as-is.
Flipped the test that pinned the old behavior (it documented an artifact, not
a contract) and proved the corrected one red on the old guard.
The reference schemas key the `hookSpecificOutput` block by `hookEventName`, so
a block naming a DIFFERENT event than the one firing is malformed — a hook
emitting `hookSpecificOutput.hookEventName: "PreToolUse"` on a `Stop` event must
not deny the Stop. The codec surfaced `hookEventName` for a bridge to compare but
never enforced the discard, so both bridges pushed every parsed output into the
merge unconditionally.
parseHookOutput now takes an optional `expectedEventName`; when the block's
`hookEventName` names a different event, its event-scoped fields
(permissionDecision/permissionDecisionReason/additionalContext/updatedInput) are
discarded (the discriminator is still surfaced for the log, and the
event-agnostic top-level decision/continue/etc. are unaffected). runHook threads
it via RunHookOptions.expectedEventName; a caller that omits it opts out.
Codex review finding on the bridges PR (PR-F); fixed here on the codec that owns
the fold and knows field provenance, then flows down to both bridges.