fix(codex): harden native alias launches

- pass ccsx help/version flags straight through to native Codex

- strip parent Codex session env before nested launches

- avoid eager version/help probes unless config override detection is needed
This commit is contained in:
Tam Nhu Tran
2026-03-29 15:54:18 -04:00
parent bcbf780f82
commit f47ab484f3
6 changed files with 182 additions and 29 deletions
+47
View File
@@ -95,6 +95,7 @@ interface RuntimeReasoningResolution {
}
const CODEX_RUNTIME_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
const CODEX_NATIVE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--version', '-v']);
/**
* Smart profile detection
@@ -239,6 +240,47 @@ function resolveNativeClaudeLaunchArgs(
return buildOfficialChannelsArgs(args, plan.appliedChannels, plan.wantsPermissionBypass);
}
function shouldPassthroughNativeCodexFlagCommand(args: string[]): boolean {
const targetArgs = stripTargetFlag(args);
if (targetArgs.length === 0) {
return false;
}
return (
resolveTargetType(args) === 'codex' && CODEX_NATIVE_PASSTHROUGH_FLAGS.has(targetArgs[0] || '')
);
}
function execNativeCodexFlagCommand(args: string[]): void {
const adapter = getTarget('codex');
if (!adapter) {
console.error(fail('Target adapter not found for "codex"'));
process.exit(1);
}
const binaryInfo = adapter.detectBinary();
if (!binaryInfo) {
console.error(fail('Codex CLI not found.'));
console.error(info('Install a recent @openai/codex build, then retry.'));
process.exit(1);
}
const targetArgs = stripTargetFlag(args);
const creds: TargetCredentials = {
profile: 'default',
baseUrl: '',
apiKey: '',
};
const builtArgs = adapter.buildArgs('default', targetArgs, {
creds,
profileType: 'default',
binaryInfo,
});
const targetEnv = adapter.buildEnv(creds, 'default');
adapter.exec(builtArgs, targetEnv, { binaryInfo });
}
async function main(): Promise<void> {
// Register target adapters
registerTarget(new ClaudeAdapter());
@@ -315,6 +357,11 @@ async function main(): Promise<void> {
}
}
if (shouldPassthroughNativeCodexFlagCommand(args)) {
execNativeCodexFlagCommand(args);
return;
}
const firstArg = args[0];
// Trigger update check early for ALL commands except version/help/update
+5 -3
View File
@@ -3,7 +3,7 @@ import * as fs from 'fs';
import type { ProfileType } from '../types/profile';
import { runCleanup } from '../errors';
import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { escapeShellArg, stripAnthropicEnv } from '../utils/shell-executor';
import { escapeShellArg, stripAnthropicEnv, stripCodexSessionEnv } from '../utils/shell-executor';
import type {
TargetAdapter,
TargetBinaryInfo,
@@ -76,7 +76,7 @@ export class CodexAdapter implements TargetAdapter {
readonly displayName = 'Codex CLI';
detectBinary(): TargetBinaryInfo | null {
return getCodexBinaryInfo();
return getCodexBinaryInfo({ includeVersion: false, includeFeatures: false });
}
async prepareCredentials(_creds: TargetCredentials): Promise<void> {
@@ -148,7 +148,9 @@ export class CodexAdapter implements TargetAdapter {
}
buildEnv(creds: TargetCredentials, profileType: ProfileType): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...stripAnthropicEnv(process.env) };
const env: NodeJS.ProcessEnv = {
...stripCodexSessionEnv(stripAnthropicEnv(process.env)),
};
delete env[CODEX_RUNTIME_ENV_KEY];
if (profileType !== 'default') {
if (!creds.apiKey?.trim()) {
+15 -4
View File
@@ -118,7 +118,10 @@ export function detectCodexCli(): string | null {
return null;
}
export function getCodexBinaryInfo(): TargetBinaryInfo | null {
export function getCodexBinaryInfo(options?: {
includeVersion?: boolean;
includeFeatures?: boolean;
}): TargetBinaryInfo | null {
const codexPath = detectCodexCli();
if (!codexPath) return null;
@@ -126,13 +129,21 @@ export function getCodexBinaryInfo(): TargetBinaryInfo | null {
return {
path: codexPath,
needsShell: isWindows && /\.(cmd|bat|ps1)$/i.test(codexPath),
version: readCodexVersion(codexPath),
features: detectCodexFeatures(codexPath),
version: options?.includeVersion === false ? undefined : readCodexVersion(codexPath),
features: options?.includeFeatures === false ? undefined : detectCodexFeatures(codexPath),
};
}
export function codexBinarySupportsConfigOverrides(
binaryInfo: TargetBinaryInfo | null | undefined
): boolean {
return Boolean(binaryInfo?.features?.includes(CODEX_CONFIG_OVERRIDE_FEATURE));
if (!binaryInfo) {
return false;
}
if (binaryInfo.features) {
return binaryInfo.features.includes(CODEX_CONFIG_OVERRIDE_FEATURE);
}
return detectCodexFeatures(binaryInfo.path).includes(CODEX_CONFIG_OVERRIDE_FEATURE);
}
+19
View File
@@ -42,6 +42,25 @@ export function stripClaudeCodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return result;
}
/**
* Strip Codex session-scoped env vars before launching a nested Codex process.
*
* Keep real user config such as CODEX_HOME intact. Only remove the known
* session/runtime metadata exported by the current Codex host process.
*/
export function stripCodexSessionEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const sessionKeys = new Set(['CODEX_CI', 'CODEX_MANAGED_BY_BUN', 'CODEX_THREAD_ID']);
const result: NodeJS.ProcessEnv = {};
for (const key of Object.keys(env)) {
const upperKey = key.toUpperCase();
if (sessionKeys.has(upperKey)) {
continue;
}
result[key] = env[key];
}
return result;
}
/**
* Resolve CCS-managed environment overrides for Claude launch.
* - preferences.auto_update: false -> DISABLE_AUTOUPDATER=1
+59 -18
View File
@@ -165,24 +165,65 @@ describe('CodexAdapter', () => {
});
test('injects CCS_CODEX_API_KEY for CCS-backed launches only', () => {
const settingsEnv = adapter.buildEnv(
{
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
},
'cliproxy'
);
expect(settingsEnv.CCS_CODEX_API_KEY).toBe('cliproxy-token');
const originalCodeXHome = process.env.CODEX_HOME;
const originalCodeXCi = process.env.CODEX_CI;
const originalCodeXThreadId = process.env.CODEX_THREAD_ID;
const originalAnthropicBaseUrl = process.env.ANTHROPIC_BASE_URL;
const defaultEnv = adapter.buildEnv(
{
profile: 'default',
baseUrl: '',
apiKey: '',
},
'default'
);
expect(defaultEnv.CCS_CODEX_API_KEY).toBeUndefined();
try {
process.env.CODEX_HOME = '/tmp/codex-home';
process.env.CODEX_CI = '1';
process.env.CODEX_THREAD_ID = 'thread-123';
process.env.ANTHROPIC_BASE_URL = 'https://stale-proxy.invalid';
const settingsEnv = adapter.buildEnv(
{
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
},
'cliproxy'
);
expect(settingsEnv.CCS_CODEX_API_KEY).toBe('cliproxy-token');
expect(settingsEnv.CODEX_HOME).toBe('/tmp/codex-home');
expect(settingsEnv.CODEX_CI).toBeUndefined();
expect(settingsEnv.CODEX_THREAD_ID).toBeUndefined();
expect(settingsEnv.ANTHROPIC_BASE_URL).toBeUndefined();
const defaultEnv = adapter.buildEnv(
{
profile: 'default',
baseUrl: '',
apiKey: '',
},
'default'
);
expect(defaultEnv.CCS_CODEX_API_KEY).toBeUndefined();
expect(defaultEnv.CODEX_HOME).toBe('/tmp/codex-home');
expect(defaultEnv.CODEX_CI).toBeUndefined();
expect(defaultEnv.CODEX_THREAD_ID).toBeUndefined();
expect(defaultEnv.ANTHROPIC_BASE_URL).toBeUndefined();
} finally {
if (originalCodeXHome === undefined) {
delete process.env.CODEX_HOME;
} else {
process.env.CODEX_HOME = originalCodeXHome;
}
if (originalCodeXCi === undefined) {
delete process.env.CODEX_CI;
} else {
process.env.CODEX_CI = originalCodeXCi;
}
if (originalCodeXThreadId === undefined) {
delete process.env.CODEX_THREAD_ID;
} else {
process.env.CODEX_THREAD_ID = originalCodeXThreadId;
}
if (originalAnthropicBaseUrl === undefined) {
delete process.env.ANTHROPIC_BASE_URL;
} else {
process.env.ANTHROPIC_BASE_URL = originalAnthropicBaseUrl;
}
}
});
});
@@ -25,6 +25,21 @@ function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
};
}
function runCodexAlias(args: string[], env: NodeJS.ProcessEnv): RunResult {
const codexEntry = path.join(process.cwd(), 'src', 'bin', 'codex-runtime.ts');
const result = spawnSync(process.execPath, [codexEntry, ...args], {
encoding: 'utf8',
env,
timeout: 20000,
});
return {
status: result.status,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function readLoggedCodexCalls(logPath: string): string[][] {
if (!fs.existsSync(logPath)) {
return [];
@@ -93,7 +108,7 @@ process.exit(0);
fs.rmSync(tmpHome, { recursive: true, force: true });
});
it('ignores numeric CCS_THINKING env overrides for native Codex default mode', () => {
it('does not preflight native Codex default launches when no runtime overrides are needed', () => {
if (process.platform === 'win32') return;
const result = runCcs(['default', '--target', 'codex', 'fix failing tests'], {
@@ -108,7 +123,7 @@ process.exit(0);
expect(result.status).toBe(0);
const calls = readLoggedCodexCalls(codexArgsLogPath);
expect(calls.at(-1)).toEqual(['fix failing tests']);
expect(calls).toEqual([['fix failing tests']]);
});
it('ignores off-style CCS_THINKING env overrides for native Codex default mode', () => {
@@ -126,7 +141,25 @@ process.exit(0);
expect(result.status).toBe(0);
const calls = readLoggedCodexCalls(codexArgsLogPath);
expect(calls.at(-1)).toEqual(['fix failing tests']);
expect(calls).toEqual([['fix failing tests']]);
});
it('passes ccsx --version straight through to the native Codex binary', () => {
if (process.platform === 'win32') return;
const result = runCodexAlias(['--version'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
});
expect(result.status).toBe(0);
expect(result.stdout).toContain('codex-cli 9.9.9-test');
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]);
});
it('fails fast when native Codex reasoning overrides need unsupported --config support', () => {
@@ -145,7 +178,7 @@ process.exit(0);
expect(result.status).toBe(1);
expect(result.stderr).toContain('does not advertise --config overrides');
const calls = readLoggedCodexCalls(codexArgsLogPath);
expect(calls).toEqual([['--version'], ['--help']]);
expect(calls).toEqual([['--help']]);
});
it('reports unsupported generic settings profiles before Codex install guidance', () => {