fix(hook-protocol): discard a hookSpecificOutput block whose hookEventName mismatches the firing event

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.
This commit is contained in:
Tianyi Cui
2026-07-01 10:45:58 +08:00
parent c28d6b837b
commit 24e9c0fa70
6 files changed
+107 -13

No files matched your search

+8 -1
View File
@@ -31,6 +31,13 @@ export interface RunHookOptions {
defaultTimeoutMs: number
/** Whether to append a trailing newline to the stdin payload (CC yes, Codex no). */
trailingNewline: boolean
/**
* The event this hook is firing for (e.g. `'PreToolUse'`). When set, a
* structured `hookSpecificOutput` block whose `hookEventName` names a DIFFERENT
* event is treated as malformed and its event-scoped fields are discarded (see
* {@link parseHookOutput}). Omit it to apply any block as-is.
*/
expectedEventName?: string
}
/** The {@link HookOutput} plus the wall-clock duration of the run (for `hook/result`). */
@@ -75,7 +82,7 @@ export async function runHook(
// `undefined` (a non-blocking error — no clean exit code to act on).
const exitCode = result.exitCode ?? undefined
return {
output: parseHookOutput(exitCode, result.stdout.text, result.stderr.text),
output: parseHookOutput(exitCode, result.stdout.text, result.stderr.text, options.expectedEventName),
durationMs: now() - started,
}
} catch (error: unknown) {