From 1b450e487e8398aa5c58934ed42e6ee174f4b906 Mon Sep 17 00:00:00 2001 From: sulthan zahran Date: Sat, 17 Oct 2026 16:51:24 +0700 Subject: [PATCH] fix(claude): don't treat a wrapper command's --version output as Claude Code's version (#7209) resolveClaudeCodeVersion() probes --version and parsed any output with the generic fallback in parseClaudeCodeVersion. With a wrapper command (e.g. cux), that output is the wrapper's own version, which then failed every minimumClaudeCodeVersion gate and silently hid Opus 5.5, Opus 5, Fable 5, Sonnet 5.5 and Haiku 5.5. Only trust output that identifies itself as Claude Code (x.y.z (Claude Code)); anything else falls into the existing unknown-version path that does not gate models. --- .../agent/providers/claude/agent.test.ts | 30 +++++++++++++++++++ .../server/agent/providers/claude/agent.ts | 8 ++++- .../agent/providers/claude/models.test.ts | 27 +++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index d07b0572b7..d6b869f1e0 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -575,6 +575,36 @@ describe("ClaudeAgentClient binary resolution", () => { await expect(resolveClaudeCodeVersion()).resolves.toMatch(/^\d+\.\d+\.\d+$/); }); + test("rejects a wrapper command's own version output instead of reading it as Claude Code's", async () => { + const wrapperDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-version-")); + const wrapperPath = path.join(wrapperDir, "wrapper"); + await fs.writeFile(wrapperPath, 'console.log("cux 0.4.1");\n'); + try { + await expect( + resolveClaudeCodeVersion({ + command: { mode: "replace", argv: [process.execPath, wrapperPath] }, + }), + ).rejects.toThrow("Unable to parse Claude Code version from --version output"); + } finally { + await fs.rm(wrapperDir, { recursive: true, force: true }); + } + }); + + test("resolves the version when the command forwards the Claude Code banner", async () => { + const wrapperDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-version-")); + const wrapperPath = path.join(wrapperDir, "wrapper"); + await fs.writeFile(wrapperPath, 'console.log("2.1.302 (Claude Code)");\n'); + try { + await expect( + resolveClaudeCodeVersion({ + command: { mode: "replace", argv: [process.execPath, wrapperPath] }, + }), + ).resolves.toBe("2.1.302"); + } finally { + await fs.rm(wrapperDir, { recursive: true, force: true }); + } + }); + test("loads user, project, and local Claude settings", async () => { const queryReturn = vi.fn(); queryReturn.mockResolvedValue(undefined); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 7517a87029..b12ab706da 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -1839,7 +1839,13 @@ export async function resolveClaudeCodeVersion( timeout: 5_000, signal, }); - const version = parseClaudeCodeVersion(`${stdout}\n${stderr}`); + // A wrapper command (the `command` provider setting) prints its own version here + // instead of Claude Code's. Only trust output that identifies itself as Claude Code, + // so a wrapper's version can't mis-gate models behind a minimumClaudeCodeVersion. + const rawVersionOutput = `${stdout}\n${stderr}`; + const version = /\d+\.\d+\.\d+\s+\(Claude Code\)/i.test(rawVersionOutput) + ? parseClaudeCodeVersion(rawVersionOutput) + : null; if (!version) { throw new Error("Unable to parse Claude Code version from --version output"); } diff --git a/packages/server/src/server/agent/providers/claude/models.test.ts b/packages/server/src/server/agent/providers/claude/models.test.ts index 16d13b9d68..0daa5e533a 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -559,6 +559,33 @@ describe("ClaudeAgentClient.fetchCatalog", () => { expect(models.map((model) => model.id)).not.toContain("claude-opus-5[1m]"); expect(models.map((model) => model.id)).not.toContain("claude-opus-5"); }); + + it("keeps version-gated models when the probe command cannot identify Claude Code", async () => { + const configDir = await createClaudeConfigDir({}); + const wrapperDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-wrapper-")); + const wrapperPath = path.join(wrapperDir, "wrapper"); + await fs.writeFile(wrapperPath, 'console.log("cux 0.4.1");\n'); + try { + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + runtimeSettings: { + command: { mode: "replace", argv: [process.execPath, wrapperPath] }, + env: { CLAUDE_CONFIG_DIR: configDir }, + }, + discoverModels: async () => [], + }); + + const { models } = await client.fetchCatalog({ + scope: "workspace", + cwd: configDir, + force: false, + }); + + expect(models.map((model) => model.id)).toContain("claude-opus-5-5"); + } finally { + await fs.rm(wrapperDir, { recursive: true, force: true }); + } + }); }); describe("normalizeClaudeRuntimeModelId", () => {