SessionStart hooks fire for phantom sessions that never materialize (no transcript is ever created), racing the real session within a few hundred ms
Summary
Claude Code sometimes fires multiple SessionStart hooks for the same project within a very short window (measured 315ms / 488ms apart). One of them belongs to the session the user actually gets; the other belongs to a "phantom" session that:
- never creates its transcript file (
~/.claude/projects/<proj>/<session_id>.jsonlnever appears), - never fires
UserPromptSubmit,Stop, or any subsequent hook, - but receives a fully-formed
SessionStartpayload — including atranscript_path— indistinguishable from the real one at hook time.
Any hook that performs a one-shot, consume-once action keyed on "the next session start" (state handover, token/baton consumption, migrations, telemetry sessions) can be consumed by the phantom, silently breaking the real session.
Measured data (single machine, macOS, VS Code + CLI mix)
From a private hook's decision log covering 3,222 SessionStart events across ~2 months:
- 56 pairs of
SessionStartevents fired for the same project within 2 seconds of each other (different session_ids), i.e. near-simultaneous double starts are routine. - Phantom characteristics verified against the payload's own
transcript_path: the file is never created, while the twin session's transcript exists and keeps growing. - Observed on versions from at least 2.1.1xx (2026-05) through 2.1.211 (2026-07-17). Both
source: "startup"andsource: "clear"twins were observed. - Timing note for hook authors: even for the real session, the transcript file is created ~460ms after the
SessionStarthook fires, so "does the transcript exist" cannot be used inside the hook to tell real from phantom.
Concrete incident (2026-07-17, times UTC), two SessionStart events 488ms apart for the same cwd:
07:39:59.395 session c4f05b96-… source=startup ← phantom: transcript never created, no further hooks
07:39:59.883 session 11e62b74-… source=startup ← real: transcript created 07:40:00.344, session continues
Our hook consumed a one-shot handover token at SessionStart; the phantom consumed it first and the real session started without its state. (We have since redesigned the hook to defer consumption to the first UserPromptSubmit, which a phantom never fires — that works as a mitigation.)
Expected behavior
Either of:
SessionStartfires only for sessions that will actually materialize (or a cancellation signal likeSessionEndis delivered for phantoms), or- Documentation explicitly states that
SessionStartmay fire for sessions that never materialize, so hook authors know not to perform consume-once side effects there.
Repro sketch
We do not have a deterministic trigger; frequency data suggests it correlates with window/webview reloads and /clear in the VS Code extension (double source:"clear" twins were observed in May). Logging session_id + transcript_path existence from a SessionStart hook and diffing against transcripts that actually appear reproduces the detection.
Environment
- Claude Code 2.1.195 – 2.1.211 (phantoms observed since at least 2026-05 on earlier versions)
- macOS 15.x (Darwin 25.5.0), VS Code extension + CLI
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗