anthropics / anthropics/claude-code
Windows: VS Code extension spawns 14 helper processes without windowsHide, each opening a focus-stealing console window
- 主要言語
- Python
- スター
- 145k
- フォーク
- 23.1k
- PR マージ指標
- PR 指標を取得中
説明
## Summary
On Windows, the Claude Code VS Code extension repeatedly opens console windows that take the foreground and eat keystrokes. It reads as random flashing during normal editing — never at a moment the user can associate with an action.
The cause is structural, not incidental. The VS Code extension host is a **GUI process with no attached console** (`GetConsoleWindow() == 0`). When it spawns a **console-subsystem** executable (`rg.exe`, `git.exe`, `where.exe`, `claude.exe`, `wsl.exe`) without `windowsHide: true`, Windows has nothing for the child to inherit and allocates a **brand-new visible console**. On Windows 11, whose default console host is Windows Terminal, that renders as a real Terminal window that takes the foreground.
The same calls under the terminal CLI inherit the terminal's existing console and are invisible. **This is structurally a VS Code extension defect**, and CLI behaviour is not evidence that a call site is safe.
I count **14 Windows-reachable `child_process` call sites in the shipped extension bundle that pass no `windowsHide`.** The main session spawn is correctly hidden, which is exactly why the symptom does not look like "Claude started something" — it is all helper code.
## Environment
| | |
|---|---|
| Extension | `anthropic.claude-code` **2.1.241** (`win32-x64`) |
| VS Code | **1.134.0** (x64) |
| OS | **Windows 11 Pro, build 10.0.26200** |
| Default terminal application | **Windows Terminal** (Windows 11 default) |
| Mode | Native panel (not `claudeCode.useTerminal`) |
## The mechanism, measured
Runtime probe, measured 2026-08-23 on Windows 11 Pro 26200. A parent process was created with `DETACHED_PROCESS` so that `GetConsoleWindow() == 0`, reproducing the extension host's state, and spawned a console-subsystem child twice:
```
NO windowsHide (flags = 0) childPid=66360 hwnd=0x3610D4 visible=True
windowsHide = true (CREATE_NO_WINDOW) childPid=78716 hwnd=0x0 visible=False
```
Node's `spawn` / `exec` / `execFile` default `windowsHide` to **`false`**. libuv maps `true` to `CREATE_NO_WINDOW` + `STARTF_USESHOWWINDOW` / `SW_HIDE`. There is no ambiguity here: with the flag the window does not exist, without it, it does.
### The Windows Terminal multiplier
Also measured 2026-08-23, same machine, same probe, only the default terminal application changed:
| Default terminal application | Window lifetime | Took foreground |
|---|---|---|
| **Windows Terminal** (Windows 11 default) | **2340 ms** | **8 / 8 samples** |
| Legacy Console Host (`conhost.exe`) | 30 ms | 0 / 3 samples |
This matters for triage. The defect is usually described as a "brief flash", which is what it is under the legacy console host. On a **default Windows 11 install** it is a two-second, foreground-stealing window per spawn. Keystrokes typed into it are lost, and with several spawns in flight it makes the editor unusable rather than merely annoying. #76423 raised the photosensitivity angle on the CLI side; the same applies here.
## Call sites
Static analysis of the shipped, minified `extension.js` from the 2.1.241 `win32-x64` VSIX (2,947,529 bytes as shipped) — **read from the bundle, not verified at runtime by symbol**. Method: every `.spawn|spawnSync|exec|execFile|execFileSync|execSync|fork(` where `` resolves to a `child_process` require, then checked for a `windowsHide` key in the options object. Offsets are **character** offsets into the UTF-8-decoded bundle and are a pointer to the source location, **not a patch target**. Symbol names are post-minification.
### Windows-reachable, missing `windowsHide`
| ~char | call | when it fires |
|---|---|---|
| 2806166 | `SM.execFile(rg, args, {cwd, maxBuffer:2e7, timeout:1e4})` | `@`-mention / fuzzy file search — **once per invocation, while the user types** |
| 2577367 | `eRe.spawn(e, t, {cwd, shell:!1})` — the `up()` helper | prompt-suggestion recon: **6–7 `git` processes per run, five of them concurrent** (`config user.email`, `status --porcelain`, `rev-parse`, `log`, `diff --stat`, `diff HEAD`) |
| 2731498 | `uM.execFile("git", ["check-ignore", fsPath], {cwd})` | per referenced file (memoised per `fsPath`) |
| 2601333 | `jL.spawn(claude, ["auth","status","--json"], {cwd, env, shell:!1})` | activation, and again on every config-epoch change |
| 2628517 | `jL.spawn(e, t, {cwd, shell:!1})` — the `execCommand` RPC | generic exec requested from the webview |
| 2600531 | `jL.spawn(claude, ["auth","logout"], {env, shell:!1})` | logout |
| 2582444 | `ZRe.execFile("git", ["remote","get-url","origin"], {cwd})` | teleport repository detection (cached per instance) |
| 2711734 | `bNe.execFileSync("wsl.exe", ["-e","wslpath","-w", t], …)` | **Windows-only by construction.** Path conversion for POSIX-shaped paths, reached from the diagnostics helper and the file-open guard — so WSL / remote workspaces hit it repeatedly |
| 2730648 | `uM.execFileSync("git", ["config","--global","core.excludesFile"], {encoding:"utf8", timeout:2000})` | once per session (memoised) |
| 2806047 | `SM.execFileSync(rg, ["--version"], {stdio:"ignore"})` | once per session (memoised ripgrep probe) |
| 1193133 | `EI.execFileSync("where", [name], {stdio:"ignore"})` for `rec`, then `arecord` | once per session, audio-backend probe |
| 1194396 | `EI.spawn(rec / arecord, …, {stdio:["ignore","pipe","ignore"]})` | voice input, when a `rec` / `arecord` backend was found |
| 609379 | `Dz.spawn(rg, ["-n","--no-heading","-e", …])` | bundled SDK built-in Grep tool |
| 1308126 | `gV.spawn(rg, ["-n","--no-heading","-e", …])` | bundled SDK built-in Grep tool (second copy of the SDK in the bundle) |
Two further omissions are **not** bugs: `Dz.spawn("/bin/bash", …)` at ~601959 and `gV.spawn("/bin/bash", …)` at ~1300704 are the SDK's POSIX shell session and are unreachable on Windows.
### Already correct — for contrast
This report is not "add the flag everywhere". Most of the bundle already gets this right, which is what makes the gaps look like oversights rather than policy:
- **The main session spawn is hidden.** `spawnLocalProcess` at ~char 2123183 passes `windowsHide:!0`. This is why the symptom reads as random flashing rather than as "Claude started" — every visible window is helper code.
- **execa's bundled defaults set `windowsHide:!0`** (~char 511878), so every execa / cross-spawn path in the bundle is fine.
- **The `pi()` `execFile` wrapper defaults to hidden**: `windowsHide: r.windowsHide ?? !0` (~char 1192209). The right pattern already exists in this codebase.
- `where.exe` binary resolution (~char 902762 and ~2164310), `git worktree list` (~char 2165118), and the MCP stdio transport (`windowsHide: process.platform === "win32"`, ~char 2704997) all set it.
### Deliberate `windowsHide: false` — please keep these
Three call sites set it to `false` on purpose, and all three are correct. Listing them so it is clear this report is not asking for a blanket rewrite:
1. **Extension, ~char 2828333** — `openURL()` launching `$BROWSER` via `pi(t, [e], {windowsHide:!1})` when `vscode.env.remoteName` is set. The point is to show a browser.
2. **CLI binary (`claude.exe` 2.1.241), byte offset 307825516** — the Claude-in-Chrome detached browser launch. Correct.
3. **CLI binary, byte offset 327324728** — the terminal launcher `L8y()`, reached from the deep-link handler whose entire job is to open a terminal window. Correct.
## No user-side workaround exists
Worth stating explicitly, because the usual triage response is a settings suggestion:
- **No settings key sets `windowsHide`.** The shipped `claude-code-settings.schema.json` (225 KB) has zero occurrences of `windowsHide`, `CREATE_NO_WINDOW`, `hideConsole` or `noWindow`; so does the extension's `package.json` `contributes.configuration`.
- **No environment variable sets it** in either the extension bundle or the CLI binary.
- **`processWrapper` / `CLAUDE_CODE_PROCESS_WRAPPER` does not cover these.** Its own schema description scopes it to "the background-agent supervisor, the sessions and workers it hosts, and the other covered background processes" — an argv prefix for the session / daemon / worker chain, not the extension's helper spawns.
- `"claudeCode.useTerminal": true` avoids most of it only by not running the native panel at all, which is not a fix.
- Switching the default terminal application to the legacy Console Host reduces a 2340 ms window to a 30 ms flash. That is a mitigation via a Windows-wide setting most users will not want to change, and it does not remove the windows.
## Suggested fix
In rough order of durability:
1. **Add `windowsHide: true` to the 14 call sites above.** Mechanical, and a no-op off Windows.
2. **Route helper spawns through the existing `pi()` wrapper** (~char 1192209), which already defaults `windowsHide` to `true` and lets a caller opt out explicitly. The correct pattern is already in the codebase; the gaps are the calls that bypass it.
3. **Add a lint rule / CI check** banning direct `child_process` calls in extension code without an explicit `windowsHide`, with an allowlist for the deliberate `false` sites. Without this, the next helper added reintroduces the bug — the history in this repo (#15572, #16880, #24708, #44039, #54683, #61005, #63623, #64163, #72331, #82975 all closed, plus #14828 open since 2025-12) shows it is reintroduced repeatedly, one call site at a time.
4. Optionally, note in the contributing docs that the extension host has no console, so `windowsHide` is not optional there the way it effectively is under the CLI.
## Related
- **#89071** — the highest-frequency instance of this bug, filed separately: the `@`-mention ripgrep spawn at ~char 2806166, which fires on keystrokes. If only one call site gets fixed, that is the one.
- **#87860** — same `@`-mention call site, different defect: unbounded ripgrep processes, no debounce, no cache, no cancellation (605 processes in ~2 min, macOS). On Windows the two compound — every one of those processes is also a console window.
- Prior CLI-side reports of the same class of bug: #14828, #58606, #66540, #70200, #73709, #73901, #75404, #76423, #78189, #79219, #80925, #83868, #84675, #86192, #87394. None of them covers the VS Code extension's own helper spawns, which is what this report enumerates.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
調査の方向性
Start by locating the source behind the shipped extension.js and compare its direct child_process calls with the existing pi() wrapper, which already defaults windowsHide to true. Check the 14 Windows-reachable omissions listed in the issue, preserve the deliberate false sites, and use the Windows reproduction described here to confirm helper processes no longer create visible console windows.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- git, javascript, node.js, vscode
- 領域
- desktop, devtools, operating-systems
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 活発
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 55/100