Summary
The SessionStart hook (hooks/session-start-profiler.mjs) emits "IMPORTANT: The Vercel CLI is not installed" on Windows even when vercel --version works fine from PowerShell, Git Bash, and Node child processes. Root cause is in resolveBinaryFromPath() + checkVercelCli() and reproduces reliably on Windows 11 + Node 22+.
Environment
- Plugin version:
vercel@0.43.0 (claude-plugins-official cache)
- OS: Windows 11 Home 10.0.26200
- Node: v24.16.0
- Vercel CLI: 54.7.1, installed via
npm i -g vercel to C:\Users\<user>\AppData\Roaming\npm\
- Shells confirmed working: PowerShell 7, Git Bash, Node child processes spawned directly
Reproduction
- On Windows, install Vercel CLI globally:
npm i -g vercel. This produces three files in the npm global dir:
vercel (extensionless POSIX shell script with #!/bin/sh)
vercel.cmd (Windows batch wrapper)
vercel.ps1 (PowerShell wrapper)
- Confirm CLI works:
vercel --version returns the version number from any shell.
- Run the hook directly with a synthetic SessionStart payload + debug logging:
import { spawnSync } from "node:child_process";
const r = spawnSync(
process.execPath,
["<plugin>/hooks/session-start-profiler.mjs"],
{
input: JSON.stringify({ session_id: "x", hook_event_name: "SessionStart", source: "startup", cwd: "<your-vercel-project>" }),
env: { ...process.env, VERCEL_PLUGIN_HOOK_DEBUG: "1" },
encoding: "utf8",
}
);
console.log(r.stdout, r.stderr);
Expected
Hook detects the installed CLI and does not emit the "not installed" warning.
Actual
Hook emits the "not installed" warning. Debug stderr shows:
{"event":"session-start-profiler:vercel-version-check-failed",
"command":"C:\\Users\\<user>\\AppData\\Roaming\\npm\\vercel",
"args":"--version",
"error":{"name":"Error","message":"spawnSync ...\\vercel ENOENT","code":"ENOENT", ...}}
Root cause (two layers)
Layer 1 — wrong file resolved
resolveBinaryFromPath() (lines 198-231) builds candidate names with the empty suffix first, then PATHEXT extensions:
const suffixes = hasExecutableExtension ? [""] : ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];
On Windows, NTFS has no POSIX exec bit, so accessSync(path, X_OK) effectively reduces to F_OK. The extensionless vercel (bash script) file exists → passes the access check → wins. The function returns ...\\npm\\vercel instead of ...\\npm\\vercel.cmd.
Layer 2 — execFileSync can't launch what was returned
checkVercelCli() (line 262) then calls:
execFileSync(vercelBinary, VERCEL_VERSION_ARGS, { ... })
-
Extensionless bash script → ENOENT (Windows CreateProcessW requires a recognized extension).
-
Even if Layer 1 were fixed to prefer .cmd, execFileSync on .cmd/.bat files in Node ≥ 18.20.2 / 20.12.2 / 22.x also fails with EINVAL unless shell: true is set. This is the CVE-2024-27980 mitigation. Direct test:
execFileSync("C:\\Users\\Jaime\\AppData\\Roaming\\npm\\vercel.cmd", ["--version"])
// Error: spawnSync ... EINVAL
Proposed fix
Two single-line changes; either alone would fix the reported symptom, but both together are correct:
-
In resolveBinaryFromPath(), on Windows, put the empty suffix last instead of first — extensionless files should be a fallback, not a preference:
const suffixes = hasExecutableExtension
? [""]
: process.platform === "win32"
? [...WINDOWS_EXECUTABLE_EXTENSIONS, ""]
: ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];
-
In checkVercelCli(), pass shell: true on Windows so .cmd files launch correctly:
const raw = execFileSync(vercelBinary, VERCEL_VERSION_ARGS, {
timeout: EXEC_SYNC_TIMEOUT_MS,
encoding: "utf-8",
stdio: SPAWN_STDIO,
shell: process.platform === "win32",
}).trim();
Note: shell: true here is safe because VERCEL_VERSION_ARGS is a hardcoded literal ("--version") and vercelBinary came from accessSync on a PATH-resolved candidate, not user input. No command-injection surface.
The same patches should be applied wherever else the plugin runs binaries this way (e.g. npmBinary on line 276).
Impact
Cosmetic in terms of CLI usability (CLI itself works fine from any shell), but functionally the hook also fails to populate the rest of its env-var injection / context profile for the session because the profiler short-circuits on the failure. Every Claude Code session on Windows with the CLI installed sees a misleading warning recommending re-installing what's already installed.
Summary
The SessionStart hook (
hooks/session-start-profiler.mjs) emits "IMPORTANT: The Vercel CLI is not installed" on Windows even whenvercel --versionworks fine from PowerShell, Git Bash, and Node child processes. Root cause is inresolveBinaryFromPath()+checkVercelCli()and reproduces reliably on Windows 11 + Node 22+.Environment
vercel@0.43.0(claude-plugins-official cache)npm i -g verceltoC:\Users\<user>\AppData\Roaming\npm\Reproduction
npm i -g vercel. This produces three files in the npm global dir:vercel(extensionless POSIX shell script with#!/bin/sh)vercel.cmd(Windows batch wrapper)vercel.ps1(PowerShell wrapper)vercel --versionreturns the version number from any shell.Expected
Hook detects the installed CLI and does not emit the "not installed" warning.
Actual
Hook emits the "not installed" warning. Debug stderr shows:
Root cause (two layers)
Layer 1 — wrong file resolved
resolveBinaryFromPath()(lines 198-231) builds candidate names with the empty suffix first, thenPATHEXTextensions:On Windows, NTFS has no POSIX exec bit, so
accessSync(path, X_OK)effectively reduces toF_OK. The extensionlessvercel(bash script) file exists → passes the access check → wins. The function returns...\\npm\\vercelinstead of...\\npm\\vercel.cmd.Layer 2 —
execFileSynccan't launch what was returnedcheckVercelCli()(line 262) then calls:Extensionless bash script →
ENOENT(WindowsCreateProcessWrequires a recognized extension).Even if Layer 1 were fixed to prefer
.cmd,execFileSyncon.cmd/.batfiles in Node ≥ 18.20.2 / 20.12.2 / 22.x also fails withEINVALunlessshell: trueis set. This is the CVE-2024-27980 mitigation. Direct test:Proposed fix
Two single-line changes; either alone would fix the reported symptom, but both together are correct:
In
resolveBinaryFromPath(), on Windows, put the empty suffix last instead of first — extensionless files should be a fallback, not a preference:In
checkVercelCli(), passshell: trueon Windows so.cmdfiles launch correctly:Note:
shell: truehere is safe becauseVERCEL_VERSION_ARGSis a hardcoded literal ("--version") andvercelBinarycame fromaccessSyncon aPATH-resolved candidate, not user input. No command-injection surface.The same patches should be applied wherever else the plugin runs binaries this way (e.g.
npmBinaryon line 276).Impact
Cosmetic in terms of CLI usability (CLI itself works fine from any shell), but functionally the hook also fails to populate the rest of its env-var injection / context profile for the session because the profiler short-circuits on the failure. Every Claude Code session on Windows with the CLI installed sees a misleading warning recommending re-installing what's already installed.