Skip to content

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

Description

@cammcd

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.

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions