Two envelopes, one hook

Three stdin handlers in run-hook.mjs can start a hook. Two of them check whether it already ran. The third set the flag and never read it. Turn both dials on to see what reached stdout.

Stdin conditions

timeout fired first

A slow producer loses the race (#3415). The timeout runs the hook against an empty payload.

payload over the cap

An image paste (#620). The truncation branch runs the hook again.

The asymmetry, in one table

handlerlineguards on stdinClosed?
process.stdin.on('end'):442yes, if (!stdinClosed)
process.stdin.on('error'):459yes, if (!stdinClosed)
process.stdin.on('data'):421NO: sets it, never reads it
Measured across six stdin shapes. Only the two that conjoin a post-timeout arrival with an over-cap payload double-emit, including the multi-chunk variant. Each condition alone was always fine, which is why three green tests sat on top of this.

Why it surfaced now

The defect predates CC 2.1.248. What that release changed is the consequence:

CC versionwhat happens to {...}{...}
2.1.247 and earliersilently treated as plain text
2.1.248 onwardreported hook error, result discarded
Neither is good. On UserPromptSubmit and SessionStart the plain-text channel is the context channel, so the old behaviour injected a doubled envelope as prompt text. The new behaviour at least says so out loud.