Summary
On Windows, a console window (a Windows Terminal window on Win11) pops up every time the browse server starts. Several console-subprocess spawns are missing windowsHide: true.
Environment
- Windows 11 (10.0.26200), gstack v1.58.5.0
- bun 1.3.14, node v22.23.1
Root cause
On Windows, spawning a console subprocess without windowsHide: true allocates a console window (windowsHide maps to the CREATE_NO_WINDOW creation flag; Node's default is false). Enumerating top-level windows (EnumWindows) during server start showed the popup owned by WindowsTerminal, running:
icacls.exe — ACL lockdown of state/log files at startup (file-permissions.ts, execFileSync)
tasklist.exe — PID liveness probe (error-handling.ts, via the Bun.spawnSync polyfill)
The Bun polyfill wrapper (bun-polyfill.cjs) also drops any windowsHide the caller passes, so per-call fixes wouldn't stick — best fixed centrally there. Also patched the detached server launcher (cli.ts) and the detached telemetry spawn (security.ts) defensively.
Fix
diff --git a/browse/src/bun-polyfill.cjs b/browse/src/bun-polyfill.cjs
index e0ada11..4307659 100644
--- a/browse/src/bun-polyfill.cjs
+++ b/browse/src/bun-polyfill.cjs
@@ -75,6 +75,7 @@ globalThis.Bun = {
timeout: options.timeout,
env: options.env,
cwd: options.cwd,
+ windowsHide: true, // don't pop a console window for CLI tools (icacls, tasklist, …) on Windows
});
return {
@@ -91,6 +92,7 @@ globalThis.Bun = {
stdio,
env: options.env,
cwd: options.cwd,
+ windowsHide: true, // don't pop a console window for CLI tools on Windows
});
return {
diff --git a/browse/src/cli.ts b/browse/src/cli.ts
index 59327b7..e4a9f91 100644
--- a/browse/src/cli.ts
+++ b/browse/src/cli.ts
@@ -323,7 +323,9 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
const launcherCode =
`const{spawn}=require('child_process');` +
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
- `{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
+ // windowsHide: a detached child otherwise gets its own console window on
+ // Windows (Node default windowsHide:false) — suppress that popup.
+ `{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
`${extraEnvStr})}).unref()`;
Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'] });
} else {
diff --git a/browse/src/file-permissions.ts b/browse/src/file-permissions.ts
index d3d404a..df204c7 100644
--- a/browse/src/file-permissions.ts
+++ b/browse/src/file-permissions.ts
@@ -71,7 +71,7 @@ export function restrictFilePermissions(filePath: string): void {
execFileSync(
'icacls',
[filePath, '/inheritance:r', '/grant:r', `${user}:(F)`],
- { stdio: 'ignore' },
+ { stdio: 'ignore', windowsHide: true },
);
} catch (err) {
warnIcaclsFailure(filePath, err);
@@ -101,7 +101,7 @@ export function restrictDirectoryPermissions(dirPath: string): void {
execFileSync(
'icacls',
[dirPath, '/inheritance:r', '/grant:r', `${user}:(OI)(CI)(F)`],
- { stdio: 'ignore' },
+ { stdio: 'ignore', windowsHide: true },
);
} catch (err) {
warnIcaclsFailure(dirPath, err);
diff --git a/browse/src/security.ts b/browse/src/security.ts
index d0e4039..fec8762 100644
--- a/browse/src/security.ts
+++ b/browse/src/security.ts
@@ -494,6 +494,7 @@ function reportAttemptTelemetry(record: AttemptRecord): void {
const child = spawn(result.cmd, result.cmdArgs, {
stdio: 'ignore',
detached: true,
+ windowsHide: true, // detached child would otherwise pop a console window on Windows
});
// unref so this subprocess doesn't hold the event loop open
child.unref();
Verification
Re-ran the EnumWindows diff around server start before/after — the icacls/tasklist Windows Terminal popups are gone. Windows-only; windowsHide is ignored on other platforms, so no behavior change elsewhere.
Note (separate, upstream)
There's a second popup source: Playwright's own browser spawn (chrome-headless-shell.exe) also lacks windowsHide in playwright-core's processLauncher.js. Filed separately upstream — not part of this change.
Summary
On Windows, a console window (a Windows Terminal window on Win11) pops up every time the
browseserver starts. Several console-subprocess spawns are missingwindowsHide: true.Environment
Root cause
On Windows, spawning a console subprocess without
windowsHide: trueallocates a console window (windowsHidemaps to theCREATE_NO_WINDOWcreation flag; Node's default isfalse). Enumerating top-level windows (EnumWindows) during server start showed the popup owned byWindowsTerminal, running:icacls.exe— ACL lockdown of state/log files at startup (file-permissions.ts,execFileSync)tasklist.exe— PID liveness probe (error-handling.ts, via theBun.spawnSyncpolyfill)The Bun polyfill wrapper (
bun-polyfill.cjs) also drops anywindowsHidethe caller passes, so per-call fixes wouldn't stick — best fixed centrally there. Also patched the detached server launcher (cli.ts) and the detached telemetry spawn (security.ts) defensively.Fix
Verification
Re-ran the
EnumWindowsdiff around server start before/after — theicacls/tasklistWindows Terminal popups are gone. Windows-only;windowsHideis ignored on other platforms, so no behavior change elsewhere.Note (separate, upstream)
There's a second popup source: Playwright's own browser spawn (
chrome-headless-shell.exe) also lackswindowsHideinplaywright-core'sprocessLauncher.js. Filed separately upstream — not part of this change.