Skip to content

Commit a128f38

Browse files
committed
feat(windows): claudePathForSdk helper — unblocks LLM scanners + auditor
Full Windows E2E was blocked by Agent SDK `spawn EINVAL` on every sdk.query() call. Root cause: our findClaudePath resolves to `C:\...\claude.cmd` on Windows, we pass that as `pathToClaudeCodeExecutable`, and Node's child_process.spawn refuses to run .cmd/.bat without `shell: true` since CVE-2024-27980 (Node 20.12+). The SDK does not set shell:true, so every spawn throws before the user's auth is even checked. Fix: new `claudePathForSdk()` helper in utils/agent-options.ts that returns undefined on win32 (lets the SDK fall back to its own bundled cli.js which it spawns correctly) and the concrete path on POSIX (where the CJS bundle fileURLToPath(undefined) crash from B-006 / D-121 still matters). All three manual queryOpts sites migrated: - src/agents/session-auditor.ts (2 queryOpts blocks) - src/agents/memory-extractor.ts (1 queryOpts block) - src/utils/agent-options.ts::buildAgentQueryOptions (was inline) Regression test test/agent-sdk-paths.test.ts updated to accept either `claudePathForSdk` or `findClaudePath` as the auth-safe import. Also bumped @anthropic-ai/claude-agent-sdk ^0.2.84 → ^0.2.112 in the same pass (latest) — no behavior change from our side, but aligns with the version installed during Windows testing. Verified on Azure Win11 Pro 24H2 (Standard_D2s_v5, native Node 20.20.2, Claude Code 2.1.112 from npm install -g): - axme-code setup --force real OAuth: 26 LLM decisions + 13 presets = 39, $0.97, 211s, zero errors (was 0 LLM + 13 presets + 3x spawn EINVAL warnings before fix) - claude --print with Bash+Read tools: 22s, substantive answer identical to Linux baseline - PreToolUse + PostToolUse + SessionEnd hooks all fire - Detached audit worker: session_end → check_result PASS → audit_complete ($0.14), auditRan: true (was false before fix) Linux: npm test 511/511 pass, no regression.
1 parent 1bbf00c commit a128f38

6 files changed

Lines changed: 53 additions & 18 deletions

File tree

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"node": ">=20"
1919
},
2020
"dependencies": {
21-
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
21+
"@anthropic-ai/claude-agent-sdk": "^0.2.112",
2222
"@modelcontextprotocol/sdk": "^1.29.0",
2323
"js-yaml": "^4.1.0"
2424
},
@@ -34,8 +34,19 @@
3434
"url": "https://github.com/AxmeAI/axme-code.git"
3535
},
3636
"license": "MIT",
37-
"keywords": ["mcp", "claude-code", "ai-agent", "developer-tools"],
38-
"files": ["dist", "templates", ".claude-plugin", "README.md", "LICENSE"],
37+
"keywords": [
38+
"mcp",
39+
"claude-code",
40+
"ai-agent",
41+
"developer-tools"
42+
],
43+
"files": [
44+
"dist",
45+
"templates",
46+
".claude-plugin",
47+
"README.md",
48+
"LICENSE"
49+
],
3950
"overrides": {
4051
"@anthropic-ai/sdk": ">=0.81.0"
4152
}

src/agents/memory-extractor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import type { Memory } from "../types.js";
1313
import { extractCostFromResult, zeroCost, type CostInfo } from "../utils/cost-extractor.js";
1414
import { toMemorySlug } from "../storage/memory.js";
15-
import { buildAgentEnv, findClaudePath } from "../utils/agent-options.js";
15+
import { buildAgentEnv, claudePathForSdk } from "../utils/agent-options.js";
1616

1717
export interface MemoryExtractionResult {
1818
memories: Memory[];
@@ -69,7 +69,7 @@ export async function runMemoryExtraction(opts: {
6969
const startTime = Date.now();
7070
const model = opts.model ?? "claude-haiku-4-5";
7171

72-
const claudePath = findClaudePath();
72+
const claudePath = claudePathForSdk();
7373
const queryOpts = {
7474
cwd: opts.projectPath,
7575
model,

src/agents/session-auditor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { basename, relative } from "node:path";
2121
import type { Memory, Decision, SessionHandoff, WorkspaceInfo } from "../types.js";
2222
import { DEFAULT_AUDITOR_MODEL } from "../types.js";
2323
import { extractCostFromResult, zeroCost, type CostInfo } from "../utils/cost-extractor.js";
24-
import { buildAgentEnv, findClaudePath } from "../utils/agent-options.js";
24+
import { buildAgentEnv, claudePathForSdk } from "../utils/agent-options.js";
2525
import { toMemorySlug } from "../storage/memory.js";
2626
import { toSlug, listDecisions } from "../storage/decisions.js";
2727
import { listMemories } from "../storage/memory.js";
@@ -625,7 +625,7 @@ async function runSingleAuditCall(opts: {
625625
}> {
626626
const sdk = await import("@anthropic-ai/claude-agent-sdk");
627627

628-
const claudePath = findClaudePath();
628+
const claudePath = claudePathForSdk();
629629
const queryOpts = {
630630
cwd: opts.sessionOrigin,
631631
model: opts.model,
@@ -886,7 +886,7 @@ JSON SCHEMA:
886886
ANALYSIS TO FORMAT:
887887
${freeTextAnalysis}`;
888888

889-
const claudePath = findClaudePath();
889+
const claudePath = claudePathForSdk();
890890
const queryOpts = {
891891
cwd: sessionOrigin,
892892
model,

src/utils/agent-options.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ export function _resetFindClaudePath(): void {
110110
_claudePath = undefined;
111111
}
112112

113+
/**
114+
* Value to pass as the SDK's `pathToClaudeCodeExecutable` option.
115+
*
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.
127+
*/
128+
export function claudePathForSdk(): string | undefined {
129+
return process.platform === "win32" ? undefined : findClaudePath();
130+
}
131+
113132
export type AgentRole = "scanner" | "tester" | "reviewer" | "engineer" | "architect" | "auditor";
114133

115134
const ROLE_TOOLS: Record<AgentRole, { allowed: string[]; disallowed: string[] }> = {
@@ -176,7 +195,8 @@ export function buildAgentQueryOptions(base: {
176195
}, role: AgentRole): Options {
177196
const tools = ROLE_TOOLS[role];
178197

179-
const claudePath = findClaudePath();
198+
// See claudePathForSdk() docstring for why this isn't just findClaudePath().
199+
const claudePath = claudePathForSdk();
180200

181201
return {
182202
cwd: base.cwd,

test/agent-sdk-paths.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function walk(dir: string, out: string[] = []): string[] {
3636
return out;
3737
}
3838

39-
test("every src/agents file calling sdk.query imports buildAgentQueryOptions or findClaudePath", () => {
39+
test("every src/agents file calling sdk.query imports buildAgentQueryOptions or claudePathForSdk", () => {
4040
const files = walk(AGENTS_DIR);
4141
const offenders: string[] = [];
4242

@@ -45,7 +45,11 @@ test("every src/agents file calling sdk.query imports buildAgentQueryOptions or
4545
if (!src.includes("sdk.query(")) continue;
4646

4747
const hasBuilder = /import\s+[^;]*\bbuildAgentQueryOptions\b[^;]*from\s+["'][^"']*agent-options/.test(src);
48-
const hasFinder = /import\s+[^;]*\bfindClaudePath\b[^;]*from\s+["'][^"']*agent-options/.test(src);
48+
// Both `claudePathForSdk` and the older `findClaudePath` are accepted —
49+
// the former is the correct Windows-safe choice (returns undefined on
50+
// win32 to dodge `spawn EINVAL` on .cmd), the latter remains allowed
51+
// only for backwards-compat regression surface.
52+
const hasFinder = /import\s+[^;]*\b(claudePathForSdk|findClaudePath)\b[^;]*from\s+["'][^"']*agent-options/.test(src);
4953

5054
if (!hasBuilder && !hasFinder) {
5155
offenders.push(file.replace(AGENTS_DIR, ""));
@@ -56,7 +60,7 @@ test("every src/agents file calling sdk.query imports buildAgentQueryOptions or
5660
offenders,
5761
[],
5862
`The following files call sdk.query() but import neither buildAgentQueryOptions ` +
59-
`nor findClaudePath from utils/agent-options. Without pathToClaudeCodeExecutable ` +
63+
`nor claudePathForSdk from utils/agent-options. Without pathToClaudeCodeExecutable ` +
6064
`the bundled CJS build will crash with fileURLToPath(undefined) (B-006 / D-121):\n` +
6165
` ${offenders.join("\n ")}`,
6266
);

0 commit comments

Comments
 (0)