fix(persist): block codex claude settings bridge

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-19 07:32:04 -04:00
1 parent a6ee3536c4
commit 3b2016462a
8 files changed
+219 -27

No files matched your search

@@ -58,7 +58,11 @@ async function createRestoreFixture(
};
await fs.promises.mkdir(claudeDir, { recursive: true });
await fs.promises.writeFile(settingsPath, JSON.stringify(originalSettings, null, 2) + '\n', 'utf8');
await fs.promises.writeFile(
settingsPath,
JSON.stringify(originalSettings, null, 2) + '\n',
'utf8'
);
await fs.promises.writeFile(backupPath, JSON.stringify(backupSettings, null, 2) + '\n', 'utf8');
return { claudeDir, settingsPath, backupPath, timestamp, originalSettings, backupSettings };
@@ -98,15 +102,15 @@ afterEach(async () => {
describe('persist command real handler paths', () => {
it('throws parseError for missing --permission-mode before profile detection', async () => {
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode']))).rejects.toThrow(
'Missing value for --permission-mode'
);
await expect(
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode']))
).rejects.toThrow('Missing value for --permission-mode');
});
it('throws parseError for empty inline --permission-mode before profile detection', async () => {
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))).rejects.toThrow(
'Missing value for --permission-mode'
);
await expect(
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))
).rejects.toThrow('Missing value for --permission-mode');
});
it('throws parseError for invalid --permission-mode before profile detection', async () => {
@@ -116,34 +120,36 @@ describe('persist command real handler paths', () => {
});
it('throws parseError for unknown flags on real handler path', async () => {
await expect(withScopedHome(() => handlePersistCommand(['glm', '--unknown-flag']))).rejects.toThrow(
/Unknown option\(s\)/
);
await expect(
withScopedHome(() => handlePersistCommand(['glm', '--unknown-flag']))
).rejects.toThrow(/Unknown option\(s\)/);
});
it('throws parseError for list/restore conflict on real handler path', async () => {
await expect(withScopedHome(() => handlePersistCommand(['--list-backups', '--restore']))).rejects.toThrow(
'--list-backups cannot be used with --restore'
);
await expect(
withScopedHome(() => handlePersistCommand(['--list-backups', '--restore']))
).rejects.toThrow('--list-backups cannot be used with --restore');
});
it('throws parseError for permission flags with --restore on real handler path', async () => {
await expect(withScopedHome(() => handlePersistCommand(['--restore', '--auto-approve']))).rejects.toThrow(
/Permission flags are not valid with backup operations/
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', '--auto-approve']))
).rejects.toThrow(/Permission flags are not valid with backup operations/);
});
it('shows help when --help is present even with other invalid args', async () => {
await expect(withScopedHome(() => handlePersistCommand(['--help', '--permission-mode']))).resolves.toBeUndefined();
await expect(
withScopedHome(() => handlePersistCommand(['--help', '--permission-mode']))
).resolves.toBeUndefined();
});
it('does not create CLAUDE_CONFIG_DIR on parseError path', async () => {
const isolatedClaudeDir = path.join(tempRoot, '.claude-parse-early');
process.env.CLAUDE_CONFIG_DIR = isolatedClaudeDir;
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))).rejects.toThrow(
'Missing value for --permission-mode'
);
await expect(
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))
).rejects.toThrow('Missing value for --permission-mode');
expect(await pathExists(isolatedClaudeDir)).toBe(false);
});
});
@@ -294,6 +300,10 @@ describe('persist command Claude extension parity', () => {
type: 'api',
settings: path.join(tempRoot, '.ccs', 'glm.settings.json'),
};
config.cliproxy.variants.codex = {
provider: 'codex',
settings: path.join(tempRoot, '.ccs', 'codex.settings.json'),
};
await fs.promises.mkdir(path.join(tempRoot, '.ccs'), { recursive: true });
await fs.promises.writeFile(
@@ -383,4 +393,50 @@ describe('persist command Claude extension parity', () => {
expect(persisted.env.CLAUDE_CONFIG_DIR).toBe(path.join(tempRoot, '.ccs', 'instances', 'work'));
expect(fs.existsSync(persisted.env.CLAUDE_CONFIG_DIR)).toBe(true);
});
it('blocks Codex CLIProxy profiles from Claude settings persistence', async () => {
await writeUnifiedConfig();
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
await fs.promises.writeFile(
settingsPath,
JSON.stringify(
{
env: {
KEEP_ME: 'still-here',
},
},
null,
2
) + '\n',
'utf8'
);
const originalConsoleLog = console.log;
const capturedLogs: string[] = [];
console.log = (...args: unknown[]) => {
capturedLogs.push(args.map((arg) => String(arg)).join(' '));
};
stubProcessExit();
try {
await expect(withScopedHome(() => handlePersistCommand(['codex', '--yes']))).rejects.toThrow(
'process.exit(1)'
);
} finally {
console.log = originalConsoleLog;
}
const renderedLogs = capturedLogs.join('\n');
expect(renderedLogs).toContain('Codex CLIProxy profile');
expect(renderedLogs).toContain('ccsxp');
expect(renderedLogs).toContain('ccs persist default --yes');
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(persisted.env.KEEP_ME).toBe('still-here');
expect(persisted.env.ANTHROPIC_BASE_URL).toBeUndefined();
});
});
@@ -0,0 +1,60 @@
import { afterEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ClaudeSettingsChecker } from '../../../../src/management/checks/config-check';
import { HealthCheck } from '../../../../src/management/checks/types';
import { runWithScopedCcsHome } from '../../../../src/utils/config-manager';
let originalClaudeConfigDir: string | undefined;
afterEach(() => {
if (originalClaudeConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
}
});
describe('ClaudeSettingsChecker', () => {
it('warns when Claude settings point at the Codex CLIProxy translator', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-config-check-'));
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
delete process.env.CLAUDE_CONFIG_DIR;
try {
await runWithScopedCcsHome(tempRoot, async () => {
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(
settingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
},
},
null,
2
) + '\n'
);
const originalConsoleLog = console.log;
console.log = () => {};
try {
const results = new HealthCheck();
new ClaudeSettingsChecker().run(results);
expect(results.warnings).toHaveLength(1);
expect(results.warnings[0].message).toContain('Codex CLIProxy translator');
expect(results.warnings[0].fix).toContain('ccs persist default --yes');
} finally {
console.log = originalConsoleLog;
}
});
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
});
@@ -34,8 +34,9 @@ describe('web-server claude-extension-routes', () => {
({ default: SharedManager } = await import('../../../src/management/shared-manager'));
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
({ default: claudeExtensionRoutes } =
await import('../../../src/web-server/routes/claude-extension-routes'));
({ default: claudeExtensionRoutes } = await import(
'../../../src/web-server/routes/claude-extension-routes'
));
const app = express();
app.use(express.json());
@@ -127,6 +128,10 @@ describe('web-server claude-extension-routes', () => {
context_mode: 'isolated',
};
config.default = 'work';
config.cliproxy.variants.codex = {
provider: 'codex',
settings: path.join(ccsDir, 'codex.settings.json'),
};
saveUnifiedConfig(config);
});
@@ -154,12 +159,23 @@ describe('web-server claude-extension-routes', () => {
expect(payload.profiles.some((profile) => profile.name === 'glm')).toBe(true);
expect(payload.profiles.some((profile) => profile.name === 'work')).toBe(true);
expect(payload.profiles.some((profile) => profile.name === 'gemini')).toBe(true);
expect(payload.profiles.some((profile) => profile.name === 'codex')).toBe(false);
expect(payload.hosts.map((host) => host.id)).toEqual(['vscode', 'cursor', 'windsurf']);
expect(payload.hosts.every((host) => host.defaultSettingsPath.endsWith('settings.json'))).toBe(
true
);
});
it('rejects Codex CLIProxy setup with native Codex guidance', async () => {
const response = await fetch(`${baseUrl}/api/claude-extension/setup?profile=codex&host=vscode`);
expect(response.status).toBe(400);
const payload = (await response.json()) as { error: string };
expect(payload.error).toContain('Codex CLIProxy profile');
expect(payload.error).toContain('ccsxp');
expect(payload.error).toContain('ccs codex --target codex');
});
it('renders VS Code setup for API profiles with disableLoginPrompt', async () => {
const response = await fetch(`${baseUrl}/api/claude-extension/setup?profile=glm&host=vscode`);
expect(response.status).toBe(200);