Skip to content

fix(windows): resolve dev-install's claude through where.exe, not a fixed suffix - #394

Merged
avifenesh merged 2 commits into
mainfrom
fix/dev-install-claude-resolve
Aug 16, 2026
Merged

fix(windows): resolve dev-install's claude through where.exe, not a fixed suffix#394
avifenesh merged 2 commits into
mainfrom
fix/dev-install-claude-resolve

Conversation

@avifenesh

Copy link
Copy Markdown
Collaborator

Why

Follow-up on the review of #393. That PR turned dev-install's four external commands into argv spawns, and the two claude ones resolved the executable with resolveExecutableForPlatform, which maps a bare claude to claude.cmd on win32.

That is exactly the assumption #390 removed from bin/cli.js. The npm global install does ship claude.cmd, but the native installer ships claude.exe. On such a host:

  • commandExists('claude') passes, because where.exe applies PATHEXT and finds claude.exe;
  • the spawn of claude.cmd then fails, because execFileSync does not;
  • both call sites (scripts/dev-install.js:293 and :304) caught the error and discarded it.

So the marketplace removal and every plugin uninstall silently did nothing, and the run still reported success - a regression against the old execSync, which got PATHEXT for free from cmd.exe.

What changed

  • The where.exe-based pick moves out of bin/cli.js into lib/utils/claude-executable.js. scripts/dev-install.js needs the same answer, and a second resolution that guesses the suffix drifts from the first the moment an install layout changes. cli.js imports it instead of keeping a copy, and stops exporting the two functions it no longer owns.
  • installClaude resolves once via claudeExecutable() and passes that to both runCommand calls.
  • The empty catches become a warning. Both removals are best-effort - nothing to remove exits non-zero, and that is normal - so the two cases are told apart by what execFileSync reports: a numeric status means claude ran and refused, while a spawn failure leaves status null and carries the errno code. Only the second is worth a line, and only the first one of them.
  • WINDOWS_BATCH_SHIM is exported from lib/utils/command-parser.js, carrying the CVE-2024-27980 note it had in cli.js, since callers that choose between executables have to know which ones cost a cmd.exe hop.

Tests

__tests__/claude-executable.test.js (new): the six resolution cases moved with the module, plus where.exe being asked exactly once, not asked at all off Windows, the fallback when where.exe itself fails, and the cache reset.

__tests__/dev-install.test.js: five tests running installClaude against a stubbed child_process and a scratch HOME, covering the native .exe spawning directly with no cmd.exe call, the .cmd shim taking the hop, the posix path, the warning on a spawn failure, and silence on a plain non-zero exit.

Local gate: npm test 89 suites / 3584 passed / 39 skipped, npm run validate, gen-docs:check, gen-adapters:check, expand-templates:check all clean.

…ixed suffix

dev-install's four external commands became argv spawns in #393, and the claude
ones resolved the executable with resolveExecutableForPlatform, which maps a bare
`claude` to `claude.cmd` on win32. That is the assumption #390 removed from
bin/cli.js: the npm global install does ship claude.cmd, but the native installer
ships claude.exe, and there commandExists('claude') passes while the spawn of
claude.cmd fails - with both call sites catching and discarding the error, so the
marketplace removal and every plugin uninstall silently did nothing.

The where.exe-based pick moves out of bin/cli.js into
lib/utils/claude-executable.js so both callers get the same answer and cannot
drift apart; cli.js imports it rather than keeping a copy. Its unit tests move to
__tests__/claude-executable.test.js with the module, and cli.js stops exporting
the two functions it no longer owns.

The empty catches also become a warning. Both removals are best-effort - nothing
to remove exits non-zero and that is normal - so the two cases are told apart by
what execFileSync reports: a numeric status means claude ran and refused, while a
spawn failure leaves status null and carries the errno code. Only the second is
worth a line, and only the first one of them.

Tests cover the native .exe spawning directly with no cmd.exe hop, the .cmd shim
taking the hop, the posix path, where.exe failing entirely, and the warning
appearing for a spawn failure but not for a non-zero exit.
Copilot AI lite review requested due to automatic review settings August 16, 2026 19:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@revuto-review revuto-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is an auto review done by revuto.


Reviewed the move of Claude executable resolution into lib/utils/claude-executable.js and the new best-effort/spawn-failure split in installClaude. The refactor itself looks consistent: bin/cli.js no longer needs resolveExecutableForPlatform (no other use remains), WINDOWS_BATCH_SHIM is exported with its CVE note intact, and the removed bin/cli.js exports had no consumers outside __tests__/cli-args.test.js, which the PR updates. One gap in the new warning logic on the exact host class this PR targets — inline.

Comment thread scripts/dev-install.js Outdated
const reportIfNeverRan = (err) => {
// execFileSync sets a numeric status when the child ran and exited non-zero;
// a spawn failure leaves status null and carries the errno code instead.
if (typeof err.status === 'number' || spawnFailureReported) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is an auto review done by revuto.


The typeof err.status === 'number' discriminator does not hold on the Windows/npm-shim host this PR is about.

runCommand sends claudeBin through planShimSpawn (line 76), and for a .cmd/.bat executable on win32 that rewrites the spawn to cmd.exe /d /s /c "..." (lib/utils/command-parser.js:120-150). So when claudeBin is claude.cmd, the process that is actually spawned is cmd.exe, which exists — a claude.cmd that cannot be launched (stale where.exe entry, wrong shim, missing target) makes cmd.exe exit non-zero (9009 / 1), i.e. err.status is a number and reportIfNeverRan returns early. The warning is only reachable when claudeBin is the direct-exec .exe/.com path or the posix claude, where execFileSync itself fails with status === null.

Net effect: on a claude.cmd host the "every removal became a silent no-op" case the PR is fixing still stays silent. The two installClaude tests that exercise the warning both run on linux, so nothing covers the shim path. If you want the warning to cover the shim too, the win32 .cmd case needs a cmd.exe-specific signal (e.g. treating exit 9009 as never-ran, or probing the resolved path with fs.existsSync before the removals) rather than relying on status === null.

The spawn-failure warning missed the host it was written for. On win32 a .cmd
claude is launched through cmd.exe, so execFileSync sees cmd.exe start
successfully and a shim it could not launch arrives as an exit code, not as a
null status - which the discriminator read as "claude ran and refused" and stayed
silent, exactly the silent no-op the warning exists to break.

cmd.exe reports a command it cannot find or launch as 9009, so that code counts
as never-ran, but only on the shim route: 9009 from a directly spawned claude.exe
is the executable's own exit code and stays a normal best-effort failure. The
reason string now carries the exit code when there is no errno to report.

Tests cover a 9009 through the shim warning, a 1 through the shim staying quiet,
and a 9009 from a .exe staying quiet.
Copilot AI review requested due to automatic review settings August 16, 2026 20:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@avifenesh
avifenesh merged commit 471c11b into main Aug 16, 2026
10 of 11 checks passed
@avifenesh
avifenesh deleted the fix/dev-install-claude-resolve branch August 16, 2026 22:18
@avifenesh avifenesh mentioned this pull request Aug 17, 2026
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.

2 participants