Vercel CLI detection always reports "not installed" on Windows (session-start-profiler)
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-official0.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\ PATHdoes 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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