vercel / vercel/vercel-plugin

SessionStart hook reports "Vercel CLI is not installed" on Windows + Node 22+ when CLI is installed

Open
#95 0 comments 0 reactions 0 assignees View on GitHub

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 vercel to C:\Users\<user>\AppData\Roaming\npm\
  • Shells confirmed working: PowerShell 7, Git Bash, Node child processes spawned directly

Reproduction

  1. 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)
  2. Confirm CLI works: vercel --version returns the version number from any shell.
  3. 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:

  1. 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];
    
  2. 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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.