Skip to content

Fix Claude CLI detection failing on Windows#84

Merged
bbsngg merged 2 commits intomainfrom
fix/windows-claude-cli-detection
Mar 24, 2026
Merged

Fix Claude CLI detection failing on Windows#84
bbsngg merged 2 commits intomainfrom
fix/windows-claude-cli-detection

Conversation

@bbsngg
Copy link
Copy Markdown
Contributor

@bbsngg bbsngg commented Mar 23, 2026

Summary

  • Use resolveAvailableCliCommand() for Claude CLI resolution in cli-auth.js, matching the pattern already used by Gemini and Codex
  • Add shell: process.platform === 'win32' to all spawn() calls for Claude CLI in both cli-auth.js and mcp.js
  • Add resolveClaudeCommand() helper in mcp.js to replace 5 hardcoded spawn('claude', ...) calls
  • Support CLAUDE_CLI_PATH env var override for custom install locations

Root Cause

On Windows, spawn('claude', ...) without shell: true cannot resolve claude.cmd/claude.exe from PATH, causing ENOENT errors. The frontend then shows "Claude Code CLI is not installed" even though it is installed.

Fixes #83

Test plan

  • Verify Claude CLI detection works on Windows 11 (requires Windows tester)
  • Verify no regression on macOS/Linux — existing behavior unchanged
  • Verify MCP server operations (list, add, remove, get) work on Windows
  • Verify CLAUDE_CLI_PATH env var override works

🤖 Generated with Claude Code

Use resolveAvailableCliCommand() for Claude CLI resolution (matching
the pattern already used by Gemini/Codex), and add shell: true on
Windows for all spawn() calls. This fixes ENOENT errors when the CLI
is installed as claude.cmd/claude.exe.

Fixes #83

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bbsngg bbsngg requested a review from davidliuk March 24, 2026 03:10
Copy link
Copy Markdown
Collaborator

@Zhang-Henry Zhang-Henry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

I've done a thorough review of the changes across both files. Overall this is a solid PR that correctly aligns Claude CLI resolution with the existing Gemini/Codex pattern. A few observations and questions:

What looks good

  1. Consistent pattern adoption — Using resolveAvailableCliCommand() with CLAUDE_CLI_PATH / appendWindowsSuffixes: true matches exactly how Gemini and Codex already work. This is the right approach.

  2. resolveClaudeCommand() helper in mcp.js — Centralizing the 5 hardcoded spawn('claude', ...) calls into one helper is a clean de-duplication.

  3. getSpawnOptions() helper — Simple and effective for ensuring shell: true on Windows across all spawn sites.

  4. Dead import cleanup — Removing the redundant const { spawn } = await import('child_process') and const { promisify } = await import('util') / const exec = promisify(spawn) inside individual route handlers is good. These were shadowing the top-level import and exec was never even used.

Issues / Questions

1. Redundant resolution — resolveClaudeCommand() is called on every single MCP request

Each route handler (/cli/list, /cli/add, /cli/add-json, /cli/remove/:name, /cli/get/:name) calls await resolveClaudeCommand() independently. Under the hood this spawns a probe process (claude --help) every time. For a user hitting multiple MCP endpoints in sequence, this means 5+ unnecessary probe spawns.

Consider resolving once at middleware level or caching the result (even a short-lived module-level cache with a TTL would help). The auth side doesn't have this problem because checkClaudeCredentials is called infrequently, but MCP routes can be hit in rapid succession.

2. resolveClaudeCommand() falls back to 'claude' when resolution fails — is this intentional?

async function resolveClaudeCommand() {
  return await resolveAvailableCliCommand({ ... }) || 'claude';
}

If resolveAvailableCliCommand returns null (i.e., no working claude binary was found), this silently falls back to 'claude' which will then fail with ENOENT on Windows — the exact original bug. The Gemini/Codex pattern in cli-auth.js handles null explicitly by returning an error early. The auth side of this PR does this correctly (early return with checkClaudeCredentialsFile({ cliAvailable: false })), but the MCP side swallows the failure.

Suggest either: (a) returning a 503 with a "Claude CLI not installed" message when resolution fails, matching how codex.js route handlers behave, or (b) at minimum, not falling back to 'claude' so the error surfaces clearly.

3. cliCommand propagation in checkClaudeCredentials — the on('error') path has a subtle inconsistency

In the error handler:

childProcess.on('error', (error) => {
  const commandMissing = error?.code === 'ENOENT';
  checkClaudeCredentialsFile({ cliAvailable: !commandMissing, cliCommand: resolvedCliCommand }).then(resolve);
});

If the command was already resolved successfully by resolveAvailableCliCommand, getting ENOENT here would be surprising (race condition where the binary was uninstalled between probe and use?). Passing cliAvailable: false while resolvedCliCommand is a valid string could produce a confusing state for the frontend. This is a minor edge case but worth noting — the pre-resolution should make ENOENT essentially impossible in this path, so the !commandMissing logic is now dead code. Consider simplifying to always pass cliAvailable: true since we already validated the command exists.

4. Minor: whitespace-only changes inflate the diff

Several hunks in mcp.js only change trailing whitespace on blank lines (e.g., removing trailing spaces between event handlers). These are harmless but add noise — roughly 15 of the 67 added lines are whitespace-only. Not a blocker, just something to be aware of for future PRs.

Summary

The core fix is correct and follows established patterns. The main actionable item is point #2 — the || 'claude' fallback in resolveClaudeCommand() defeats the purpose of the fix for MCP routes on Windows. Points 1 and 3 are non-blocking suggestions for robustness.

- Cache resolved Claude command in mcp.js with 30s TTL to avoid
  redundant probe spawns on rapid MCP requests (point #1)
- Remove || 'claude' fallback in resolveClaudeCommand(); return 503
  with clear error when CLI is not found (point #2)
- Simplify error handler in checkClaudeCredentials() since the command
  was already validated by resolveAvailableCliCommand (point #3)
- Keep diff minimal with no whitespace-only changes (point #4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bbsngg bbsngg merged commit a7469fa into main Mar 24, 2026
1 check passed
@bbsngg bbsngg deleted the fix/windows-claude-cli-detection branch March 24, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: CLaude Code status cannot be correctly recognized.

2 participants