[BUG] VS Code extension spawns a new claude --resume process on every session-tab activation and never stops the previous one

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 1, 2026

What's Wrong?

The extension starts a new claude --resume=<sessionId> process every time a session tab
becomes active
. It does not check whether a process for that session already exists, and
it does not stop the previous one. Processes accumulate for the lifetime of the window.

The duplicate-prevention guard exists but is keyed on channelId, which is freshly
randomised on every launch — so it can never match. There is no sessionId-based liveness
check anywhere in the launch path.

On this machine that reached 42 live claude processes across 15 conversations, holding
10.18 GB
— of which 6.52 GB is redundant (27 surplus processes). Eight of the fifteen
sessions had four processes each.

Two consequences beyond the memory:

  • Background work started by the superseded process is orphaned. When a tab respawns,

the webview moves its channel to the new process. Any background shell job, subagent,
or monitor the old process started keeps running, but the agent can no longer collect its
result. I hit this with a monitoring script that stayed parented to the abandoned process
and whose completion was never reported.

  • Under a two-window condition it forks the transcript and loses history. Filed

separately as #83719, since the severity and reproduction differ.

The duplicates do not make API calls. That is probably the first thing to wonder about,
so I checked it four independent ways: CPU time (0.4–2.0 s over 11 minutes — idle — against
7.0 s for a process actually working), no forked assistant branches in the transcript, all
request IDs unique, and a flat request rate across spawn waves. Token consumption is not
multiplied.

Steps to Reproduce

  1. Open the Claude Code panel with at least two session tabs.
  2. From tab A, switch to another tab, then switch back to A.
  3. ps -eo command | grep -c 'claude .*--resume=<A's sessionId>'

The count goes up by one every time you return to A. Nothing exits. Repeat and it grows
without bound. 100% reproducible.

Measured here (extension log, JST):

09:55:31.902  new_conversation_tab
09:55:32.098  launch_claude channelId=b8suysvgszo                       (no resume)
09:55:32.202  Spawning Claude with SDK query function           -> PID 2393

09:55:35.189  get_session_request  46234561-…                           (switched back)
09:55:35.246  launch_claude channelId=8qubmed6jdp  resume="46234561-…"
09:55:35.289  Spawning Claude with SDK query function           -> PID 2445

38 → 40 processes, zero exits. Across a full day of logs:

launch_claude                = 12   Closing Claude on channel = 0
launch_claude                = 10   Closing Claude on channel = 0
Channel already exists       = 0    (the guard never fires)

The only Closing Claude on channel lines all day matched moments when I manually
SIGTERM-ed processes — never a new spawn.

Root cause

Names below are from the minified bundle, beautified locally for reading.

1. The duplicate guard is keyed on channelId, not sessionId — and channelId is
freshly randomised per launch, so the guard can never match.

// extension host
async launchClaude(e, t, r, n, i) {          // e = channelId, t = resume (sessionId)
  if (this.channels.has(e) || this.pendingChannelInputs.has(e)) {
    this.logger.error(`Channel already exists: ${e}`); return
  }
// webview caller
async launchClaude() {
  if (this.claudeChannelId) return …;              // per-instance memo only
  let e = Math.random().toString(36).slice(2);     // new channelId every time
  …
  let i = t.launchClaude(e, this.sessionId.value, …);
}

this.claudeChannelId lives on the session object instance, so it is lost on webview
reload, panel recreation, or whenever doListSessions() rebuilds those instances. Nothing
consults process state or a session lock.

2. Nothing closes the previous process at spawn time. closeChannel() runs only on an
explicit close_channel from the webview, on panel dispose, or on extension-host exit.
sessionPanels is keyed by sessionId, but on collision it overwrites the entry instead of
terminating the previous process:

for (let [f, m] of this.sessionPanels) if (m === e && f !== u) this.sessionPanels.delete(f);
if (this.sessionPanels.set(u, e), e.active) this.activeSessionId = u

3. The trigger is an unconditional reactive effect on active-session change:

uo(() => {
  let n = this.activeSession.value;
  if (n) { … n.preloadConnection() … }   // -> launchClaude()
})

4. deserializeWebviewPanel passes undefined for the sessionId
setupPanel(g, undefined, undefined, E) — so restored panels are never registered in
sessionPanels. After a VS Code restart the sessionId-level bookkeeping is absent entirely,
which is when I saw the largest batch of duplicates appear.

There is also a bulk-terminate path (Sb + process.on("exit", …)) that SIGTERMs every
spawned child, but it only runs when the extension host itself exits. It is the last safety
net, not per-session cleanup.

Still present in the current release. Reproduced on 2.1.220; in 2.1.221 all four markers
above are byte-identical.

Ruled out

Checked in code and against logs, none of these are the trigger: extension-host
crash/restart (exthost.log shows one Extension host with pid … started, no restarts),
sleep/wake (pmset -g log events don't line up with the spawn times), network reconnect
(network.log is 0 bytes), window focus (onDidChangeWindowState does not appear in the
bundle), auth-token refresh (no matching interval constant), and setInterval health checks
(the four sites are generic polling, voice keepalive, and silence detection).

Measuring memory

Sum RSS across these processes and you get 17.35 GB, but that double-counts shared pages.
Measured with footprint (physical footprint) instead:

15 sessions, 42 processes
  processes per session: 4 -> 8 sessions, 3 -> 1, 2 -> 1, 1 -> 5

  sum of RSS        : 17.35 GB   (inflated ~69%)
  sum of footprint  : 10.18 GB   (actual)
  surplus (27 procs): 6.52 GB
  per process       : 110 MB min / 258 MB median / 341 MB max

Some duplicates in this snapshot were induced by my own reproduction testing; the steady
state before testing was 39 processes across 10 conversations.

Suggested direction (untested)

A pointer rather than a fix — I have not validated any of this beyond reading the bundle:

  1. Key the in-flight registry on sessionId rather than channelId, and consult it before

spawning.

  1. In the extension host's launchClaude(), reverse-look-up this.channels by sessionId;

if a live process exists, reuse it, or closeChannel() it before spawning a replacement.

  1. Pass the sessionId from deserializeWebviewPanel() into setupPanel() so restored panels

are tracked.

  1. Pair the active-session effect's preloadConnection() with closing the channel of the

session being deactivated.

Detection, if useful

~/.claude/sessions/<pid>.json records {pid, sessionId, cwd, procStart, …} per process.
Because the file is per-PID, up to four live PIDs claimed the same sessionId here. Tooling
that resolves sessionId → process sees only one of them and reports the situation as
healthy, which is why this went unnoticed for days. Counting entries in that directory by
sessionId is a reliable detector.

Environment

| | |
|---|---|
| Extension | anthropic.claude-code 2.1.220 (reproduced); 2.1.221 byte-identical on all relevant paths |
| VS Code | 1.131.0, commit e4c7e7b1d6d060162f4aa7f8225271b67ce1df75, arm64 |
| OS | macOS, Darwin 25.5.0, arm64 |
| CLI | Not installed separately — only the extension-bundled binary was running |

Logs

--debug-to-stderr output lands in:

~/Library/Application Support/Code/logs/<launch>/window<N>/exthost/Anthropic.claude-code/Claude VSCode.log

It rotates at 5 MB × 7, which on a busy day is a few hours. All timestamps above come from
these files; happy to supply excerpts.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗