@-mention file completion spawns unbounded ripgrep processes (605 procs in ~2 min, 204 zombies, extension host crash)
Summary
The @-mention file completion in the VS Code extension spawns one full-repository ripgrep process per invocation, with no caching, no debouncing, no in-flight cancellation, and no concurrency limit. On a moderately large repository this saturates disk I/O and process scheduling, and takes down the extension host.
Observed on a single workspace: ~605 child processes of the extension host within ~2 minutes, ~603 of them file-scanning processes, 204 of which had become zombies. The extension host crashed and restarted shortly after.
Environment
| | |
|---|---|
| Extension | anthropic.claude-code 2.1.235 (darwin-arm64) |
| VS Code | 1.133.0 |
| OS | macOS 26.6 (Apple Silicon) |
| ripgrep | 14.1.1 (from PATH) |
| Workspace | 3,058 tracked files, 129 MB .git |
Root cause
From the shipped (minified) extension.js. Symbol names are post-minification.
The @-mention completion provider — a method on the webview host class — calls a helper that builds ripgrep arguments:
async function TLe(e, t, r) {
let n = ["--files", "--follow", "--hidden"];
if (!(Bn("respectGitIgnore") ?? !0)) n.push("--no-ignore-vcs");
// ...builds `--glob !<pattern>` entries, but ONLY from
// workspace config `search.exclude` and `files.exclude`
let o = await Apr(n, t);
// ...fuzzy-matches the full file list against the query
}
and that helper spawns ripgrep directly:
function Apr(e, t) {
return new Promise((r, n) => {
JN.execFile(Tpr(), e, { cwd: t, maxBuffer: Cpr, timeout: kpr }, (i, o) => { /* ... */ })
})
}
Four independent problems compound here:
- No caching. Every invocation re-runs a complete
rg --filesover the workspace. The previous result is never reused, even though the file list is nearly static between keystrokes. - No debouncing and no in-flight cancellation. The completion provider is driven by typing, so each additional character starts another full scan while the previous ones are still running. Nothing aborts the superseded processes.
- No concurrency limit.
execFileis called directly — there is no queue, semaphore, or in-flight de-duplication. Process count grows linearly with typing speed and inversely with scan latency. - Zombie accumulation. With 204 of ~603 processes in a zombie state, reaping is not keeping up with spawning. The
execFiletimeout cannot help a process already blocked in uninterruptible disk I/O.
Aggravating factors
--hiddenwalks.git/. On this workspace that is 129 MB of loose objects and packs, scanned in full on every keystroke..gitis almost never a useful@-mention target.- Exclusions come only from
search.exclude/files.exclude. Both are unset by default, so out of the box the glob exclusion list is empty and every process scans the entire tree. --followon symlink-heavy trees. WithrespectGitIgnoredisabled, following symlinks through a pnpm-stylenode_modules(a symlink farm into a content-addressed store) means the same directories are traversed repeatedly.
Suggested fixes
Roughly in order of impact:
- Cancel in-flight scans when a new query arrives. Keep the child process handle and
kill()it — the result of a superseded keystroke is always discarded anyway. - Cache the file list and invalidate it with a filesystem watcher, rather than re-scanning per keystroke. The fuzzy match can then run in-process against the cached list.
- Add a concurrency cap / in-flight de-duplication, so a pathological case degrades into queuing rather than fork-bombing the machine.
- Debounce the completion provider (even 100–150 ms would collapse most of the storm).
- Exclude
.gitby default, independently ofsearch.exclude. Consider dropping--hidden, or pairing it with--glob '!.git'. - Consider preferring the built-in
vscode.workspace.findFilesAPI, which the code already uses as a fallback when ripgrep throws — it is served by the editor's own search service, which is throttled and does not spawn per-query processes.
Workaround for others hitting this
Populating search.exclude is the only user-side lever, since it is the sole source of --glob ! entries:
"search.exclude": {
"**/node_modules": true,
"**/.git": true,
"**/dist": true,
"**/out": true
}
This reduces the work each process does, but not the number of processes — the unbounded spawning is not addressable from configuration. There is no setting that disables @-mention file completion; the only way to avoid the code path entirely is "claudeCode.useTerminal": true, since the scan hangs off the webview host class and terminal mode never constructs it.