#2057·LifeOS

Windows: Inference.ts cannot spawn npm's claude.cmd (EINVAL) — memory reviewer never completes a run

Author: cammcdCreated Sep 4, 2026Updated Sep 17, 2026

Summary

On Windows, LIFEOS/TOOLS/Inference.ts cannot spawn the Claude CLI at all. Every inference call dies before reaching the model, so the memory reviewer has never completed a single successful run on a fresh Windows install.

Environment

  • Windows 11, LifeOS 7.40.4
  • bun 1.4.0, Node 22.9
  • Claude Code CLI installed via npm i -g @anthropic-ai/claude-code (2.1.258)

What happens

resolveClaudeBin() returns the path from Bun.which("claude"), which on Windows is npm's shim claude.cmd. That path is handed to spawn imported from node:child_process, which refuses to execute a .cmd without a shell and throws:

error: spawn C:\Users\<user>\AppData\Roaming\npm\claude.cmd EINVAL
    syscall: "spawn"
    errno: -4071
    code: "EINVAL"

Node tightened this in the CVE-2024-27980 fix: .cmd and .bat require shell: true.

Observed downstream effect

MemoryHealthCheck reports critical with reviewer-latest-timed-out and priorSuccesses: 0. The reviewer run directory contains prompt.system.md, prompt.user.md and transcript.txt but no response.raw.txt, because inference returns before writing one. The failure is silent from the user's perspective: the reviewer simply never produces memory.

Worth noting Bun.spawn handles .cmd correctly; it is specifically the node:child_process import that fails. So the bug is invisible to anyone testing the spawn path under Bun's own API.

Suggested fix

Use shell: true on win32 when the resolved binary is a .cmd/.bat. cmd.exe does not auto-quote, so arguments containing spaces and empty-string arguments must be quoted, or the deliberate --tools '' and --setting-sources '' arguments silently vanish and change the meaning of the invocation.

typescript
const claudeBin = resolveClaudeBin();
const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(claudeBin);
const q = (a: string): string => (a === '' || /\s/.test(a) ? `"${a}"` : a);
const proc = spawn(
  needsShell ? q(claudeBin) : claudeBin,
  needsShell ? args.map(q) : args,
  { env, stdio: ['pipe', 'pipe', 'pipe'], ...(needsShell ? { shell: true, windowsHide: true } : {}) },
);

Relationship to #2003

Fixing this uncovers #2003 as the next failure: once the spawn succeeds, cmd.exe's ~8191 character command-line limit rejects the reviewer's ~9 KB system prompt. The two are sequential, not alternatives. Both must be fixed for the memory loop to work on Windows.

Verified locally: after applying the patch above plus the #2003 threshold change, a reviewer run returns ok: true, parse_ok: true, with items dispatched and zero failures.