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.
This commit is contained in:
Tam Nhu Tran committed 2026-06-27 10:54:50 -04:00
1 parent 97e16d1d1c
commit 454e1555a7
2 files changed
+34

No files matched your search

@@ -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;
+8
View File
@@ -144,6 +144,7 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
// Spawn: Windows .cmd/.bat/.ps1 need shell escaping; all others spawn directly
let claude: ChildProcess;
try {
if (needsShell) {
const cmdString = [claudeCli, ...launchArgs].map(escapeShellArg).join(' ');
claude = spawn(cmdString, {
@@ -159,6 +160,13 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
env: tracedEnv,
});
}
} catch (spawnError) {
// spawn() can throw synchronously (e.g. invalid arg/env). Remove the
// runtime settings overlay before propagating so the secret-bearing temp
// file is not orphaned. cleanup is idempotent.
cleanupLaunchSettings();
throw spawnError;
}
// Remove the runtime settings overlay once Claude has read it and exited.
// cleanup is idempotent, so registering on both events is safe.