From 454e1555a7b1d50f77d77c2fd7b6690930e52260 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 27 Jun 2026 10:54:50 -0400 Subject: [PATCH] fix(cliproxy): clean up launch-settings overlay on synchronous spawn failure The runtime settings overlay cleanup was registered only on the child 'exit'/'error' events, which run after spawn() returns. A synchronous spawn() throw (e.g. invalid arg/env) propagated out of launchClaude before those handlers were wired, orphaning the secret-bearing 0600 overlay file in os.tmpdir(). Wrap the spawn in try/catch and run the idempotent cleanup before rethrowing. --- .../__tests__/claude-launcher.test.ts | 26 ++++++++++++++ src/cliproxy/executor/claude-launcher.ts | 36 +++++++++++-------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/cliproxy/executor/__tests__/claude-launcher.test.ts b/src/cliproxy/executor/__tests__/claude-launcher.test.ts index e6111e50..3e587ca9 100644 --- a/src/cliproxy/executor/__tests__/claude-launcher.test.ts +++ b/src/cliproxy/executor/__tests__/claude-launcher.test.ts @@ -87,6 +87,16 @@ mock.module('../../quota/quota-manager', () => ({ stopQuotaMonitor: jest.fn(), })); +const mockCleanupLaunchSettings = jest.fn(); +const mockPrepareLaunchSettings = jest.fn().mockReturnValue({ + settingsPath: '/tmp/fake-settings-overlay.json', + cleanup: mockCleanupLaunchSettings, +}); + +mock.module('../launch-settings', () => ({ + prepareLaunchSettings: mockPrepareLaunchSettings, +})); + // ── Subject under test ──────────────────────────────────────────────────────── import { launchClaude } from '../claude-launcher'; @@ -131,6 +141,12 @@ describe('launchClaude', () => { mockSpawn.mockClear(); mockSetupCleanupHandlers.mockClear(); mockEscapeShellArg.mockClear(); + mockCleanupLaunchSettings.mockClear(); + mockPrepareLaunchSettings.mockClear(); + mockPrepareLaunchSettings.mockReturnValue({ + settingsPath: '/tmp/fake-settings-overlay.json', + cleanup: mockCleanupLaunchSettings, + }); }); it('calls spawn with claudeCli and includes --settings arg', async () => { @@ -189,6 +205,16 @@ describe('launchClaude', () => { expect(result).toBe(mockSpawnResult); }); + it('calls cleanup and rethrows when spawn throws synchronously', async () => { + const spawnErr = new Error('ERR_INVALID_ARG_VALUE'); + mockSpawn.mockImplementationOnce(() => { + throw spawnErr; + }); + + await expect(launchClaude(baseContext())).rejects.toThrow('ERR_INVALID_ARG_VALUE'); + expect(mockCleanupLaunchSettings).toHaveBeenCalledTimes(1); + }); + describe('Windows shell escaping', () => { const originalPlatform = process.platform; diff --git a/src/cliproxy/executor/claude-launcher.ts b/src/cliproxy/executor/claude-launcher.ts index ca539971..ff189bbf 100644 --- a/src/cliproxy/executor/claude-launcher.ts +++ b/src/cliproxy/executor/claude-launcher.ts @@ -144,20 +144,28 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise