From f0ec82005446d51434816e5d64b74e3d0c0ea602 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:54:13 -0400 Subject: [PATCH 01/16] fix(cliproxy): disable spoofable default bg keepalive probe (#1340) --- src/cliproxy/executor/session-bridge.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cliproxy/executor/session-bridge.ts b/src/cliproxy/executor/session-bridge.ts index c160de2a..4b181903 100644 --- a/src/cliproxy/executor/session-bridge.ts +++ b/src/cliproxy/executor/session-bridge.ts @@ -275,12 +275,13 @@ export function setupCleanupHandlers( } }; - const backgroundProbe = - keepAliveOptions.hasBackgroundWorkerUsingBaseUrl ?? hasClaudeBackgroundWorkerUsingBaseUrl; + // Security hardening: only allow keepalive when caller provides an explicit, + // trusted detector. Falling back to global process-list probing is spoofable. + const backgroundProbe = keepAliveOptions.hasBackgroundWorkerUsingBaseUrl; const startKeepAliveWatcher = (baseUrl: string) => { const timer = setInterval(() => { - if (backgroundProbe(baseUrl)) return; + if (backgroundProbe?.(baseUrl)) return; log('No Claude bg worker is using the session proxy; cleaning up'); stopSessionResources(); process.exit(0); @@ -299,6 +300,7 @@ export function setupCleanupHandlers( const backgroundKeepAliveBaseUrl = keepAliveOptions.backgroundKeepAliveBaseUrl; if ( + backgroundProbe && shouldKeepSessionProxiesAlive({ code, signal, From fd561b59bc4b1bd46419848a1babc388d31d5321 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:54:30 -0400 Subject: [PATCH 02/16] fix(dispatcher): treat --print as non-subcommand Claude session (#1341) * fix(dispatcher): treat --print as non-subcommand Claude session * style: apply prettier formatting * fix(dispatcher): correct return type in subcommand detector getClaudeSubcommandName returns string | null; return false (boolean) was invalid after dev refactored isClaudeSubcommandInvocation to delegate to it. Change to return null to preserve the --print safety fix intent. --- src/utils/claude-subcommand-detector.ts | 10 +++++++--- tests/unit/utils/claude-subcommand-detector.test.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/utils/claude-subcommand-detector.ts b/src/utils/claude-subcommand-detector.ts index 9f425e82..4ecdaf8d 100644 --- a/src/utils/claude-subcommand-detector.ts +++ b/src/utils/claude-subcommand-detector.ts @@ -119,9 +119,11 @@ const SUBCOMMAND_ALLOWED_SESSION_FLAGS: Record> = { * * Heuristic: * 1. Walk args until the `--` terminator or end. - * 2. Skip known value-taking flags together with their next token. - * 3. Skip unknown `--flag=value` forms and bare `--flag` / `-x` tokens. - * 4. The first remaining positional token is the candidate. + * 2. If `--print` is present before the first positional token, treat as + * prompt/headless session mode (never a subcommand launch). + * 3. Skip known value-taking flags together with their next token. + * 4. Skip unknown `--flag=value` forms and bare `--flag` / `-x` tokens. + * 5. The first remaining positional token is the candidate. * 5. Match against CLAUDE_SUBCOMMANDS. * * Anything after the candidate is irrelevant — once a subcommand is in play, @@ -142,6 +144,8 @@ export function getClaudeSubcommandName(args: readonly string[]): string | null const arg = args[i]; if (arg === '--') return null; + if (arg === '--print') return null; + if (arg.startsWith('-')) { if (VALUE_TAKING_FLAGS.has(arg)) { // Skip the next token as the flag's value (when present and not another flag). diff --git a/tests/unit/utils/claude-subcommand-detector.test.ts b/tests/unit/utils/claude-subcommand-detector.test.ts index ae6f0488..27492e0c 100644 --- a/tests/unit/utils/claude-subcommand-detector.test.ts +++ b/tests/unit/utils/claude-subcommand-detector.test.ts @@ -56,6 +56,12 @@ describe('isClaudeSubcommandInvocation', () => { expect(isClaudeSubcommandInvocation(['--name', 'auth'])).toBe(false); }); + + it('treats --print prompt mode as non-subcommand even with subcommand-like prompt text', () => { + expect(isClaudeSubcommandInvocation(['--print', 'agents'])).toBe(false); + expect(isClaudeSubcommandInvocation(['--print', 'doctor'])).toBe(false); + }); + it('handles --flag=value forms', () => { expect(isClaudeSubcommandInvocation(['--model=sonnet', 'agents'])).toBe(true); }); @@ -201,6 +207,13 @@ describe('subcommand passthrough — injectors short-circuit', () => { expect(appendBrowserToolArgs(['remote-control'])).toEqual(['remote-control']); }); + + it('injectors still inject in --print prompt mode with subcommand-like prompt text', () => { + const out = appendThirdPartyWebSearchToolArgs(['--print', 'agents']); + expect(out).toContain('--append-system-prompt'); + expect(out).toContain('--disallowedTools'); + }); + it('injectors still inject for non-subcommand interactive launches', () => { const out = appendThirdPartyWebSearchToolArgs(['fix the bug']); expect(out).toContain('--append-system-prompt'); From af7aa0c2dda834237a3aa79661cd62d096c32876 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:54:48 -0400 Subject: [PATCH 03/16] fix(cliproxy): sanitize local port before config regeneration (#1342) --- src/web-server/routes/proxy-routes.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/web-server/routes/proxy-routes.ts b/src/web-server/routes/proxy-routes.ts index a98867ed..8032149a 100644 --- a/src/web-server/routes/proxy-routes.ts +++ b/src/web-server/routes/proxy-routes.ts @@ -155,8 +155,9 @@ router.put('/backend', (req: Request, res: Response) => { // Pre-flight read: check running state before acquiring write lock const currentConfig = loadOrCreateUnifiedConfig(); const currentBackend = currentConfig.cliproxy?.backend ?? DEFAULT_BACKEND; - const localPort = - currentConfig.cliproxy_server?.local?.port ?? DEFAULT_CLIPROXY_SERVER_CONFIG.local.port; + const localPort = validatePort( + currentConfig.cliproxy_server?.local?.port ?? DEFAULT_CLIPROXY_SERVER_CONFIG.local.port + ); if (currentBackend !== backend && isProxyRunning() && !force) { res.status(409).json({ error: 'Proxy is running. Stop proxy first or use force=true to change backend.', From 66966f35276556551db52f38af46b5a5f8634c03 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:55:05 -0400 Subject: [PATCH 04/16] fix(pricing): guard malformed models.dev model entries (#1344) * fix(pricing): guard malformed models.dev model entries * style: apply prettier formatting --- src/web-server/models-dev/pricing-resolver.ts | 2 ++ tests/unit/model-pricing.test.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/web-server/models-dev/pricing-resolver.ts b/src/web-server/models-dev/pricing-resolver.ts index daad1c92..08599e3c 100644 --- a/src/web-server/models-dev/pricing-resolver.ts +++ b/src/web-server/models-dev/pricing-resolver.ts @@ -91,6 +91,7 @@ function findModel(provider: ModelsDevProvider, model: string): ModelsDevModel | const normalizedEntries = new Map(); for (const [key, value] of Object.entries(models)) { + if (!value || typeof value !== 'object') continue; normalizedEntries.set(normalizeId(key), value); if (typeof value.id === 'string') normalizedEntries.set(normalizeId(value.id), value); } @@ -188,6 +189,7 @@ export function getKnownModelsDevModels(): string[] { const ids = new Set(); for (const provider of Object.values(registry)) { for (const model of Object.values(provider.models ?? {})) { + if (!model || typeof model !== 'object') continue; if (typeof model.id === 'string') ids.add(`${provider.id}/${model.id}`); } } diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 779dfac9..b19e9f75 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -392,6 +392,26 @@ describe('model-pricing', () => { expect(pricing.outputPerMillion).toBe(0); }); + it('ignores malformed models.dev entries during provider-aware lookups', () => { + setCachedModelsDevRegistry({ + openai: { + id: 'openai', + name: 'OpenAI', + models: { + bad: null as unknown as never, + 'gpt-4o': { + id: 'gpt-4o', + name: 'GPT-4o', + cost: { input: 2.5, output: 10 }, + }, + }, + }, + }); + + expect(() => getModelPricing('gpt-4o', { provider: 'openai' })).not.toThrow(); + expect(getModelPricing('gpt-4o', { provider: 'openai' }).inputPerMillion).toBe(2.5); + }); + it('prefers provider-aware models.dev pricing over exact static table matches', () => { const pricing = getModelPricing('gpt-4o', { provider: 'github-copilot' }); expect(pricing.inputPerMillion).toBe(0); From b71fe5dd6ec975ac13ea1f33bd00d51df8ac80dc Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:55:55 -0400 Subject: [PATCH 05/16] fix(codex-quota): guard feature label type in quota windows (#1346) --- .../__tests__/quota-fetcher-codex.test.ts | 20 +++++++++++++++++++ src/cliproxy/quota/quota-fetcher-codex.ts | 6 +++++- ui/src/lib/utils.ts | 4 ++-- .../unit/ui/lib/codex-quota-breakdown.test.ts | 5 +++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts b/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts index 284f9b38..7a913713 100644 --- a/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts +++ b/src/cliproxy/quota/__tests__/quota-fetcher-codex.test.ts @@ -342,6 +342,26 @@ describe('Codex Quota Fetcher', () => { expect(windows.find((w) => w.category === 'additional')).toBeUndefined(); }); + it('should coerce non-string additional limit_name values to fallback label', () => { + const response = { + additional_rate_limits: [ + { + limit_name: { unexpected: true }, + rate_limit: { + primary_window: { used_percent: 10, reset_after_seconds: 3600 }, + }, + }, + ], + }; + + const windows = buildCodexQuotaWindows(response as never); + + expect(windows).toHaveLength(1); + expect(windows[0].category).toBe('additional'); + expect(windows[0].featureLabel).toBe('Additional'); + expect(windows[0].label).toBe('Additional (Primary)'); + }); + it('should accept camelCase additionalRateLimits and rateLimit fields', () => { const response = { additionalRateLimits: [ diff --git a/src/cliproxy/quota/quota-fetcher-codex.ts b/src/cliproxy/quota/quota-fetcher-codex.ts index 82cf99ed..78ab225b 100644 --- a/src/cliproxy/quota/quota-fetcher-codex.ts +++ b/src/cliproxy/quota/quota-fetcher-codex.ts @@ -380,7 +380,11 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[] const entryRateLimit = entry.rate_limit || entry.rateLimit; if (!entryRateLimit) continue; - const featureLabel = entry.limit_name || entry.limitName || 'Additional'; + const rawFeatureLabel = entry.limit_name ?? entry.limitName; + const featureLabel = + typeof rawFeatureLabel === 'string' && rawFeatureLabel.trim().length > 0 + ? rawFeatureLabel.trim() + : 'Additional'; addWindow( `${featureLabel} (Primary)`, entryRateLimit.primary_window || entryRateLimit.primaryWindow, diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 00a936f7..e064e810 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -384,8 +384,8 @@ function getCodexWindowKindFromLabel(label: string): CodexWindowKind { * "OmniReview" -> "OmniReview" * "" -> "" */ -export function prettifyCodexFeatureLabel(featureLabel: string): string { - const trimmed = (featureLabel || '').trim(); +export function prettifyCodexFeatureLabel(featureLabel: unknown): string { + const trimmed = typeof featureLabel === 'string' ? featureLabel.trim() : ''; if (!trimmed) return ''; const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, ''); if (stripped !== trimmed && stripped.length > 0) { diff --git a/ui/tests/unit/ui/lib/codex-quota-breakdown.test.ts b/ui/tests/unit/ui/lib/codex-quota-breakdown.test.ts index 99c6c6e1..d7631901 100644 --- a/ui/tests/unit/ui/lib/codex-quota-breakdown.test.ts +++ b/ui/tests/unit/ui/lib/codex-quota-breakdown.test.ts @@ -37,6 +37,11 @@ describe('prettifyCodexFeatureLabel', () => { expect(prettifyCodexFeatureLabel(' ')).toBe(''); expect(prettifyCodexFeatureLabel(' GPT-5.3-Codex-Spark ')).toBe('Codex Spark'); }); + + it('returns empty string for non-string input', () => { + expect(prettifyCodexFeatureLabel({ label: 'Spark' })).toBe(''); + expect(prettifyCodexFeatureLabel(123)).toBe(''); + }); }); describe('getCodexWindowKind', () => { From 469c9ffde44ca7bba8c9e33365d92b9a819cdec7 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:56:20 -0400 Subject: [PATCH 06/16] fix(analytics): harden sqlite3 invocation for native Droid usage scan (#1347) * fix(analytics): use trusted sqlite3 binary path * style: apply prettier formatting --- src/web-server/usage/sqlite-cli.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/web-server/usage/sqlite-cli.ts b/src/web-server/usage/sqlite-cli.ts index cde5afcb..ea09a1e0 100644 --- a/src/web-server/usage/sqlite-cli.ts +++ b/src/web-server/usage/sqlite-cli.ts @@ -4,6 +4,11 @@ import { promisify } from 'util'; const execFileAsync = promisify(execFile); const SQLITE_JSON_MAX_BUFFER = 10 * 1024 * 1024; +const TRUSTED_SQLITE_PATHS = [ + '/usr/bin/sqlite3', + '/usr/local/bin/sqlite3', + '/opt/homebrew/bin/sqlite3', +]; export type SqliteJsonRow = Record; @@ -13,13 +18,31 @@ function isCommandMissing(error: unknown): boolean { return nodeError.code === 'ENOENT' || /not found/i.test(nodeError.message); } +function resolveTrustedSqlitePath(): string { + const trustedPath = TRUSTED_SQLITE_PATHS.find((candidate) => { + try { + fs.accessSync(candidate, fs.constants.X_OK); + return true; + } catch { + return false; + } + }); + + if (!trustedPath) { + throw new Error('sqlite3 command not available'); + } + + return trustedPath; +} + export async function querySqliteJson(dbPath: string, sql: string): Promise { if (!fs.existsSync(dbPath)) { return []; } try { - const { stdout } = await execFileAsync('sqlite3', ['-json', dbPath, sql], { + const sqlitePath = resolveTrustedSqlitePath(); + const { stdout } = await execFileAsync(sqlitePath, ['-json', dbPath, sql], { maxBuffer: SQLITE_JSON_MAX_BUFFER, }); const trimmed = stdout.trim(); From 825efb8b109e88c4f778259176cf2485406eb8ed Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:56:44 -0400 Subject: [PATCH 07/16] fix(codex-auth): stop spawning powershell in shell detection (#1348) --- src/codex-auth/shell-detect.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/codex-auth/shell-detect.ts b/src/codex-auth/shell-detect.ts index f1425f2d..d9046775 100644 --- a/src/codex-auth/shell-detect.ts +++ b/src/codex-auth/shell-detect.ts @@ -3,8 +3,6 @@ * Determines current shell to emit correct eval-safe export syntax. */ -import * as childProcess from 'child_process'; - export type Shell = 'bash' | 'zsh' | 'fish' | 'pwsh' | 'cmd'; /** @@ -20,7 +18,7 @@ export function detectShell( if (platform === 'win32') { return ( shellFromExecutable(env.SHELL) ?? - shellFromExecutable(parentProcessName ?? detectParentProcessName(platform)) ?? + shellFromExecutable(parentProcessName) ?? shellFromExecutable(env.ComSpec ?? env.COMSPEC) ?? 'cmd' ); @@ -31,26 +29,6 @@ export function detectShell( return 'bash'; // default for bash, sh, dash, ksh } -function detectParentProcessName(platform: string): string | undefined { - if (platform !== 'win32' || !Number.isInteger(process.ppid) || process.ppid <= 0) { - return undefined; - } - - const result = childProcess.spawnSync( - 'powershell.exe', - [ - '-NoProfile', - '-NonInteractive', - '-Command', - `(Get-Process -Id ${process.ppid} -ErrorAction Stop).ProcessName`, - ], - { encoding: 'utf8', timeout: 1500, windowsHide: true } - ); - - if (result.status !== 0 || !result.stdout) return undefined; - return result.stdout.trim().split(/\r?\n/).pop()?.trim(); -} - function shellFromExecutable(value: string | undefined): Shell | null { if (!value) return null; const base = value From 58df9999db44727695cbc0631f646623eb7e7fb5 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 16:57:08 -0400 Subject: [PATCH 08/16] fix(ui-docs): remove third-party runtime assets from health report (#1349) * fix(ui-docs): remove third-party runtime assets from health report * style: apply prettier formatting --- ui/docs/health-redesign/report.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/docs/health-redesign/report.html b/ui/docs/health-redesign/report.html index 80cad7dc..13a1b2cf 100644 --- a/ui/docs/health-redesign/report.html +++ b/ui/docs/health-redesign/report.html @@ -4,10 +4,6 @@ UI Verification Report - Health Page Redesign - - - -