Skip to content

claude-cli on Windows: unconditional claude.cmd preference lets a broken npm shim shadow a working native install (silent fallback to Community N) #2573

Description

@efreeman518

Summary

On Windows, claude-cli resolution prefers shutil.which("claude.cmd") unconditionally, with no fallback and no validation. When npm's claude.cmd is present but its bundled binary is not, every call fails and graphify label degrades silently to Community N placeholders while exiting 0 - even though a working native claude.exe is on PATH.

This is the mirror image of #1288 / #1268 / #1072. Those fixed "bare claude cannot be executed" by explicitly resolving claude.cmd, under the environment noted in #1288: "Claude Code CLI installed via npm (claude.cmd on PATH, no claude.exe)". With the native installer that assumption inverts - claude.exe is the real CLI and any claude.cmd on PATH may be a stale or broken npm leftover.

Environment

  • graphify 0.9.36 / graphifyy 0.9.37
  • Windows 11 x64, Python 3.14
  • Claude Code installed natively (~/.local/bin/claude.exe, works), plus a broken npm global install

What happens

[graphify label] batch 1/7 (100 communities) failed: claude -p exited 1: This version of
  %APPDATA%\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe is not compatible with
  the version of Windows you're running.
... x7 ...
[graphify label] warning: community labeling failed (...); using Community N placeholders.
Done - 690 communities. GRAPH_REPORT.md and graph.json updated.

Exit code 0. graph.json and GRAPH_REPORT.md are rewritten anyway - label re-clusters regardless - so a run that named nothing produces a large diff indistinguishable from a successful one. On a tracked graphify-out/, that is ~61k inserted / ~61k deleted lines of pure churn for zero naming benefit.

Root cause

llm.py (both the _call_claude_cli path ~L1449-1463 and the _call_llm path ~L2572-2584):

claude_cmd = "claude"
if platform.system() == "Windows":
    cmd_path = shutil.which("claude.cmd")
    if cmd_path:
        claude_cmd = cmd_path          # taken unconditionally
    elif shutil.which("claude") is None:
        raise RuntimeError("Claude Code CLI not found on $PATH")

claude.cmd wins even when it is broken and a working claude.exe sits earlier on PATH.

Why npm's can be broken: the package installs a ~500-byte placeholder at bin/claude.exe whenever postinstall did not fetch the platform-native optional dependency (--ignore-scripts, --omit=optional, some pnpm configs). It is a shell script, not a PE:

echo "Error: claude native binary not installed." >&2
echo "Either postinstall did not run (--ignore-scripts, some pnpm configs)" >&2
echo "or the platform-native optional dependency was not downloaded (--omit=optional)." >&2

Windows refuses to execute a text file as .exe, hence OS error 216.

Two suggested fixes

1. Validate, or fall back. Prefer claude.cmd, but do not trust it blindly:

candidates = []
if platform.system() == "Windows":
    candidates += [shutil.which("claude.cmd"), shutil.which("claude.exe")]
candidates.append(shutil.which("claude"))

for cand in filter(None, candidates):
    try:
        probe = subprocess.run([cand, "--version"], capture_output=True,
                               text=True, timeout=30, **_no_window_kwargs())
        if probe.returncode == 0:
            claude_cmd = cand
            break
    except OSError:
        continue
else:
    raise RuntimeError("Claude Code CLI not found or not executable on $PATH")

A one-time --version probe is cheap next to a labeling run, and it fixes every variant of this class - broken shim, stale shim, .ps1-only, wrong-arch binary.

2. Do not exit 0 after labeling failed. Even with resolution fixed, "every batch failed" should not look like success. #1288 flagged this too. Either exit non-zero when all batches fail, or skip rewriting graph.json when zero labels were produced, so the placeholder fallback cannot masquerade as a completed run. The second is arguably more valuable: it prevents a no-op run from producing a huge, misleading diff.

Workaround for anyone hitting this

Put a claude.cmd earlier on PATH than %APPDATA%\npm that forwards to the working binary:

@echo off
"%~dp0claude.exe" %*
exit /b %ERRORLEVEL%

Or remove the broken package with npm uninstall -g @anthropic-ai/claude-code. Note it can come back - ours was silently reinstalled between runs, which is why the shim is the durable option.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions