[BUG] SessionStart:startup systemMessage intermittently not rendered in the interactive TUI (deferred hook set dropped on session.id change)
Preflight
Searched existing issues. This is related to #96699 and #96698, but it is a different path:
- #96699:
/clearmessages are never shown in the default renderer. - #96698: the message is dropped deterministically on resume/fork when
additionalContextrepeats. - This report: on a plain
startup, the SessionStartsystemMessageis shown only some of the time. The drop follows from how the interactive TUI defers SessionStart hook results; see "Likely cause".
What's Wrong?
In the interactive terminal UI, a SessionStart hook's systemMessage (⎿ SessionStart:startup says: …) appears on screen only intermittently for the same hook and the same output.
- We measured 4 of 15 interactive starts on 2.1.278 (tmux- and expect-driven). None of the variables we changed correlated with the result:
--verbose, prompt text, the trust dialog, the driver, or hook latency (our hook takes 73–102 ms). - On the same starts, the hook's
additionalContextreached the model every time we checked (3/3). claude -p … --output-format stream-json --include-hook-eventsalways shows the completehook_responsewith thesystemMessagein it, so the hook output is correct. Only the on-screen display is lost.- Emitting on stderr with exit code 2 is not a more reliable alternative. It lands in the same deferred message set (see below) and renders as a hook error.
Today's retest on 2.1.284 happened to render on both starts we tried (startup, then clear). The intermittency is real, but it is not constant.
What Should Happen?
A SessionStart hook's systemMessage should be displayed on every interactive start. If the CLI deliberately discards it, a debug-log line should say so, so hooks can tell "shown" from "discarded".
Steps to Reproduce
- Register a SessionStart hook, via plugin or
settings.json, that prints:
``json``
{"systemMessage": "notice <random>", "hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "context <random>"}}
- Start
claudeinteractively about 10 times, e.g. in tmux, using a fresh trusted directory or the same one. - Count the starts that show
SessionStart:startup says: notice …. In our runs it was about 1 in 4. On the same starts the model can always repeat theadditionalContexttoken.
Likely cause
This comes from reading the bundled CLI (versions/2.1.284; the same symbols are in 2.1.278). Minified names are approximate.
- In the TUI, SessionStart results are deferred:
deferHookMessages(promise)storesthis._pendingHookSet = { run, sessionId: this.session.id }. - When the promise resolves,
_settleHookSet(h, v)inserts the messages only if all three of these hold:this._pendingHookSet === h && this.session.id === h.sessionId && v.length > 0. Otherwise it does nothing, silently. - Three paths clear the set first, all without logging:
- a
session.subscribe(...)callback registered instart()runs_releaseHookSet(pending, true)wheneversession.id !== pending.sessionId; awaitPendingHooks(), which the first prompt submission calls, drops the set and returns ifsession.idhas already changed;- any second
deferHookMessages/releasePendingHookMessages()call. additionalContextreaches the model by a separate path that this set does not gate. That explains why the model always sees the context while the screen line comes and goes.blockingError(exit 2) is pushed into the same array that_settleHookSetgates.- The wrapper around the deferred promise holds a real
AbortController, and its signal reaches the hook subprocess spawn. When the set is released, a still-running SessionStart hook can be killed mid-run, not just have its output ignored. Slow SessionStart hooks, such as installers, are exposed to this.
We could not find what changes session.id after boot: this.session is assigned once, in the constructor. So we don't know whether the churn happens on every interactive start or only in some cases.
Version
2.1.278 and 2.1.284 (same symbols), macOS, default renderer.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗