danielmiessler / danielmiessler/LifeOS
Windows: Inference.ts cannot spawn npm's claude.cmd (EINVAL) — memory reviewer never completes a run
- Dominant language
- TypeScript
- Stars
- 19k
- Forks
- 2.5k
- Avg merge
- 8d 17h
- Merged PRs (30d)
- 1
Description
## 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\\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.
```ts
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.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in LIFEOS/TOOLS/Inference.ts at resolveClaudeBin() and the node:child_process spawn call, then reproduce the Windows claude.cmd failure. Verify that the Windows invocation preserves arguments containing spaces and empty strings, while non-Windows behavior remains unchanged. Done means a fresh Windows install can complete a reviewer run successfully, including response.raw.txt and parsed results.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, node.js, typescript
- Domain
- ai, cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100