Skip to content

Commit 9af6fec

Browse files
George-iamclaude
andcommitted
fix(windows): resolve real claude.exe so SDK skips broken fallback
In our standalone CJS bundle (what install.ps1 deploys), esbuild emits 'var import_meta = {};' because CJS has no native import.meta. The Agent SDK fallback at sdk.mjs:63 then crashes with fileURLToPath(undefined) when pathToClaudeCodeExecutable is omitted - which was the case on Windows since claudePathForSdk() returned undefined to dodge CVE-2024- 27980 spawn EINVAL on .cmd shims. Fix: on Windows, derive the real claude.exe from npm's claude.cmd shim location (<dir>\node_modules\@Anthropic-AI\claude-code\bin\ claude.exe) and pass that to the SDK. .exe is unaffected by CVE-2024- 27980 (only .cmd/.bat trigger spawn EINVAL), and bypassing the SDK broken fallback means the import.meta.url stub doesn't matter. claudePathForSdk() simplified to findClaudePath() - uniform across Linux/macOS/Windows, no platform special-case (D-136). Verified Azure Win11 Pro 24H2 native (Standard_D2s_v5, Node 20.20.2, Claude Code 2.1.123, standalone bundle deployed exactly as install.ps1 would): - axme-code setup --force real OAuth: 10 LLM + 13 presets = 23 decisions, 0.59 USD, 117s, zero errors (was 0 LLM + 13 presets + 4x fileURLToPath TypeError before fix) - claude --print --dangerously-skip-permissions with Write tool: notes.md created, PreToolUse + PostToolUse + SessionEnd all fired - Detached audit worker: session_end -> check_result PASS -> audit_complete (0.137 USD) Linux: npm test 511/511 pass, no regression. Prior 2026-04-17 'Windows verified' actually tested via dist/+ node_modules path (SDK loaded as ESM from disk so import.meta.url was defined and the SDK fallback worked). The standalone bundle path - what install.ps1 actually deploys - was never E2E-tested on Windows before today because v0.2.9 had no Windows binaries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8695d21 commit 9af6fec

1 file changed

Lines changed: 46 additions & 28 deletions

File tree

src/utils/agent-options.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { execSync } from "node:child_process";
66
import { existsSync, readdirSync } from "node:fs";
7-
import { join } from "node:path";
7+
import { dirname, join } from "node:path";
88
import { homedir } from "node:os";
99
import { resolveAuthMode } from "./auth-config.js";
1010

@@ -21,7 +21,11 @@ type Options = import("@anthropic-ai/claude-agent-sdk").Options;
2121
* Resolution order (B-009 — first match wins):
2222
* 1. `AXME_CLAUDE_EXECUTABLE` env var — explicit override for CI / unusual installs
2323
* 2. `CLAUDE_CODE_ENTRYPOINT` env var — set by Claude Code itself in some contexts
24-
* 3. `which claude` — standard PATH lookup (works on most dev machines)
24+
* 3. `which`/`where.exe` PATH lookup. On Windows the npm shim is
25+
* `claude.cmd`, but spawn() of .cmd hits CVE-2024-27980 EINVAL — so we
26+
* derive the real `claude.exe` underneath
27+
* `<npm-prefix>/node_modules/@anthropic-ai/claude-code/bin/claude.exe`
28+
* and return that instead.
2529
* 4. Standard install locations (no PATH dependency):
2630
* - ~/.local/bin/claude
2731
* - /usr/local/bin/claude
@@ -56,19 +60,40 @@ export function findClaudePath(): string | undefined {
5660
const lookup = process.platform === "win32" ? "where.exe claude" : "which claude";
5761
const p = execSync(lookup, { encoding: "utf-8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }).trim();
5862
const lines = p.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
59-
// On Windows, `where.exe claude` returns every matching entry including
60-
// the bare-name shebang file that npm ships for Unix compatibility
61-
// (e.g. C:\node\claude with no extension). cmd.exe / the Agent SDK
62-
// cannot execute such files — they need .cmd/.exe/.bat/.ps1. Pick the
63-
// first Windows-executable from the list; fall back to the first entry
64-
// only if nothing else matches (unlikely but keeps behaviour defined).
65-
const preferred = process.platform === "win32"
66-
? lines.find((r) => /\.(cmd|exe|bat|ps1)$/i.test(r))
67-
: undefined;
68-
const first = preferred ?? lines[0];
69-
if (first && existsSync(first)) {
70-
_claudePath = first;
71-
return _claudePath;
63+
if (process.platform === "win32") {
64+
// npm's claude.cmd is a shim that ultimately invokes the real
65+
// claude.exe under <npm-prefix>/node_modules/@anthropic-ai/claude-code/
66+
// bin/claude.exe. The Agent SDK can spawn .exe directly (no
67+
// CVE-2024-27980 EINVAL like .cmd/.bat) AND avoids the SDK's own
68+
// import.meta.url-based fallback which crashes inside our esbuild
69+
// bundle (fileURLToPath(undefined)). Always prefer the .exe.
70+
const cmd = lines.find((r) => /\\claude\.cmd$/i.test(r));
71+
if (cmd) {
72+
const exeCandidate = join(
73+
dirname(cmd),
74+
"node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe",
75+
);
76+
if (existsSync(exeCandidate)) {
77+
_claudePath = exeCandidate;
78+
return _claudePath;
79+
}
80+
}
81+
// Fallback: any .exe directly in the where.exe output (rare on npm
82+
// installs but possible for custom layouts).
83+
const directExe = lines.find((r) => /\.exe$/i.test(r) && existsSync(r));
84+
if (directExe) {
85+
_claudePath = directExe;
86+
return _claudePath;
87+
}
88+
// Last resort on Windows: bare name / .ps1 / .cmd. SDK will likely
89+
// fail on these (.cmd → spawn EINVAL, bare → not executable by
90+
// cmd.exe), so caller's deterministic fallback kicks in.
91+
} else {
92+
const first = lines[0];
93+
if (first && existsSync(first)) {
94+
_claudePath = first;
95+
return _claudePath;
96+
}
7297
}
7398
} catch { /* not in PATH — continue to standard locations */ }
7499

@@ -113,20 +138,14 @@ export function _resetFindClaudePath(): void {
113138
/**
114139
* Value to pass as the SDK's `pathToClaudeCodeExecutable` option.
115140
*
116-
* On POSIX this is the same as findClaudePath() — the SDK needs the concrete
117-
* path because its own `require.resolve("./cli.js")` fallback can hit
118-
* `fileURLToPath(undefined)` inside CJS bundles (B-006 / D-121).
119-
*
120-
* On Windows we return undefined, deliberately. The SDK's fallback in its own
121-
* bundled cli.js path works correctly there; meanwhile passing our PATH-
122-
* resolved `claude.cmd` triggers `spawn EINVAL` on every call because Node's
123-
* child_process.spawn refuses .cmd/.bat without `shell: true` since CVE-
124-
* 2024-27980, and the SDK does not set shell:true. Every site that constructs
125-
* queryOpts and calls `sdk.query()` MUST use this helper, not findClaudePath
126-
* directly, otherwise scanners/auditor/extractor all break on Windows.
141+
* Always equal to findClaudePath() — the SDK needs an explicit path because
142+
* its own `fileURLToPath(import.meta.url)` fallback crashes inside our
143+
* esbuild bundle (import.meta.url is undefined there). On Windows
144+
* findClaudePath() now resolves the `.cmd` shim back to the real `.exe`
145+
* (CVE-2024-27980 only affected .cmd/.bat — passing .exe to spawn is safe).
127146
*/
128147
export function claudePathForSdk(): string | undefined {
129-
return process.platform === "win32" ? undefined : findClaudePath();
148+
return findClaudePath();
130149
}
131150

132151
export type AgentRole = "scanner" | "tester" | "reviewer" | "engineer" | "architect" | "auditor";
@@ -195,7 +214,6 @@ export function buildAgentQueryOptions(base: {
195214
}, role: AgentRole): Options {
196215
const tools = ROLE_TOOLS[role];
197216

198-
// See claudePathForSdk() docstring for why this isn't just findClaudePath().
199217
const claudePath = claudePathForSdk();
200218

201219
return {

0 commit comments

Comments
 (0)