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.
Summary
On Windows, the
SessionStarthook always injectsIMPORTANT: 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 recommendnpm i -g vercelto a user who already has it. In my case it caused a wrong conclusion in an unrelated task before I checked the claim againstGet-Command vercel.Environment
vercel@claude-plugins-official0.45.1process.platform === "win32"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:
getBinaryPathCandidatesreturns the bare name first:and
resolveBinaryFromPath(~line 343) accepts the first candidate that passes:On Windows, Node treats
X_OKas equivalent toF_OK— any existing file passes. So the sh script always wins andvercel.cmdis never even tested.2.
execFileSynccannot run.cmd/.batwithoutshell: 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/.batthroughexecFile/spawnunlessshell: trueis set.Both failures land in the same
catch, which returns{ installed: false, needsUpdate: false }.Reproduction
Observed:
Adding
shell: trueto the.cmdcall succeeds and returns58.5.1.I also confirmed the PATH is not at fault: replicating
resolveBinaryFromPathverbatim in both PowerShell and Git Bash resolves toC:\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
b) Run
.cmd/.batthrough the shellNote that
shell: truewith an args array emitsDEP0190in recent Node. Since the args here are a fixed literal (--version), the risk is nil, but if you want to avoid the warning, invokeprocess.env.ComSpecexplicitly 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.tsonly 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.cmdandvercel.ps1,resolveBinaryFromPath("vercel")returns the.cmdonwin32.