SessionStart hook additionalContext, when not bound in time, is redelivered later as a mislabeled origin:human queued prompt

Open 💬 0 comments Opened Jul 7, 2026 by AJBcoding

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest related: #10373, #13650 — both describe SessionStart additionalContext being silently lost; this report describes it being redelivered later, mislabeled as human input, which is a distinct and arguably higher-severity variant)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

Environment

  • Claude Code Version: 2.1.202
  • Platform: macOS (Darwin arm64)
  • Node.js Version: v22.22.3

What's Wrong?

When a SessionStart hook's additionalContext output isn't bound into the session in time (the session has already begun processing), the content does not appear to be silently dropped (as in #10373 / #13650). Instead, it resurfaces later, mid-session, delivered through the same path as genuine human-typed CLI input — tagged with human provenance — and presented to the model as "The user sent a new message while you were working: <hook content>".

This means automated, tooling-generated session-priming content can become indistinguishable from real user input at the harness level, including any directive/imperative language the hook happens to include (e.g. "you MUST address this"). In the observed case, the receiving model session correctly treated this as suspicious and flagged it as a likely prompt-injection attempt before eventually tracing it back to hook-delivery timing — i.e. the mislabeling was convincing enough to raise a legitimate injection concern.

Expected Behavior

Per the hooks documentation, a SessionStart hook that exits 0 with:

{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "..."}}

should have that content added to context, attributed as hook/tooling-originated. If timing prevents that binding, the content should either be discarded (matching #10373/#13650's behavior) or, if it must still be delivered, it should retain clear tooling/hook provenance — never be presented as if freshly typed by the human user.

Actual Behavior

  • The SessionStart hook ran and produced valid hookSpecificOutput JSON (confirmed via hook-side logging: the hook completed in under 1 second).
  • Its content did not appear as bound hook context at session start.
  • Several tool-call turns into the same session, the exact same content (verbatim, including a trailing directive sentence) arrived as if it were a new human chat message, wrapped in the harness's queued-user-input presentation.
  • Session transcript JSONL evidence:
  • An early record (type: queue-operation, operation: enqueue) timestamped essentially simultaneous with true session start, containing the full hook payload.
  • A later record for the same content tagged commandMode: prompt, origin: {kind: human}, entrypoint: cli, userType: external.
  • This is reproducible in the sense that it has now been observed independently across more than one session using the same SessessionStart hook configuration; it appears to correlate with the hook's completion time being non-trivial (several hundred ms to ~1s) rather than near-instant, though I don't have an exact threshold.

Steps to Reproduce

  1. Configure a SessionStart hook (matcher: "startup") whose command takes on the order of several hundred milliseconds to ~1 second to run (e.g. it does a small amount of I/O or a subprocess call) before printing:

``json
{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "TEST MARKER: this should be hook context, not a chat message."}}
``

  1. Start a brand-new interactive session under conditions with some system load/latency variance (this appears to be a timing-sensitive race, not a deterministic failure — it does not reproduce on every session start).
  2. Watch whether "TEST MARKER" ever appears as a normal user-turn message (wrapped as "The user sent a new message while you were working") rather than as invisible hook context. Inspect the session's transcript JSONL for a queue-operation/enqueue record followed by a commandMode: prompt, origin: {kind: human} record carrying the marker text.

Why This Matters

Beyond the correctness bug (hook content should bind as hook context, not get lost/relabeled), this has a security-adjacent angle: any tooling that primes sessions via SessionStart additionalContext — including directive language ("you must...") — can, under this race, be delivered in a form that is byte-for-byte indistinguishable from genuine human input. A model (or a human reviewing transcripts) has no reliable signal to distinguish "the user really typed this" from "a hook's output got misdelivered." This seems like it should at minimum preserve some marker of tooling origin even on the fallback path, given the injection-risk surface it opens.

Related Issues

  • #10373 — SessionStart hooks' additionalContext never bound for brand-new interactive sessions (qz("startup") not called on that path) — describes silent loss, not redelivery.
  • #13650 — SessionStart hook stdout "silently dropped despite valid JSON output and exit code 0" — also silent loss, no redelivery through another channel observed/reported.

Both confirm SessionStart additionalContext binding is unreliable in more than one way; this report is about the specific case where the content is not lost but instead resurfaces mislabeled as human-originated input, which I believe is a distinct mechanism worth tracking separately given its different (and arguably more concerning) impact.

View original on GitHub ↗