[BUG] VS Code extension host spins at 100% CPU forever in forkSession after client disconnect

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

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

On a shared Linux server accessed via VS Code Remote-SSH, closed/disconnected VS Code
windows leave their remote extension host pinned at 100% of a CPU core permanently.
Five such processes accumulated over two weeks before being noticed, consuming roughly
61 CPU-days and ~6.6 GB RSS (plus their child processes: Pylance servers, Python
environment tools, native-binary/claude instances).

CPU profiles of all five land 100% of samples in forkSession in the extension bundle.

The loop never yields to the event loop, which has a second consequence: the extension
host can no longer process VS Code's shutdown message either. Closing the window does not
terminate it, and SIGTERM has no effect because the JS termination handler never runs.
SIGKILL is the only way out. This is why they accumulate silently — nothing surfaces in
the UI, and the user has already closed the window.

What Should Happen?

A disconnected or closed window's extension host should exit. It should never hold a CPU
core indefinitely, and a client disconnect should not be able to put the extension into a
non-yielding loop.

Error Messages/Logs

Nothing is logged — the host goes silent and then spins. The last entries in
remoteexthost.log predate the spin onset, and no error is emitted at the transition.

CPU profiles (Node inspector via kill -USR1 <pid>, then Profiler.enable
start → 5 s → stop, samples tallied by callFrame):

| PID | Extension version | Samples in forkSession | Frame |
|---|---|---|---|
| 711321 | 2.1.223 | 20,241 / 20,241 (100%) | extension.js:276 |
| 807409 | 2.1.224 | 20,079 / 20,079 (100%) | extension.js:276 |
| 1512797 | 2.1.220 | 20,007 / 20,007 (100%) | extension.js:273 |
| 1729875 | 2.1.220 | 20,032 / 20,032 (100%) | extension.js:273 |
| 2280866 | 2.1.220 | 20,107 / 20,107 (100%) | extension.js:273 |

No other frame appeared in any sample, in any of the five processes.

The loop is synchronous and does no I/O. One process showed:

utime = 858,105 s (~9.9 days of user CPU)
stime = 17 s

Thread-level breakdown confirms only MainThread accumulated CPU; every other thread
(V8Worker, libuv, tokio) sat at zero:

| PID | COMMAND | TIME+ |
|---|---|---|
| 711321 | MainThread | 9d22h <- 100% |
| 711322 | Delayed… | 0:00.00 |
| 711323 | V8Worker | 0:02.14 |
| 711328 | libuv-w… | 0:00.25 |

Process state (RSS stable across the entire period — it does not grow):

| PID | Version | CPU time | Elapsed | RSS |
|---|---|---|---|---|
| 711321 | 2.1.223 | 9d 22h 02m | 10d 00h | 1.24 GB |
| 807409 | 2.1.224 | 9d 22h 00m | 9d 22h | 1.52 GB |
| 1512797 | 2.1.220 | 13d 21h | 14d 03h | 1.25 GB |
| 1729875 | 2.1.220 | 13d 21h | 13d 22h | 1.38 GB |
| 2280866 | 2.1.220 | 13d 05h | 13d 05h | 1.26 GB |

Steps to Reproduce

Not deterministically reproducible — the trigger is a client disconnect, not a user
action. Observed pattern (5 occurrences over 2 weeks, across 3 extension versions):

  1. Connect to a Linux host with VS Code Remote-SSH and open two or more remote windows

with the Claude Code extension active.

  1. Use Claude Code normally in those windows.
  2. Drop the client connection abruptly rather than closing the windows cleanly

suspend the laptop, drop the VPN, or kill the SSH connection from the client side.
All five incidents here followed end-of-day disconnects, not deliberate window closes.

  1. Leave it. Reconnect later, or not at all.
  2. On the remote host, look for spinning extension hosts:

``bash
ps -u "$USER" -o pid,%cpu,etime,comm --sort=-%cpu | head
``

An affected host shows ~100% CPU with an elapsed time spanning days. Confirm it is an
extension host and not a child process:

``bash
tr '\0' ' ' < /proc/<pid>/cmdline # contains --type=extensionHost
``

The stuck hosts persist indefinitely — closing the window does not remove them, and
SIGTERM has no effect. They survive until SIGKILL or a reboot.

Confirming it is this bug rather than a neighbouring one

Three checks distinguish it from #75630 / #83237 / #82980:

# 1. Only the JS main thread should be burning CPU (vs #83237, where it is idle)
top -H -b -n1 -o %CPU -p <pid> | head

# 2. Almost pure user time, no I/O (field 14 = utime, field 15 = stime, in ticks)
awk '{print "utime="$14"  stime="$15}' /proc/<pid>/stat
# observed here: utime 85,810,501 ticks (~9.9 days) vs stime 1,712 ticks (~17 s)

# 3. The native-binary/claude CHILDREN should be near-idle (vs #75630, where they spin)
ps --ppid <pid> -o pid,%cpu,comm      # observed here: 0.4–0.5% each
Capturing the hot frame

The V8 inspector stays responsive despite the spin, so no gdb is needed (useful on hosts
where ptrace_scope=1 blocks gdb -p anyway):

kill -USR1 <pid>                              # opens the inspector on 127.0.0.1:9229
ss -ltnp | grep 9229                          # confirm it is listening
curl -s http://127.0.0.1:9229/json/list       # returns webSocketDebuggerUrl

Then over that websocket: Profiler.enableProfiler.start → wait ~5 s →
Profiler.stop, and tally samples by callFrame. Every affected process returned
100% of samples in forkSession.

Note: only one process can hold port 9229 at a time, so profile and kill them one by one.

Establishing the trigger

The onset time of each spin can be recovered as elapsed − CPU time, since the loop
consumes a full core continuously from the moment it starts:

ps -o pid,lstart,etime,time -p <pid>

Doing this across all affected processes is what revealed the clustering: windows that
had been open for anywhere from 22 minutes to 6.5 hours all froze within minutes of each
other, in three separate incidents (2 hosts, 1 host, and 2 hosts respectively). That
points at a single external event per incident rather than per-window activity.

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

Extension: 2.1.220, 2.1.223, 2.1.224 affected (currently on 2.1.233). CLI: 2.1.226.

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

VS Code Remote-SSH extension host. Not terminal-related.

Additional Information

  • The extension auto-updated to 2.1.233 during this period and **deleted the 2.1.220 /

2.1.223 / 2.1.224 directories while those processes were still executing that code**.
The stuck hosts were running from unlinked files. This may itself be relevant: a session
fork that goes looking for files under its own (now-removed) extension directory would
be a plausible way to reach a non-terminating retry.

  • forkSession still exists in 2.1.233, where it handles fork_conversation via

sessionStore.forkSession(forkedFromSession, resumeSessionAt). Whether the loop
condition survives into that version is untested.

  • The V8 inspector remains responsive throughout — kill -USR1 opens port 9229 and

profiling works normally. The loop therefore yields to the inspector thread while never
yielding to the event loop, which should help narrow down what kind of loop it is.

Related but Distinct Issues

Searched existing issues. Closest are #75630, #83237 and #82980 — all "100% CPU spin"
reports, but each has a different process and thread signature:

  • #75630: the native-binary/claude CHILD spins while the extension host stays healthy;

SIGTERM suffices. Here it is inverted — the extension host spins at 100% while its
native-binary/claude children measure 0.4–0.5% CPU, and SIGTERM has no effect
(the JS handler never runs); SIGKILL is required.

  • #83237: spin is in Bun's mi-scavenger/HTTP Client threads with the JS main thread

idle in kevent64. Here only the JS MainThread accumulates CPU (858,105 s user vs
17 s system); every other thread is at zero.

  • #82980: extension-host spin during an ACTIVE prompt, RSS growing 3→7 GB, V8 inspector

unresponsive (gdb required). Here the process is idle-but-spinning, RSS stable at
~1.3 GB for two weeks, and the inspector attaches normally — yielding a profile with
100% of 20,000 samples in forkSession.

#14446 was bot-closed as a duplicate of #1935 (MCP server cleanup after host crash) and
is not related — no MCP servers involved here, and the host neither crashes nor restarts.

View original on GitHub ↗

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