From d501e786b8afffabcc3dca19976ff7df9d8b3e5a Mon Sep 17 00:00:00 2001 From: nyxst4ck Date: Wed, 15 Jul 2026 20:34:35 -0300 Subject: [PATCH] fix(init): resolve Windows claude.exe only (#161) * fix(init): resolve Windows claude.exe only Windows startup can resolve npm or Git Bash shims from PATH and then pass that path into shell-less child_process calls. Those .cmd, .bat, .ps1, or extensionless shim matches are not spawnable as the CLAUDE binary, so fail fast with a clear hint instead of returning an unusable path. No cli.js citation: this only changes local startup binary discovery and fatal diagnostics. It does not change any Class A/Class B endpoint, header, request field, response field, or wire behavior. Co-Authored-By: Codex * fix(init): simplify Windows claude lookup Remove the redundant where.exe claude.exe probe and explain the intentional native .exe allow-list for shell-less Windows spawning. Alignment: no cli.js citation applies; this changes only local executable discovery and fatal diagnostics, not a Class A or Class B wire operation. Co-Authored-By: claude-flow --------- Co-authored-by: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Co-authored-by: Codex Co-authored-by: nyxst4ck Co-authored-by: claude-flow --- server.mjs | 77 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/server.mjs b/server.mjs index 8baf469..b908f49 100644 --- a/server.mjs +++ b/server.mjs @@ -97,8 +97,40 @@ function _collectNodeManagerCandidates(home) { return out; } +function _joinIfBase(base, ...parts) { + return base ? join(base, ...parts) : null; +} +function _collectWindowsClaudeCandidates() { + const userProfile = process.env.USERPROFILE || process.env.HOME || ""; + const localAppData = process.env.LOCALAPPDATA || ""; + return [ + _joinIfBase(userProfile, ".local", "bin", "claude.exe"), + _joinIfBase(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"), + _joinIfBase(localAppData, "Microsoft", "WindowsApps", "claude.exe"), + ].filter(Boolean); +} +function _isWindowsSpawnableBinary(path) { + return /\.exe$/i.test(path); +} +function _lookupLines(out) { + return out.split(/\r?\n/).map(line => line.trim()).filter(Boolean); +} +function _warnUnspawnableWindowsMatches(lines) { + const unspawnable = lines.filter(p => !/\.exe$/i.test(p)); + if (unspawnable.length > 0) { + console.warn(`[init] Ignoring non-exe Windows claude command(s): ${unspawnable.join(", ")}`); + } +} function resolveClaude() { + const isWin = process.platform === "win32"; if (process.env.CLAUDE_BIN) { + if (isWin && !_isWindowsSpawnableBinary(process.env.CLAUDE_BIN)) { + console.error( + `FATAL: CLAUDE_BIN="${process.env.CLAUDE_BIN}" is not a native Windows executable.\n` + + " Set CLAUDE_BIN to claude.exe; shell shims cannot be spawned without a shell." + ); + process.exit(1); + } try { accessSync(process.env.CLAUDE_BIN, constants.X_OK); return process.env.CLAUDE_BIN; @@ -108,28 +140,43 @@ function resolveClaude() { } } - const home = process.env.HOME || ""; - const candidates = [ - "/opt/homebrew/bin/claude", - "/usr/local/bin/claude", - "/usr/bin/claude", - join(home, ".local/bin/claude"), - ..._collectNodeManagerCandidates(home), - ]; + const home = process.env.HOME || process.env.USERPROFILE || ""; + const candidates = isWin + ? _collectWindowsClaudeCandidates() + : [ + "/opt/homebrew/bin/claude", + "/usr/local/bin/claude", + "/usr/bin/claude", + join(home, ".local/bin/claude"), + ..._collectNodeManagerCandidates(home), + ]; for (const p of candidates) { try { accessSync(p, constants.X_OK); console.warn(`[init] CLAUDE_BIN not set, resolved to ${p}`); return p; } catch {} } - try { - const resolved = execFileSync("which", ["claude"], { encoding: "utf8", timeout: 5000 }).trim(); - if (resolved) { console.warn(`[init] CLAUDE_BIN not set, resolved via which: ${resolved}`); return resolved; } - } catch {} + if (isWin) { + try { + const lines = _lookupLines(execFileSync("where.exe", ["claude"], { encoding: "utf8", timeout: 5000 })); + const resolved = lines.find(_isWindowsSpawnableBinary); + if (resolved) { console.warn(`[init] CLAUDE_BIN not set, resolved via where.exe: ${resolved}`); return resolved; } + _warnUnspawnableWindowsMatches(lines); + } catch {} + } else { + try { + const resolved = execFileSync("which", ["claude"], { encoding: "utf8", timeout: 5000 }).trim(); + if (resolved) { console.warn(`[init] CLAUDE_BIN not set, resolved via which: ${resolved}`); return resolved; } + } catch {} + } console.error( "FATAL: claude binary not found.\n" + - " Set CLAUDE_BIN=/path/to/claude or ensure claude is in PATH.\n" + - " Hint: if you use nvm/fnm/asdf, set CLAUDE_BIN to the absolute path\n" + - " shown by `which claude` in your interactive shell.\n" + + (isWin + ? " Set CLAUDE_BIN to the absolute path of claude.exe or ensure claude.exe is in PATH.\n" + + " Hint: npm .cmd/.bat/.ps1 shims cannot be spawned without a shell.\n" + + " The .exe requirement is an intentional allow-list for shell-less spawning.\n" + : " Set CLAUDE_BIN=/path/to/claude or ensure claude is in PATH.\n" + + " Hint: if you use nvm/fnm/asdf, set CLAUDE_BIN to the absolute path\n" + + " shown by `which claude` in your interactive shell.\n") + " Checked: " + candidates.join(", ") ); process.exit(1);