From 47fbe8bb3a5e09af6d439be9115174ce942f079c Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 28 Apr 2026 11:59:37 -0400 Subject: [PATCH] fix(proxy): clean up temp launch settings --- src/ccs.ts | 8 +++---- src/delegation/headless-executor.ts | 23 +++++++++++++++---- src/utils/openai-compat-launch-settings.ts | 23 ++++++++++++++++--- src/utils/shell-executor.ts | 14 ++++++++++- .../settings-profile-browser-launch.test.ts | 21 ++++++++++++++++- .../utils/claudecode-env-stripping.test.ts | 19 ++++++++++++++- 6 files changed, 93 insertions(+), 15 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 4a3edea6..878224c9 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -82,7 +82,7 @@ import { tryHandleRootCommand } from './commands/root-command-router'; // Import extracted utility functions import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor'; import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation'; -import { createOpenAICompatLaunchSettingsPath } from './utils/openai-compat-launch-settings'; +import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings'; import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning'; import { createLogger } from './services/logging'; import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides'; @@ -1460,14 +1460,14 @@ async function main(): Promise { ), }; delete proxyEnv.ANTHROPIC_API_KEY; - const launchSettingsPath = createOpenAICompatLaunchSettingsPath( + const launchSettings = createOpenAICompatLaunchSettings( expandedSettingsPath, settings ); const launchArgs = [ '--settings', - launchSettingsPath, + launchSettings.settingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs), ]; const traceEnv = createWebSearchTraceContext({ @@ -1478,7 +1478,7 @@ async function main(): Promise { settingsPath: expandedSettingsPath, }); - execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }); + execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }, launchSettings.cleanup); return; } const launchArgs = [ diff --git a/src/delegation/headless-executor.ts b/src/delegation/headless-executor.ts index 3ee916a5..26e2760c 100644 --- a/src/delegation/headless-executor.ts +++ b/src/delegation/headless-executor.ts @@ -23,7 +23,7 @@ import { stripAnthropicRoutingEnv, stripClaudeCodeEnv, } from '../utils/shell-executor'; -import { createOpenAICompatLaunchSettingsPath } from '../utils/openai-compat-launch-settings'; +import { createOpenAICompatLaunchSettings } from '../utils/openai-compat-launch-settings'; import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance'; import { appendThirdPartyImageAnalysisToolArgs, @@ -256,12 +256,12 @@ export class HeadlessExecutor { // Smart slash command detection and preservation const processedPrompt = this._processSlashCommand(enhancedPrompt); - const launchSettingsPath = openAICompatProfile - ? createOpenAICompatLaunchSettingsPath(settingsPath, settings) - : settingsPath; + const launchSettings = openAICompatProfile + ? createOpenAICompatLaunchSettings(settingsPath, settings) + : { settingsPath, cleanup: () => {} }; // Prepare arguments - const args: string[] = ['-p', processedPrompt, '--settings', launchSettingsPath]; + const args: string[] = ['-p', processedPrompt, '--settings', launchSettings.settingsPath]; // Always use stream-json for real-time progress visibility args.push('--output-format', 'stream-json', '--verbose'); @@ -373,6 +373,7 @@ export class HeadlessExecutor { imageAnalysisEnv, runtimeEnvVars, traceEnv, + launchCleanup: launchSettings.cleanup, }); } @@ -393,6 +394,7 @@ export class HeadlessExecutor { imageAnalysisEnv?: Record; runtimeEnvVars?: NodeJS.ProcessEnv; traceEnv?: Record; + launchCleanup?: () => void; } ): Promise { const { @@ -406,6 +408,7 @@ export class HeadlessExecutor { imageAnalysisEnv = {}, runtimeEnvVars = {}, traceEnv = {}, + launchCleanup = () => {}, } = ctx; return new Promise((resolve, reject) => { @@ -443,6 +446,14 @@ export class HeadlessExecutor { let progressInterval: NodeJS.Timeout | undefined; const messages: StreamMessage[] = []; let timedOut = false; + let cleanedUp = false; + const cleanupLaunchArtifacts = () => { + if (cleanedUp) { + return; + } + cleanedUp = true; + launchCleanup(); + }; // Setup signal handlers for cleanup const cleanupHandler = () => { @@ -501,6 +512,7 @@ export class HeadlessExecutor { // Handle completion proc.on('close', (exitCode: number | null) => { + cleanupLaunchArtifacts(); const duration = Date.now() - startTime; if (progressInterval) { @@ -593,6 +605,7 @@ export class HeadlessExecutor { // Handle errors proc.on('error', (error: Error) => { + cleanupLaunchArtifacts(); if (progressInterval) clearInterval(progressInterval); reject(new Error(`Failed to execute Claude CLI: ${error.message}`)); }); diff --git a/src/utils/openai-compat-launch-settings.ts b/src/utils/openai-compat-launch-settings.ts index eaefe3f1..bfb0887d 100644 --- a/src/utils/openai-compat-launch-settings.ts +++ b/src/utils/openai-compat-launch-settings.ts @@ -5,10 +5,15 @@ import * as path from 'path'; import type { Settings } from '../types/config'; import { stripAnthropicRoutingEnv } from './shell-executor'; -export function createOpenAICompatLaunchSettingsPath( +export interface OpenAICompatLaunchSettings { + settingsPath: string; + cleanup: () => void; +} + +export function createOpenAICompatLaunchSettings( settingsPath: string, settings: Settings -): string { +): OpenAICompatLaunchSettings { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-openai-compat-settings-')); fs.chmodSync(tempDir, 0o700); @@ -31,5 +36,17 @@ export function createOpenAICompatLaunchSettingsPath( mode: 0o600, }); - return launchSettingsPath; + let cleanedUp = false; + const cleanup = (): void => { + if (cleanedUp) { + return; + } + cleanedUp = true; + fs.rmSync(tempDir, { recursive: true, force: true }); + }; + + return { + settingsPath: launchSettingsPath, + cleanup, + }; } diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index a6a5fd63..35a7505d 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -208,7 +208,8 @@ export function getWindowsEscapedCommandShell(): SpawnOptions['shell'] { export function execClaude( claudeCli: string, args: string[], - envVars: NodeJS.ProcessEnv | null = null + envVars: NodeJS.ProcessEnv | null = null, + onExitCleanup?: () => void ): void { const isWindows = process.platform === 'win32'; const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli); @@ -286,6 +287,17 @@ export function execClaude( }); } + let cleanedUp = false; + const runExitCleanup = (): void => { + if (cleanedUp) { + return; + } + cleanedUp = true; + onExitCleanup?.(); + }; + child.once('exit', runExitCleanup); + child.once('error', runExitCleanup); + wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => { if (err.code === 'EACCES') { console.error(`[X] Claude CLI is not executable: ${claudeCli}`); diff --git a/tests/unit/targets/settings-profile-browser-launch.test.ts b/tests/unit/targets/settings-profile-browser-launch.test.ts index 47ab1f20..73d04f8c 100644 --- a/tests/unit/targets/settings-profile-browser-launch.test.ts +++ b/tests/unit/targets/settings-profile-browser-launch.test.ts @@ -92,6 +92,8 @@ describe('settings profile browser launch', () => { let fakeClaudePath = ''; let claudeArgsLogPath = ''; let claudeEnvLogPath = ''; + let claudeSettingsPathLogPath = ''; + let claudeSettingsSnapshotPath = ''; let browserProfileDir = ''; let devtoolsServer: ChildProcess | undefined; let baseEnv: NodeJS.ProcessEnv; @@ -107,6 +109,8 @@ describe('settings profile browser launch', () => { fakeClaudePath = path.join(tmpHome, 'fake-claude.sh'); claudeArgsLogPath = path.join(tmpHome, 'claude-args.txt'); claudeEnvLogPath = path.join(tmpHome, 'claude-env.txt'); + claudeSettingsPathLogPath = path.join(tmpHome, 'claude-settings-path.txt'); + claudeSettingsSnapshotPath = path.join(tmpHome, 'claude-settings-snapshot.json'); browserProfileDir = path.join(tmpHome, 'chrome-user-data'); fs.mkdirSync(ccsDir, { recursive: true }); @@ -138,6 +142,19 @@ describe('settings profile browser launch', () => { fakeClaudePath, `#!/bin/sh printf "%s\n" "$@" > "${claudeArgsLogPath}" +settings_path="" +prev="" +for arg in "$@"; do + if [ "$prev" = "--settings" ]; then + settings_path="$arg" + break + fi + prev="$arg" +done +printf "%s" "$settings_path" > "${claudeSettingsPathLogPath}" +if [ -n "$settings_path" ] && [ -f "$settings_path" ]; then + cat "$settings_path" > "${claudeSettingsSnapshotPath}" +fi { printf "userDataDir=%s\n" "$CCS_BROWSER_USER_DATA_DIR" printf "legacyProfileDir=%s\n" "$CCS_BROWSER_PROFILE_DIR" @@ -308,7 +325,7 @@ exit 0 expect(launchSettingsPath).not.toBe(settingsPath); const persistedLaunchSettings = JSON.parse( - fs.readFileSync(launchSettingsPath as string, 'utf8') + fs.readFileSync(claudeSettingsSnapshotPath, 'utf8') ) as { env?: Record; hooks?: { @@ -333,6 +350,8 @@ exit 0 expect(launchedEnv).not.toContain('anthropicBaseUrl=https://api.openai.com/v1'); expect(launchedEnv).toContain('anthropicModel=gpt-5.4'); expect(launchedEnv).toContain('maxOutputTokens=12345'); + expect(fs.readFileSync(claudeSettingsPathLogPath, 'utf8')).toBe(launchSettingsPath); + expect(fs.existsSync(launchSettingsPath as string)).toBe(false); }); it('does not auto-enable browser reuse for settings-profile launches from env overrides alone', async () => { diff --git a/tests/unit/utils/claudecode-env-stripping.test.ts b/tests/unit/utils/claudecode-env-stripping.test.ts index 2febe108..165314b2 100644 --- a/tests/unit/utils/claudecode-env-stripping.test.ts +++ b/tests/unit/utils/claudecode-env-stripping.test.ts @@ -31,6 +31,7 @@ const STEERING_PROMPT_SNIPPET = 'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches'; const spawnCalls: SpawnCall[] = []; const spawnSyncCalls: SpawnSyncCall[] = []; +const launchSettingsSnapshots: Array<{ path: string; content: string }> = []; const originalPlatform = process.platform; let baselineSigintListeners: Array<(...args: unknown[]) => void> = []; let baselineSigtermListeners: Array<(...args: unknown[]) => void> = []; @@ -99,6 +100,16 @@ function registerChildProcessMock(): void { } spawnCalls.push({ command, args, options }); + const settingsIndex = args.indexOf('--settings'); + if (settingsIndex >= 0) { + const settingsPath = args[settingsIndex + 1]; + if (settingsPath && fs.existsSync(settingsPath)) { + launchSettingsSnapshots.push({ + path: settingsPath, + content: fs.readFileSync(settingsPath, 'utf8'), + }); + } + } const child = createMockChild(); setTimeout(() => child.emit('close', 0), 0); @@ -193,6 +204,7 @@ describe('CLAUDECODE environment stripping', () => { beforeEach(() => { spawnCalls.length = 0; spawnSyncCalls.length = 0; + launchSettingsSnapshots.length = 0; process.env.CCS_QUIET = '1'; // Save original env values for restoration in afterEach @@ -695,8 +707,12 @@ describe('CLAUDECODE environment stripping', () => { const launchSettingsPath = args[settingsIndex + 1]; expect(launchSettingsPath).toBeDefined(); expect(launchSettingsPath).not.toBe(path.join(ccsDir, 'bridge.settings.json')); + const launchSettingsSnapshot = launchSettingsSnapshots.find( + (snapshot) => snapshot.path === launchSettingsPath + ); + expect(launchSettingsSnapshot).toBeDefined(); - const persistedLaunchSettings = JSON.parse(fs.readFileSync(launchSettingsPath, 'utf8')) as { + const persistedLaunchSettings = JSON.parse(launchSettingsSnapshot?.content || '{}') as { env?: Record; hooks?: { PreToolUse?: Array<{ @@ -713,6 +729,7 @@ describe('CLAUDECODE environment stripping', () => { expect( persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command ).toBe('echo headless-bridge-hook'); + expect(fs.existsSync(launchSettingsPath)).toBe(false); }); it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => {