[BUG] VS Code: CLI with a running background task outlives a crashed extension host and holds the session ("still open somewhere else")

Status Open
Reported on v2.1.296
Maintainer reply None cached
Activity 0 comments · opened Oct 14, 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?

When the VS Code extension host dies (crash, exit code 5 in my case), a claude CLI that has a background Bash task still running does not exit. It is reparented to launchd (ppid 1), its stdio sockets have no peer, and its ~/.claude/sessions/<pid>.json entry stays with a live PID. A CLI with no background task exits within about a second and removes its entry.

VS Code restarts the extension host, the panel relaunches the conversation with waitForHolderToLetGo: true, and after ~10 s it shows "This conversation is still open somewhere else. Using it in two places at once can mix up its messages." The only holder is the orphan left by the crashed host of the same window.

If I continue, a second --resume process starts on the same session. There are then two live registry entries with the same sessionId. The new process reports the orphan's background task as stopped ("Background shell command didn't finish before the previous session ended"), although the task is still running under the orphan. When the orphan later exits, it appends its own last-prompt and cost-state after the new process's records, so the session metadata goes back to an older state.

What Should Happen?

The CLI should exit, stopping or handing off its background tasks, when its stdio peer goes away. Failing that, the extension should recognise a holder whose parent was its own crashed extension host and take the session over instead of warning.

Error Messages/Logs

main.log and window*/renderer.log:

10:41:49.617 [info] Extension host with pid 2570 exited with code: 5, signal: unknown.
10:41:49.626 [error] Extension host (LocalProcess pid: 2570) terminated unexpectedly. The following extensions were running: ..., Anthropic.claude-code, ...
10:41:49.630 [info] Automatically restarting the extension host.

exthost/Anthropic.claude-code/Claude VSCode.log:

10:44:54.952 [info] Received message from webview: {"type":"launch_claude","channelId":"qo62x57p7w","cwd":"<string>","resume":"<string>","freshStartIfUnresumable":false,"permissionMode":"<string>",...}
10:44:57.012 [info] Session 5333e4a4-0abb-4d21-825a-2c3abefa3c8e is still open in another Claude process; waiting for the user before opening it on channel qo62x57p7w

The orphan afterwards (lsof -p <pid>):

claude  81799  0u  unix 0x63187a5258fe2132  0t0  ->(none)
claude  81799  1u  unix 0x40571eb81add809   0t0  ->(none)
claude  81799  2u  unix 0x5a8b8b4b848e37c6  0t0  ->(none)

ps showed it with ppid 1, still the parent of the background task shells. Every "still open in another Claude process" line in my logs from Oct 13 and 14 comes 10–20 s after an extension host exit with code 5.

Steps to Reproduce

I reproduced this without VS Code by using a small Python process as the extension host. It starts the extension's bundled binary over unix socketpairs with the extension's flags and environment, then gets killed with kill -9 (script below). Same results on 2.1.296 and 2.1.299.

  1. Control: send "Reply with the single word ok", then kill -9 the parent. The CLI exits in ~1 s and its sessions/<pid>.json is removed.
  2. Send a prompt that runs sleep 1800 with run_in_background: true, then kill -9 the parent. After 60 s the CLI is still alive with ppid 1, stdio ->(none), and its registry entry is status: idle.
  3. Start a second CLI with --resume=<same id>. It starts without complaint. Both PIDs now have registry entries with the same sessionId. The transcript gets a task-notification with status: stopped for the sleep, while pgrep still shows it running.
  4. Send SIGTERM to the orphan. It exits in ~3 s and appends last-prompt (its own first prompt) and cost-state (its own total) after the second process's newer prompt, plus a second task-notification for the same task with status: killed.

In VS Code, steps 1–2 correspond to an extension host crash while a background task is running, and step 3 to clicking "open anyway" on the warning.

<details><summary>harness.py</summary>

"""Stand-in for the VS Code extension host: spawns the claude CLI over unix
socketpairs (like libuv stdio pipes), runs one turn, then stays alive as the
CLI's parent until it is killed.

usage: harness.py <name> <prompt> [resume-session-id]
"""
import json, os, socket, subprocess, sys, threading, time

BIN = os.environ.get("CLAUDE_BIN") or os.path.expanduser(
    "~/.vscode/extensions/anthropic.claude-code-2.1.299-darwin-arm64/resources/native-binary/claude")
HERE = os.path.dirname(os.path.abspath(__file__))
CWD = os.path.join(HERE, "cwd")
name, prompt = sys.argv[1], sys.argv[2]
resume = sys.argv[3] if len(sys.argv) > 3 else None
info_path = os.path.join(HERE, f"{name}.info.json")
os.makedirs(CWD, exist_ok=True)
if os.path.exists(info_path):
    os.remove(info_path)

if os.fork() > 0:
    # launcher: wait for the holder to report, then exit
    for _ in range(240):
        if os.path.exists(info_path):
            print(open(info_path).read())
            sys.exit(0)
        time.sleep(0.5)
    print("timeout waiting for holder"); sys.exit(1)

os.setsid()
devnull = os.open(os.devnull, os.O_RDWR)
for fd in (0, 1, 2):
    os.dup2(devnull, fd)
pairs = [socket.socketpair() for _ in range(3)]
args = [BIN, "--output-format", "stream-json", "--verbose", "--input-format", "stream-json",
        "--max-thinking-tokens=0", "--setting-sources=project,local",
        "--model", "claude-haiku-5-5", "--allowedTools", "Bash(sleep:*)",
        "--include-partial-messages", "--debug", "--debug-to-stderr", "--replay-user-messages"]
if resume:
    args.append(f"--resume={resume}")
env = dict(os.environ, CLAUDE_CODE_ENTRYPOINT="claude-vscode", CLAUDE_CODE_ENABLE_TASKS="0",
           CLAUDE_CODE_SDK_READS_SESSION_STATE="1", CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING="true",
           MCP_CONNECTION_NONBLOCKING="true")
p = subprocess.Popen(args, cwd=CWD, env=env, stdin=pairs[0][1].fileno(),
                     stdout=pairs[1][1].fileno(), stderr=pairs[2][1].fileno())
for a, b in pairs:
    b.close()
stdin, stdout, stderr = pairs[0][0], pairs[1][0].makefile("r"), pairs[2][0]

def drain_stderr():
    with open(os.path.join(HERE, f"{name}.stderr.log"), "ab") as f:
        while chunk := stderr.recv(65536):
            f.write(chunk); f.flush()
threading.Thread(target=drain_stderr, daemon=True).start()

msg = {"type": "user", "message": {"role": "user", "content": prompt}}
stdin.sendall((json.dumps(msg) + "\n").encode())
session_id, result = None, None
with open(os.path.join(HERE, f"{name}.stdout.jsonl"), "a") as log:
    for line in stdout:
        log.write(line); log.flush()
        o = json.loads(line)
        session_id = session_id or o.get("session_id")
        if o.get("type") == "result":
            result = o.get("result"); break

with open(info_path, "w") as f:
    json.dump({"holder_pid": os.getpid(), "cli_pid": p.pid, "session_id": session_id,
               "result": result}, f)

# keep reading stdout so the CLI never blocks while we are alive
with open(os.path.join(HERE, f"{name}.stdout.jsonl"), "a") as log:
    for line in stdout:
        log.write(line); log.flush()
while True:
    time.sleep(3600)

</details>

Claude Model

Haiku (for the repro); not model-specific.

Is this a regression?

I don't know

Claude Code Version

2.1.299 (anthropic.claude-code-2.1.299-darwin-arm64); first seen on 2.1.296

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal (VS Code extension panel)

Additional Information

Why the extension host crashes with exit code 5 is a separate question I have not pinned down.

Related: #83059 (duplicate --resume processes orphaning background work, closed as inactive), #101007 (same warning, different cause: a stale registry file whose PID was recycled), #92102 (CLI lifetime tied to the extension host).

View original on GitHub ↗