vercel / vercel/vercel-plugin

Vercel CLI detection always reports "not installed" on Windows (session-start-profiler)

Open
#140 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
287
Forks
58
Avg merge
1d 1h
Merged PRs (30d)
17

Description

Summary

On Windows, the SessionStart hook always injects IMPORTANT: The Vercel CLI is not installed. even when the CLI is installed and working. The detection has two independent failures, either of which alone is enough to break it.

The impact is not cosmetic: the model is told a capability is unavailable when it isn't. It will avoid vercel env pull, vercel deploy, vercel logs, and will recommend npm i -g vercel to a user who already has it. In my case it caused a wrong conclusion in an unrelated task before I checked the claim against Get-Command vercel.

Environment

  • Plugin: vercel@claude-plugins-official 0.45.1
  • OS: Windows 11 (26200), process.platform === "win32"
  • Node: v25.9.0
  • Vercel CLI: 58.5.1, installed globally via npm at C:\Users\mimic\AppData\Roaming\npm\
  • PATH does contain …\AppData\Roaming\npm (verified in both PowerShell and Git Bash)

Root cause

hooks/src/session-start-profiler.mts:

1. Candidate order puts the non-executable shim first (getBinaryPathCandidates, ~line 333)

npm installs three shims for a global CLI on Windows:

vercel        <- POSIX sh script (for Git Bash / Cygwin), NOT runnable by Windows
vercel.cmd    <- the one Windows can execute
vercel.ps1

getBinaryPathCandidates returns the bare name first:

const suffixes = hasExecutableExtension ? [""] : ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];
// -> ["vercel", "vercel.COM", "vercel.EXE", "vercel.BAT", "vercel.CMD", ...]

and resolveBinaryFromPath (~line 343) accepts the first candidate that passes:

accessSync(candidatePath, fsConstants.X_OK);

On Windows, Node treats X_OK as equivalent to F_OK — any existing file passes. So the sh script always wins and vercel.cmd is never even tested.

2. execFileSync cannot run .cmd/.bat without shell: true (checkVercelCli, ~line 407)

Even if resolution returned vercel.cmd, the version check would still fail. Since the batch-injection fix (CVE-2024-27980), Node refuses to spawn .cmd/.bat through execFile/spawn unless shell: true is set.

Both failures land in the same catch, which returns { installed: false, needsUpdate: false }.

Reproduction

// node repro.mjs — on Windows, with the Vercel CLI installed globally via npm
import { execFileSync } from "node:child_process";
const base = "C:\\Users\\mimic\\AppData\\Roaming\\npm\\";
for (const name of ["vercel", "vercel.cmd", "vercel.ps1"]) {
  try {
    const out = execFileSync(base + name, ["--version"], {
      encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 20000,
    }).trim();
    console.log("OK   ", name, "->", out);
  } catch (e) {
    console.log("FAIL ", name, "->", e.code, e.message.split("\n")[0]);
  }
}

Observed:

FAIL  vercel      -> ENOENT   spawnSync ...\npm\vercel ENOENT
FAIL  vercel.cmd  -> EINVAL   spawnSync ...\npm\vercel.cmd EINVAL
FAIL  vercel.ps1  -> EFTYPE   spawnSync ...\npm\vercel.ps1 EFTYPE

Adding shell: true to the .cmd call succeeds and returns 58.5.1.

I also confirmed the PATH is not at fault: replicating resolveBinaryFromPath verbatim in both PowerShell and Git Bash resolves to C:\Users\mimic\AppData\Roaming\npm\vercel — detection finds the binary, it just can't execute it.

Suggested fix

Both changes are needed; either alone leaves it broken.

a) Prefer extensioned candidates on Windows

const suffixes = hasExecutableExtension
  ? [""]
  : process.platform === "win32"
    ? [...WINDOWS_EXECUTABLE_EXTENSIONS, ""]   // extensions first; bare name last
    : [""];

b) Run .cmd/.bat through the shell

const needsShell = /\.(cmd|bat)$/i.test(vercelBinary);
const raw = execFileSync(vercelBinary, VERCEL_VERSION_ARGS, {
  timeout: EXEC_SYNC_TIMEOUT_MS,
  encoding: "utf-8",
  stdio: SPAWN_STDIO,
  shell: needsShell,
});

Note that shell: true with an args array emits DEP0190 in recent Node. Since the args here are a fixed literal (--version), the risk is nil, but if you want to avoid the warning, invoke process.env.ComSpec explicitly with ["/d", "/s", "/c", "${bin}" --version].

Consider also treating "resolved but unexecutable" as distinct from "not installed" — the current code collapses both into the same message, which is what makes this so confusing to diagnose.

Test coverage gap

hooks/session-start-profiler-platform.test.ts only covers editor detection (Claude Code vs Cursor). There is no test for Windows binary resolution or for the version check, which is why this passes CI while being broken for every Windows user with an npm-installed CLI.

A regression test could assert that, given a directory containing vercel, vercel.cmd and vercel.ps1, resolveBinaryFromPath("vercel") returns the .cmd on win32.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in hooks/src/session-start-profiler.mts by reading getBinaryPathCandidates, resolveBinaryFromPath, and checkVercelCli, then review the Windows behavior described in the reproduction. Add regression coverage in hooks/session-start-profiler-platform.test.ts for resolving and checking a Windows npm-installed CLI. Done means an installed Vercel CLI is detected and its version check succeeds on Windows.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
cli, tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.