Skip to content

fix(claude): detect the CLI version on Windows instead of pinning the stale fallback - #74

Open
w3a11y wants to merge 1 commit into
V1ki:mainfrom
w3a11y:fix/windows-claude-version-detection
Open

fix(claude): detect the CLI version on Windows instead of pinning the stale fallback#74
w3a11y wants to merge 1 commit into
V1ki:mainfrom
w3a11y:fix/windows-claude-version-detection

Conversation

@w3a11y

@w3a11y w3a11y commented Sep 8, 2026

Copy link
Copy Markdown

Problem

npm installs Claude Code on Windows as claude.cmd (a batch shim) — there is no claude.exe. execFileSync cannot spawn either name directly:

call result
execFileSync('claude', …) ENOENTexecFileSync does not apply PATHEXT, so the extensionless name resolves to nothing
execFileSync('claude.cmd', …) EINVAL — Node refuses to spawn .cmd/.bat without shell: true (the CVE-2024-27980 fix, Node >= 18.20.2/20.12.2/21.7.3)

So detectClaudeVersion() could never succeed on Windows. The bare catch {} swallowed both errors and every request advertised CLAUDE_CLI_FALLBACK_VERSION regardless of what was installed.

Since Anthropic gates models on the version in that user-agent, this made newer models unusable on Windows:

claude API error (HTTP 400): "Claude Code 2.1.234 does not support this model;
version 2.1.251 or newer is required. Run 'claude update' ..."
error_code: claude_code_version_too_old

The 2.1.234 in that message is the hardcoded constant, not the installed version — I had 2.1.263. claude update cannot fix it, because the installed version is never read.

Fix

Route the Windows probes through cmd.exe, with claude.cmd as a second candidate. The argument sits inside the command string rather than in the argv array, which avoids Node's DEP0190 warning about unescaped shell arguments. Non-Windows behaviour is unchanged — same single execFileSync('claude', ['--version']) call.

Three incidental hardenings, all Windows-motivated:

  • timeout 3000 -> 10000. cmd.exe startup is now in the path and 3s is tight on a cold filesystem cache; a timeout here silently costs the real version.
  • Regex unanchored, so a shell that prefixes output cannot defeat parsing.
  • stdio: ['ignore', 'pipe', 'ignore'] keeps CLI stderr chatter out of the parsed text.

Fallback bump

CLAUDE_CLI_FALLBACK_VERSION also goes 2.1.234 -> 2.1.263.

It is sent verbatim whenever detection fails for any reason — CLI not on PATH, restricted spawn, sandbox — so at 2.1.234 the safety net had itself drifted below the floor Anthropic enforces, making it a guaranteed HTTP 400 on every platform rather than a graceful degradation.

Tests

Two regression tests added to test/detect-cli.spec.ts.

The first cannot assume a real CLI in CI, so it writes a tiny stub claude/claude.cmd to a temp dir, puts that dir on PATH, and asserts detection reports the stub's version rather than the constant — pinning the property that actually broke, on every platform.

Verified they fail against the current implementation and pass with this change:

# before (buggy implementation)
x detectClaudeVersion reads a resolvable CLI rather than the fallback
  detection fell back to the hardcoded constant even though a working
  `claude` was on PATH (expected 9.9.9). On Windows this is the
  execFileSync ENOENT/EINVAL batch-shim failure.
x detectClaudeVersion fallback is new enough for currently gated models
  fallback 2.1.234 is below the 2.1.251 floor required by current models

# after
tests 421   pass 421   fail 0

Full suite green (pnpm test, which runs tsc first): 421/421.

The existing returns the fallback when claude is not in PATH test still passes — I checked specifically, since shell: true could plausibly have broken it: cmd.exe honours an empty PATH for command lookup, so the stubbed-out environment still yields the fallback.

Verification

Confirmed on Windows 11 (build 26200), Node v24.0.2, npm 11.6.2, Claude Code 2.1.263 installed as claude.cmd: detection now returns 2.1.263 in ~60 ms and Sonnet 4.5 works.


One thing I did not change, but worth flagging: the bare catch {} is what made this hard to diagnose. The resulting error names a version that appears nowhere in the user's install and suggests a remedy that cannot work. A single debug log on the fallback path would point straight at the cause. Happy to add that here or separately if you'd like.

… fallback

npm installs Claude Code on Windows as `claude.cmd`; no `claude.exe`
exists. `execFileSync` cannot spawn either name directly there — the
extensionless one fails `ENOENT` because `execFileSync` does not apply
`PATHEXT`, and the `.cmd` fails `EINVAL` because Node refuses to spawn
batch files without a shell (the CVE-2024-27980 fix).

`detectClaudeVersion()` therefore could never succeed on Windows. The
bare `catch {}` swallowed both errors, so every request advertised
`CLAUDE_CLI_FALLBACK_VERSION` regardless of the installed version.

Anthropic gates models on that user-agent, so this made newer models
unusable on Windows. Sonnet 4.5 requires 2.1.251+ and failed with
`claude_code_version_too_old` naming 2.1.234 — the constant, not the
installed CLI (2.1.263 locally). `claude update` could not help,
because the installed version was never read.

Route the Windows probes through `cmd.exe`, keeping the argument inside
the command string so Node does not warn about unescaped shell
arguments (DEP0190). Non-Windows behaviour is unchanged.

Also raise the fallback to 2.1.263. It is sent verbatim whenever
detection fails for any reason (CLI absent, sandboxed spawn), so at
2.1.234 the safety net sat below the floor Anthropic enforces and was
itself a guaranteed HTTP 400.

Both regression tests fail against the previous implementation and pass
with this change. The existing empty-PATH fallback test still passes:
cmd.exe honours an empty PATH for command lookup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant