Skip to content

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

Description

@mimicofernandes-jpg

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.

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