From c4d91507dd5c5bd3604029874424579af359b328 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 17 Aug 2026 10:24:27 -0700 Subject: [PATCH] chore(browse): explicit windowsHide on every Bun.spawn site + census tripwire (#2575 residual) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bun.spawn sites were structurally outside the windowsHide census (it swept child_process bindings only). The runtime was already safe — native Bun hides consoles by default and bun-polyfill.cjs defaults windowsHide !== false since #2523/#2539 — but implicit defaults are exactly what regress silently. Every Bun.spawn/spawnSync in browse/src now carries the explicit flag (harmless on unix-only sites like Xvfb/xattr/open), and a second SWEEP in windows-spawn-hide.test.ts fails CI on any new flagless Bun.spawn site. Co-Authored-By: Claude Fable 5 --- browse/src/browser-skill-commands.ts | 1 + browse/src/cli.ts | 2 ++ browse/src/config.ts | 3 +++ browse/src/cookie-import-browser.ts | 6 +++--- browse/src/find-browse.ts | 1 + browse/src/terminal-agent.ts | 1 + browse/src/write-commands.ts | 2 +- browse/src/xprotect-heal.ts | 1 + browse/src/xvfb.ts | 3 +++ browse/test/windows-spawn-hide.test.ts | 27 ++++++++++++++++++++++++++ 10 files changed, 43 insertions(+), 4 deletions(-) diff --git a/browse/src/browser-skill-commands.ts b/browse/src/browser-skill-commands.ts index 5174e76d3..2427d5e2a 100644 --- a/browse/src/browser-skill-commands.ts +++ b/browse/src/browser-skill-commands.ts @@ -256,6 +256,7 @@ async function runToFiles(cmd: string[], opts: RunToFilesOptions): Promise { // macOS may show an Allow/Deny dialog that blocks until the user responds. const proc = Bun.spawn( ['security', 'find-generic-password', '-s', service, '-w'], - { stdout: 'pipe', stderr: 'pipe' }, + { stdout: 'pipe', stderr: 'pipe', windowsHide: true }, ); const timeout = new Promise((_, reject) => @@ -639,7 +639,7 @@ async function getLinuxSecretPassword(browser: BrowserInfo): Promise { try { - const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe' }); + const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe', windowsHide: true }); const timeout = new Promise((_, reject) => setTimeout(() => { proc.kill(); @@ -870,7 +870,7 @@ export async function importCookiesViaCdp( '--disable-extensions', '--disable-sync', '--no-default-browser-check', - ], { stdout: 'pipe', stderr: 'pipe' }); + ], { stdout: 'pipe', stderr: 'pipe', windowsHide: true }); // Wait for Chrome to start, then find a page target's WebSocket URL. // Network.getAllCookies is only available on page targets, not browser. diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts index ab9f6a54d..a2c3e81d2 100644 --- a/browse/src/find-browse.ts +++ b/browse/src/find-browse.ts @@ -14,6 +14,7 @@ import { homedir } from 'os'; function getGitRoot(): string | null { try { const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], { + windowsHide: true, stdout: 'pipe', stderr: 'pipe', }); diff --git a/browse/src/terminal-agent.ts b/browse/src/terminal-agent.ts index 21ce5581a..f75f6a28d 100644 --- a/browse/src/terminal-agent.ts +++ b/browse/src/terminal-agent.ts @@ -347,6 +347,7 @@ function spawnClaude(cols: number, rows: number, onData: (chunk: Buffer) => void const tabHint = buildTabAwarenessHint(stateDir); const proc = (Bun as any).spawn([claudePath, '--append-system-prompt', tabHint], { + windowsHide: true, terminal: { rows, cols, diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 50efb7ac8..6382d8b03 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -754,7 +754,7 @@ export async function handleWriteCommand( const code = generatePickerCode(); const pickerUrl = `http://127.0.0.1:${port}/cookie-picker?code=${code}`; try { - Bun.spawn(['open', pickerUrl], { stdout: 'ignore', stderr: 'ignore' }); + Bun.spawn(['open', pickerUrl], { stdout: 'ignore', stderr: 'ignore', windowsHide: true }); } catch (err: any) { // open may fail on non-macOS or if 'open' binary is missing — URL is in the message below if (err?.code !== 'ENOENT' && !err?.message?.includes('spawn')) throw err; diff --git a/browse/src/xprotect-heal.ts b/browse/src/xprotect-heal.ts index 6c366975a..e4d9e65f3 100644 --- a/browse/src/xprotect-heal.ts +++ b/browse/src/xprotect-heal.ts @@ -167,6 +167,7 @@ export function findGstackInstallRoot( function defaultRunXattr(target: string): number | null { const res = Bun.spawnSync(['xattr', '-dr', 'com.apple.quarantine', target], { + windowsHide: true, stdout: 'pipe', stderr: 'pipe', timeout: 10_000, diff --git a/browse/src/xvfb.ts b/browse/src/xvfb.ts index 17269c78d..cf90b9bf6 100644 --- a/browse/src/xvfb.ts +++ b/browse/src/xvfb.ts @@ -63,6 +63,7 @@ export function isDisplayFree(displayNum: number): boolean { // the X socket/lock files, the same signal X servers themselves use. try { const result = Bun.spawnSync(['xdpyinfo', '-display', `:${displayNum}`], { + windowsHide: true, stdout: 'ignore', stderr: 'ignore', timeout: 2000, }); return result.exitCode !== 0; @@ -95,6 +96,7 @@ export function pickFreeDisplay( export function readPidStartTime(pid: number): string { if (!isProcessAlive(pid)) return ''; const result = Bun.spawnSync(['ps', '-p', String(pid), '-o', 'lstart='], { + windowsHide: true, stdout: 'pipe', stderr: 'pipe', timeout: 2000, }); if (result.exitCode !== 0) return ''; @@ -159,6 +161,7 @@ export async function spawnXvfb(displayNum: number): Promise { // Spawn detached: Xvfb's lifetime is tied to whether we've explicitly // killed it via the handle's close() method, not to the parent process. const proc = Bun.spawn(['Xvfb', display, '-screen', '0', '1920x1080x24', '-ac'], { + windowsHide: true, stdio: ['ignore', 'ignore', 'ignore'], }); proc.unref(); diff --git a/browse/test/windows-spawn-hide.test.ts b/browse/test/windows-spawn-hide.test.ts index a9949a566..512279446 100644 --- a/browse/test/windows-spawn-hide.test.ts +++ b/browse/test/windows-spawn-hide.test.ts @@ -127,4 +127,31 @@ describe('windowsHide on Windows-reachable spawns (#1835)', () => { } expect(offenders).toEqual([]); }); + + test('SWEEP: every Bun.spawn call in src/ passes windowsHide (#2575 residual)', () => { + // Bun.spawn sites are structurally outside the child_process sweep above. + // Native Bun hides consoles by default and the Node polyfill + // (bun-polyfill.cjs) defaults windowsHide !== false since #2523/#2539 — + // this census exists so an explicit flag documents the intent at every + // site AND catches a regression if either default ever flips. Exemptions + // carry reasons, same contract as the child_process sweep. + const EXEMPT: Array<{ file: string; needle: string; reason: string }> = []; + + const srcDir = path.join(import.meta.dir, '../src'); + const offenders: string[] = []; + for (const file of fs.readdirSync(srcDir).filter((f) => f.endsWith('.ts'))) { + const raw = fs.readFileSync(path.join(srcDir, file), 'utf-8'); + const code = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); + const re = /(?:\(Bun as any\)|Bun)\.spawn(?:Sync)?\(/g; + for (const m of code.matchAll(re)) { + const slice = code.slice(m.index!, m.index! + 900); + const exempt = EXEMPT.some((e) => e.file === file && slice.includes(e.needle)); + if (exempt) continue; + if (!/windowsHide:\s*true/.test(slice)) { + offenders.push(`${file}: ${slice.split('\n')[0].slice(0, 100)}`); + } + } + } + expect(offenders).toEqual([]); + }); });