[BUG] VS Code window reload fires SessionStart twice (throwaway source=startup + resume) — creates a phantom session hooks cannot distinguish

Open 💬 0 comments Opened Jul 11, 2026 by msuter-cbre

Summary

In the VS Code extension, reloading the window (or reopening it) fires the SessionStart hook twice, a few seconds apart:

  1. source=startup with a brand-new, throwaway session_id, then
  2. source=resume with the durable session_id of the session actually being resumed.

The startup session from step 1 is immediately abandoned — the window resumes the session from step 2, and all subsequent activity (including MCP server subprocesses) runs under the durable id. But any SessionStart hook that records or bootstraps per-session state has already created an entry for the throwaway id. The result is a phantom session per reload, and there is no hook-visible field that distinguishes the throwaway startup from a genuine first-launch startup, so a hook cannot filter it out.

Environment

  • Claude Code v2.1.197 (VS Code extension, CLAUDE_CODE_ENTRYPOINT=claude-vscode), macOS. (Latest at time of filing ~v2.1.207; no related changelog entry seen.)

Reproduction

  1. Configure a SessionStart hook (matchers startup and resume) that logs the stdin payload source + session_id and the CLAUDE_CODE_SESSION_ID env var.
  2. Open a Claude Code chat in VS Code and use it briefly (so there's a session to resume).
  3. Reload Window (Cmd/Ctrl+Shift+P → "Reload Window").
  4. Inspect the hook log.

Actual behavior

The reload produces two hook fires (~3–5s apart; observed gaps ranged 0.3s–5.5s):

source=startup   session_id=<AAAA — new, never seen before>     env CLAUDE_CODE_SESSION_ID=<AAAA>
source=resume    session_id=<BBBB — the durable, pre-reload id>  env CLAUDE_CODE_SESSION_ID=<BBBB>

The <AAAA> session is never used again. A genuine fresh launch (new chat, nothing to resume) fires a single startup with its own id and no following resume.

The throwaway startup is indistinguishable from a genuine startup

Capturing the full SessionStart payload and the CLAUDE_*/VSCODE_* env for all three cases — throwaway startup, resume, and a genuine fresh-launch startup — the throwaway startup and the genuine startup are byte-for-byte identical in every field except the per-session session_id / transcript_path:

| Field | throwaway startup | genuine startup | resume |
| --- | --- | --- | --- |
| source | startup | startup | resume |
| payload session_title | absent | absent | present |
| env CLAUDE_CODE_CHILD_SESSION | 1 | 1 | 1 |
| env CLAUDE_CODE_ENTRYPOINT | claude-vscode | claude-vscode | claude-vscode |
| all other payload/env fields | identical | identical | — |

So CLAUDE_CODE_CHILD_SESSION=1 is present on all three (it appears to mark "running under the VS Code extension host," not a throwaway), and session_title only distinguishes resume from startup — neither separates a throwaway startup from a genuine one. env CLAUDE_CODE_SESSION_ID always equals the per-event payload session_id.

Impact

Any SessionStart-hook-based session tracking (analytics, per-session bootstrapping, audit/session records) accumulates one phantom session per window reload, with no reliable way to filter it — the hook has no signal at startup time that the session will be superseded by a resume.

Expected / requested

Either:

  1. Don't fire a source=startup for the throwaway session on reload — fire only source=resume for the session being restored (matching what actually happens); or
  2. If the transient startup is intentional, expose a field (payload or env) that flags it as superseded/child so a hook can no-op on it — e.g. a will_resume/superseded flag on the startup payload, or a distinct env marker that is genuinely absent on a real first launch.

Happy to provide the full captured payloads/env for all three cases.

View original on GitHub ↗