Skip to content

Windows: browse cold-start flashes focus-stealing console windows (Bun.spawnSync ignores windowsHide) #1784

Closed

Description

@kaiwulff

Windows: browse cold-start flashes focus-stealing console windows (Bun.spawnSync ignores windowsHide)

Summary

On Windows 11, every cold start of browse (i.e. when no healthy daemon is already running) briefly flashes 2+ focus-stealing console windows on the interactive desktop. They steal foreground focus and interrupt typing in other apps. The cause is several console-subsystem child processes spawned without an effective windowsHide.

Root cause (verified with an elevated Win32_ProcessStartTrace)

Tracing process creation during a cold browse goto shows the flashing conhost.exe windows are parented to:

spawned process spawned by call site
node.exe (the detach launcher) browse.exe browse/src/cli.ts startServer() — Bun.spawnSync(['node','-e',launcherCode], …)
git.exe (rev-parse --show-toplevel) browse.exe browse/src/config.ts getGitRoot() and browse/src/find-browse.ts — Bun.spawnSync(['git', …])
icacls.exe ×2 daemon node browse/src/file-permissions.ts restrictFilePermissions / restrictDirectoryPermissions — execFileSync('icacls', …, { stdio:'ignore' }) (no windowsHide)

The key finding

Bun.spawnSync does not honor the windowsHide option on Windows (tested on bun 1.3.11). Adding windowsHide: true to a Bun.spawnSync(...) call has no effect — the child still allocates a console window.

Node's child_process (spawn / spawnSync / execFileSync) does honor windowsHide. (Confirmed in the same trace: the detached daemon node, spawned via Node's child_process.spawn({ detached:true, windowsHide:true }) inside launcherCode, did not get a console window — only the Bun.spawnSync-spawned launcher and git did.)

So the fix is: for Windows console spawns, use Node's child_process with windowsHide:true rather than Bun.spawnSync, and add windowsHide:true to the execFileSync('icacls', …) calls.

Suggested fix

# browse/src/cli.ts
-import { spawn as nodeSpawn } from 'child_process';
+import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'child_process';
@@ startServer() (Windows branch)
-      `{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
+      `{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
       `${extraEnvStr})}).unref()`;
-    Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'] });
+    // Bun.spawnSync ignores windowsHide on Windows -> the launcher node flashes a
+    // console. Node's spawnSync honors it.
+    nodeSpawnSync('node', ['-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true });

# browse/src/config.ts  (and browse/src/find-browse.ts likewise)
+import { execFileSync } from 'child_process';
-    const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], { stdout:'pipe', stderr:'pipe', timeout:2_000 });
-    if (proc.exitCode !== 0) return null;
-    return proc.stdout.toString().trim() || null;
+    // Node's execFileSync honors windowsHide; Bun.spawnSync does not.
+    const out = execFileSync('git', ['rev-parse', '--show-toplevel'],
+      { encoding:'utf-8', timeout:2_000, windowsHide:true, stdio:['ignore','pipe','ignore'] });
+    return out.trim() || null;

# browse/src/file-permissions.ts  (both execFileSync('icacls', …) calls)
-        { stdio: 'ignore' },
+        { stdio: 'ignore', windowsHide: true },

The detached daemon spawn just needs the windowsHide:true added to its options object (Node honors it).

Repro

  1. Windows 11, no browse daemon running.
  2. browse goto https://example.com.
  3. Observe 2+ console windows flash and steal focus. An elevated Win32_ProcessStartTrace shows the conhost.exe parents are node (launcher), git, and icacls.

Environment

  • Windows 11, gstack @ 920a13a, bun 1.3.11, Node from C:\Program Files\nodejs.
  • Default headless path. (Separately, chrome-headless-shell.exe is a console-subsystem binary; launching full chromium with --headless=new avoids that one too, but it was not the cause of these particular flickers.)

Activity

  1. mvanhorn commented on Jun 4, 2026

    @mvanhorn
    Contributor

    Implemented in #1864.

  2. time-attack commented on Jul 15, 2026

    @time-attack
    Contributor

    @kaiwulff Browse startup still has child-process paths that can show focus-stealing consoles, including find-browse.

  3. capy-ai commented on Oct 2, 2026

    @capy-ai
    Contributor

    Believed fixed on main (v1.91.13.0): every spawn site in browse/src now passes windowsHide (enforced by the browse/test/windows-spawn-hide.test.ts sweep, passing on the Windows CI lane), the Node polyfill forwards it, and the terminal-agent watchdog fix with browse/test/terminal-agent-watchdog.test.ts shipped in v1.67.0.0. We can't prove console suppression or agent survival on a real Windows desktop from CI. Could you confirm on v1.91.13.0 or later? We'll close once you do, or reopen the investigation if it still happens.

  4. kaiwulff commented on Oct 3, 2026

    @kaiwulff
    Author

    Confirmed fixed on a real Windows 11 desktop. Clean, unpatched build of main @ 74512c2 (v1.91.15.0), bun 1.3.11, run from Claude Code's Bash tool. A watcher polling every 2 ms logged every new visible window and every foreground change. Positive control: a detached node spawning cmd without windowsHide → Windows Terminal window plus focus steal. Same spawn with windowsHide → nothing. Three browse goto cold starts and two browse stop runs → zero console windows, no focus steal. Fine to close. Thanks!

  5. capy-ai commented on Oct 3, 2026

    @capy-ai
    Contributor

    Every browse spawn site now passes windowsHide (enforced by browse/test/windows-spawn-hide.test.ts), and the reporter confirmed zero console windows and no focus steal on a real Windows 11 desktop with v1.91.15.0.

    Fixed in v1.91.13.0 (#3013).
    If you still see this on the latest gstack, please reopen with details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions