@-mention file completion spawns unbounded ripgrep processes (605 procs in ~2 min, 204 zombies, extension host crash)

Status Open
Reported on v2.1.235
Maintainer reply None cached
Activity 0 comments · opened Aug 19, 2026

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:

  1. No caching. Every invocation re-runs a complete rg --files over the workspace. The previous result is never reused, even though the file list is nearly static between keystrokes.
  2. 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.
  3. No concurrency limit. execFile is called directly — there is no queue, semaphore, or in-flight de-duplication. Process count grows linearly with typing speed and inversely with scan latency.
  4. Zombie accumulation. With 204 of ~603 processes in a zombie state, reaping is not keeping up with spawning. The execFile timeout cannot help a process already blocked in uninterruptible disk I/O.

Aggravating factors

  • --hidden walks .git/. On this workspace that is 129 MB of loose objects and packs, scanned in full on every keystroke. .git is 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.
  • --follow on symlink-heavy trees. With respectGitIgnore disabled, following symlinks through a pnpm-style node_modules (a symlink farm into a content-addressed store) means the same directories are traversed repeatedly.

Suggested fixes

Roughly in order of impact:

  1. 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.
  2. 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.
  3. Add a concurrency cap / in-flight de-duplication, so a pathological case degrades into queuing rather than fork-bombing the machine.
  4. Debounce the completion provider (even 100–150 ms would collapse most of the storm).
  5. Exclude .git by default, independently of search.exclude. Consider dropping --hidden, or pairing it with --glob '!.git'.
  6. Consider preferring the built-in vscode.workspace.findFiles API, 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.

View original on GitHub ↗