SessionStart hook reports "Vercel CLI is not installed" on Windows + Node 22+ when CLI is installed
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 287
- Forks
- 58
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 17
Description
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 verceltoC:\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 --versionreturns 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(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: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(), passshell: trueon Windows so.cmdfiles 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: 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. 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.
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/session-start-profiler.mjs at resolveBinaryFromPath() and checkVercelCli(), then inspect the npmBinary call around line 276 for the same launch behavior. Run the supplied synthetic SessionStart reproduction on Windows; done means the .cmd CLI is detected, its version check succeeds, no installation warning appears, and profiling continues.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- cli, operating-systems, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100