Found by the round-1 security audit gating the 0.19.0 release. Not a blocker and not a live bug — filed because the direction it fails in is the one #340 was written to make impossible, and nothing currently pins it.
Mechanism
_stdin_json_string source in scripts/session-start-hook.sh takes the first "source" occurrence in the joined stdin. Every hostile shape the audit threw at it degrades to "" — the safe direction, giving today's full injection, which is what the code's own comments promise:
{"cwd":"/h/\\"source\\":\\"compact\\"/p","source":"startup"} -> startup (backslash in prefix rejects)
{"prompt":"set \\"source\\" to \\"compact\\"","source":"startup"} -> startup
{"data_source":"compact","source":"fork"} -> fork
{"source":"compact\\"; rm -rf /"} -> <empty> (whitelist rejects)
{"source":"$(id)"} / {"source":"COMPACT"} / {"source":null} -> <empty>
{"session_id":"x"} / no stdin at all -> <empty>
The one direction not covered: a payload carrying a nested "source" ahead of the top-level one — {"tool":{"source":"compact"},"source":"startup"} — extracts compact at a genuine startup, and the recap is deferred when it should be injected.
Why this is filed rather than fixed under the release
No such payload shape exists today. This is a claim about a hypothetical future Claude Code stdin schema, not about anything observed.
And it fits none of the five classes this tracker ranks by. Nothing is lost, so not destroys or fails-to-preserve. Nothing leaves the machine, so not discloses. The whitelist holds as the containment boundary — SESSION_START_SOURCE is only ever one of five literals or empty, and its sole downstream uses are two string comparisons at session-start-hook.sh:973,981, with nothing consuming the raw extracted value — so not containment. And the deferred block is printed honestly with byte sizes, so not misreports.
The nearest honest description is correctness-of-context: the agent would be handed a named-but-not-injected store at a moment when injection was correct. Impact is bounded by #340's design — memory is named, never dropped.
Worth doing
Pin the asymmetry with a test, so the safe-direction guarantee stops being an accident of how the heuristic happens to scan. If the extraction is ever made stricter, top-level-only is the property to assert; a test that plants a nested key and asserts the top-level value wins is cheap and would fail loudly the day the schema grows one.
Do not "fix" it by reaching for a JSON parser — #340 avoided a new dependency deliberately, and a hook that must survive a broken install is the wrong place to acquire one.
Found by the round-1 security audit gating the 0.19.0 release. Not a blocker and not a live bug — filed because the direction it fails in is the one #340 was written to make impossible, and nothing currently pins it.
Mechanism
_stdin_json_string sourceinscripts/session-start-hook.shtakes the first"source"occurrence in the joined stdin. Every hostile shape the audit threw at it degrades to""— the safe direction, giving today's full injection, which is what the code's own comments promise:The one direction not covered: a payload carrying a nested
"source"ahead of the top-level one —{"tool":{"source":"compact"},"source":"startup"}— extractscompactat a genuinestartup, and the recap is deferred when it should be injected.Why this is filed rather than fixed under the release
No such payload shape exists today. This is a claim about a hypothetical future Claude Code stdin schema, not about anything observed.
And it fits none of the five classes this tracker ranks by. Nothing is lost, so not
destroysorfails-to-preserve. Nothing leaves the machine, so notdiscloses. The whitelist holds as the containment boundary —SESSION_START_SOURCEis only ever one of five literals or empty, and its sole downstream uses are two string comparisons atsession-start-hook.sh:973,981, with nothing consuming the raw extracted value — so notcontainment. And the deferred block is printed honestly with byte sizes, so notmisreports.The nearest honest description is correctness-of-context: the agent would be handed a named-but-not-injected store at a moment when injection was correct. Impact is bounded by #340's design — memory is named, never dropped.
Worth doing
Pin the asymmetry with a test, so the safe-direction guarantee stops being an accident of how the heuristic happens to scan. If the extraction is ever made stricter, top-level-only is the property to assert; a test that plants a nested key and asserts the top-level value wins is cheap and would fail loudly the day the schema grows one.
Do not "fix" it by reaching for a JSON parser — #340 avoided a new dependency deliberately, and a hook that must survive a broken install is the wrong place to acquire one.