From 4633d41d9ee07105f8d93d7a07486c445c89023b Mon Sep 17 00:00:00 2001 From: June Kim Date: Fri, 20 Mar 2026 23:16:31 -0700 Subject: [PATCH 1/3] fix: use synchronous stderr write before exit to prevent silent failures (#16363) When SEATBELT_PROFILE points to a missing sandbox profile, the CLI exits silently because writeToStderr() is buffered and process.exit() terminates before the buffer flushes. Add writeFileSync(2, message) to guarantee the error is written to stderr before the process exits. --- packages/cli/src/utils/relaunch.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/relaunch.ts b/packages/cli/src/utils/relaunch.ts index 7e287e4565d..4b434a6e430 100644 --- a/packages/cli/src/utils/relaunch.ts +++ b/packages/cli/src/utils/relaunch.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { writeFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { RELAUNCH_EXIT_CODE } from './processUtils.js'; import { @@ -23,9 +24,12 @@ export async function relaunchOnExitCode(runner: () => Promise) { process.stdin.resume(); const errorMessage = error instanceof Error ? (error.stack ?? error.message) : String(error); - writeToStderr( - `Fatal error: Failed to relaunch the CLI process.\n${errorMessage}\n`, - ); + const message = `Fatal error: Failed to relaunch the CLI process.\n${errorMessage}\n`; + // Use synchronous write to fd 2 (stderr) to guarantee the message is + // visible before process.exit() terminates. The buffered writeToStderr() + // can lose output on macOS when stderr is piped. + writeFileSync(2, message); + writeToStderr(message); process.exit(1); } } From ddc70413da91af9da208c6c2a63b128ce18dc2c9 Mon Sep 17 00:00:00 2001 From: June Kim Date: Mon, 23 Mar 2026 22:37:01 -0700 Subject: [PATCH 2/3] fix: remove redundant async writeToStderr before exit Address review feedback: the synchronous writeFileSync(2, message) already guarantees the error is visible before process.exit(). The async writeToStderr call was duplicating the message. Remove it and its unused import. --- packages/cli/src/utils/relaunch.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/cli/src/utils/relaunch.ts b/packages/cli/src/utils/relaunch.ts index 4b434a6e430..2b9dd668f2b 100644 --- a/packages/cli/src/utils/relaunch.ts +++ b/packages/cli/src/utils/relaunch.ts @@ -7,10 +7,7 @@ import { writeFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { RELAUNCH_EXIT_CODE } from './processUtils.js'; -import { - writeToStderr, - type AdminControlsSettings, -} from '@google/gemini-cli-core'; +import { type AdminControlsSettings } from '@google/gemini-cli-core'; export async function relaunchOnExitCode(runner: () => Promise) { while (true) { @@ -29,7 +26,6 @@ export async function relaunchOnExitCode(runner: () => Promise) { // visible before process.exit() terminates. The buffered writeToStderr() // can lose output on macOS when stderr is piped. writeFileSync(2, message); - writeToStderr(message); process.exit(1); } } From 44633b782f46bd3e4762a444f85f9a0e69325eca Mon Sep 17 00:00:00 2001 From: June Kim Date: Sun, 29 Mar 2026 19:40:52 -0700 Subject: [PATCH 3/3] test: update relaunch tests to assert on writeFileSync instead of writeToStderr The implementation now uses synchronous writeFileSync(2, message) instead of the async writeToStderr. Update test mocks accordingly so the error- handling assertion matches the actual code path. --- packages/cli/src/utils/relaunch.test.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/utils/relaunch.test.ts b/packages/cli/src/utils/relaunch.test.ts index 255671e27f2..fb60e127375 100644 --- a/packages/cli/src/utils/relaunch.test.ts +++ b/packages/cli/src/utils/relaunch.test.ts @@ -16,20 +16,18 @@ import { import { EventEmitter } from 'node:events'; import { RELAUNCH_EXIT_CODE } from './processUtils.js'; import { spawn, type ChildProcess } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; -const mocks = vi.hoisted(() => ({ - writeToStderr: vi.fn(), -})); - -vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - writeToStderr: mocks.writeToStderr, + writeFileSync: vi.fn(), }; }); +const mockedWriteFileSync = vi.mocked(writeFileSync); + vi.mock('node:child_process', async (importOriginal) => { const actual = await importOriginal(); return { @@ -55,7 +53,7 @@ describe('relaunchOnExitCode', () => { .spyOn(process.stdin, 'resume') .mockImplementation(() => process.stdin); vi.clearAllMocks(); - mocks.writeToStderr.mockClear(); + mockedWriteFileSync.mockClear(); }); afterEach(() => { @@ -100,7 +98,8 @@ describe('relaunchOnExitCode', () => { ); expect(runner).toHaveBeenCalledTimes(1); - expect(mocks.writeToStderr).toHaveBeenCalledWith( + expect(mockedWriteFileSync).toHaveBeenCalledWith( + 2, expect.stringContaining( 'Fatal error: Failed to relaunch the CLI process.', ), @@ -123,7 +122,7 @@ describe('relaunchAppInChildProcess', () => { beforeEach(() => { vi.clearAllMocks(); - mocks.writeToStderr.mockClear(); + mockedWriteFileSync.mockClear(); process.env = { ...originalEnv }; delete process.env['GEMINI_CLI_NO_RELAUNCH'];