chore(browse): explicit windowsHide on every Bun.spawn site + census tripwire (#2575 residual)

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 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-17 10:24:27 -07:00
co-authored by Claude Fable 5
parent ddeeb18edb
commit c4d91507dd
10 changed files with 43 additions and 4 deletions
+1
View File
@@ -256,6 +256,7 @@ async function runToFiles(cmd: string[], opts: RunToFilesOptions): Promise<RunTo
// here and closing them after exit instead put us in Bun's fd bookkeeping,
// which surfaced as a stray EBADF from epoll_ctl on a later spawn.
const proc = Bun.spawn(cmd, {
windowsHide: true,
cwd: opts.cwd,
env: opts.env as any,
stdout: Bun.file(outPath) as any,
+2
View File
@@ -212,6 +212,7 @@ function cleanupLegacyState(): void {
if (data.pid && isProcessAlive(data.pid)) {
// Verify this is actually a browse server before killing
const check = Bun.spawnSync(['ps', '-p', String(data.pid), '-o', 'command='], {
windowsHide: true,
stdout: 'pipe', stderr: 'pipe', timeout: 2000,
});
const cmd = check.stdout.toString().trim();
@@ -1645,6 +1646,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
// (the user asked to SEE the shared browser), and connect's #2219 guard
// would otherwise refuse to replace the healthy headless daemon.
const connectProc = Bun.spawn([browseBin, 'connect', '--force-restart'], {
windowsHide: true,
cwd: process.cwd(),
stdio: ['ignore', 'inherit', 'inherit'],
// Disable parent-PID monitoring: pair-agent needs the server to outlive
+3
View File
@@ -32,6 +32,7 @@ export interface BrowseConfig {
export function getGitRoot(): string | null {
try {
const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], {
windowsHide: true,
stdout: 'pipe',
stderr: 'pipe',
// Raised from 2s: under heavy machine load `git rev-parse` routinely
@@ -86,6 +87,7 @@ export function resolveConfig(
function isIgnoredByGit(projectDir: string, relPath: string): boolean {
try {
const proc = Bun.spawnSync(['git', 'check-ignore', '-q', '--', relPath], {
windowsHide: true,
cwd: projectDir, stdout: 'pipe', stderr: 'pipe',
timeout: 2_000,
});
@@ -160,6 +162,7 @@ export function ensureStateDir(config: BrowseConfig): void {
export function getRemoteSlug(): string {
try {
const proc = Bun.spawnSync(['git', 'remote', 'get-url', 'origin'], {
windowsHide: true,
stdout: 'pipe',
stderr: 'pipe',
timeout: 2_000,
+3 -3
View File
@@ -564,7 +564,7 @@ async function getMacKeychainPassword(service: string): Promise<string> {
// 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<never>((_, reject) =>
@@ -639,7 +639,7 @@ async function getLinuxSecretPassword(browser: BrowserInfo): Promise<string | nu
async function runPasswordLookup(cmd: string[], timeoutMs: number): Promise<string | null> {
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<never>((_, 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.
+1
View File
@@ -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',
});
+1
View File
@@ -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,
+1 -1
View File
@@ -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;
+1
View File
@@ -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,
+3
View File
@@ -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<XvfbHandle> {
// 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();
+27
View File
@@ -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([]);
});
});