Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
Observed behavior and impact
Both stale extension hosts logged ReferenceError: Cannot access 'g' before initialization in Cursor’s file-search cleanup during process.exit(), yet remained alive. Their individual process footprints were approximately 2,642 MiB and 2,765 MiB. One had remained since the previous day.
After verifying the current windows and their extension hosts, I terminated only the two stale hosts and six dedicated child processes. All eight exited with SIGTERM; no SIGKILL was necessary. The two current windows and their extension hosts remained alive. Aggregate Cursor process footprint decreased from approximately 13.5 GiB to 7.3 GiB. Footprint includes compressed/swapped memory; this is not a claim that 6.2 GiB of physical RAM was immediately freed, or that the cleanup callbacks themselves retained that entire amount.
Concrete failure path in the installed build
In out/vs/workbench/api/node/extensionHostProcess.js, minified line 173, cmdTraversal registers a cleanup callback in the global Sy Set before initializing g:
const cleanup = () => child && child.kill();
pendingCleanups.add(cleanup);
const search = startRipgrepSearch(); // synchronous spawn failure is possible
const child = search.cmd;
These are descriptive names for the installed minified code. The actual startup helper is EDe; it checks the search directory and calls spawn for ripgrep. The actual child-process variable is g.
If startup throws synchronously, the callback remains registered while g remains uninitialized. The global exit listener iterates Sy. Invoking that callback then throws the ReferenceError from the temporal dead zone. That exception propagates through process.exit() and can interrupt termination. Repeated failed starts also accumulate cleanup callbacks.
Real log evidence, with local paths removed
One host logged:
2026-09-17 14:11:02.257 [info] Extension host ... exiting with code 0
2026-09-17 14:11:03.260 [error] ReferenceError: Cannot access 'g' before initialization
The other host logged the same exception on 2026-09-16 at 17:23:11.187 and was still alive the following day. Relevant stack frames:
at l (extensionHostProcess.js:173:3334)
at extensionHostProcess.js:173:1328
at Set.forEach (<anonymous>)
at process.<anonymous> (extensionHostProcess.js:173:1317)
at process.emit (node:events:521:24)
at SignalExit.processEmit_fn (zenclabs.previewjs-1.30.1/dist/index.js:1690:20)
at __privateGet.emit (zenclabs.previewjs-1.30.1/dist/index.js:1642:75)
at process.exit (node:internal/process/per_thread:236:15)
at Object.exit (extensionHostProcess.js:728:6864)
at extensionHostProcess.js:152:18591
Both hosts’ accumulated logs also contained many spawn EBADF errors. The initial cause of EBADF remains unknown. Preview.js appears as an event wrapper in the real stack; the isolated reproduction below requires no extensions, so its presence alone does not establish blame.
Steps to Reproduce
Isolated reproduction using the installed method
Run this in a disposable Node process, passing the installed extensionHostProcess.js path as its first argument. It extracts the exact method and stubs only its startup helper to throw EBADF. It does not run a real search or touch a live Cursor process.
const fs = require('node:fs');
const source = fs.readFileSync(process.argv[2], 'utf8');
const start = source.indexOf('cmdTraversal(e,n,t,a,s){');
const end = source.indexOf('spawnFindCmd(e){', start);
if (start < 0 || end < 0) throw new Error('Method not found');
const pending = new Set();
const failSpawn = () => {
throw Object.assign(new Error('spawn EBADF'), { code: 'EBADF' });
};
const host = new Function('Sy', 'ht', 'EDe',
'return {' + source.slice(start, end) + '}')(pending, false, failSpawn);
host.config = {};
host.folderExcludePatterns = new Map([['/diagnostic-only', { expression: {} }]]);
host.initDirectoryTree = () => ({});
for (let i = 0; i < 10; i++) {
try {
host.cmdTraversal({ folder: { fsPath: '/diagnostic-only' } },
undefined, undefined, () => {}, () => {});
} catch (e) { if (e.code !== 'EBADF') throw e; }
}
const result = { retainedCleanupCallbacks: pending.size };
const onExit = () => pending.forEach(fn => fn());
process.on('exit', onExit);
try { process.exit(0); }
catch (e) {
result.exitException = e.name + ': ' + e.message;
result.continuedAfterExitAttempt = true;
}
process.removeListener('exit', onExit);
fs.writeSync(1, JSON.stringify(result, null, 2) + '\n');
process.exit(0);
Observed result with the installed method:
{
"retainedCleanupCallbacks": 10,
"exitException": "ReferenceError: Cannot access 'g' before initialization",
"continuedAfterExitAttempt": true
}
This proves the cleanup/exit failure path, not the original trigger for EBADF or a complete UI-level reproduction.
Expected Behavior
Search startup failure should unregister cleanup and leave no closure referring to an uninitialized binding. Initialize the child-process binding before registering cleanup, remove the callback on synchronous startup failure, and isolate exceptions from individual cleanup callbacks so that one failed search cannot block extension-host termination.
Operating System
MacOS
Version Information
Cursor 3.20.21
Commit: f09fca384ceca23f7bf21f9c23655b162641d740
macOS 26.5.2 (25F84)
Apple M1 Pro / arm64, 16 GiB RAM
Two project windows open; two older extension hosts remained after their windows were gone.
Additional Information
Possibly related: Xtension-host processes orphaned after window close/reuse — SIGTERM ignored, requires SIGKILL
Unlike that report, SIGTERM successfully stopped the stale processes in this case.
The underlying trigger for spawn EBADF is unknown; the supplied reproduction isolates the cleanup and exit failure.
Does this stop you from using Cursor
No - Cursor works, but with this issue