Fix: keep the node runtime probe clear of cmd.exe metacharacters (#458)

Volta's Windows shims exec through `cmd /C`, which re-parses the argument
list, so the `>=` inside the probe's `node -e` payload was read as output
redirection. The command died with "The filename, directory name, or volume
label syntax is incorrect" before node started, the guard read that as a
missing runtime, and the hook it exists to protect was disabled on every
PostToolUse and Stop. A user on a supported Node 24 got a one-time notice
telling them to install Node 22, then silence.

Clamping with Math.min is the same floor test in the same ES5-only syntax,
with no character cmd.exe can claim. Verified through the Volta shim on Node
24.16.0 and 22.18.0 (exit 0) and against a real Node 20.6.1 binary (exit 1),
so the floor is unchanged. Adds a regression test asserting no `<`, `>`, or
newline reaches any generated `node -e` payload.

Upstream cause: volta-cli/volta#1791.

Prepared with AI assistance (Claude Code).
This commit is contained in:
Rex Lorenzo
2026-08-03 15:02:29 -07:00
committed by GitHub
parent e2761cae80
commit 14d2641685
8 changed files with 26 additions and 12 deletions
+7 -1
View File
@@ -72,7 +72,13 @@ const NODE_MAJOR_FLOOR = 22;
// renders only on DENY, so warning would block the edit -> probe only
// Grok Build: PostToolUse/Stop stdout is ignored outright -> probe only
// Copilot: output contract unconfirmed; do not guess a shape -> probe only
const NODE_PROBE = `node -e "process.exit(parseInt(process.versions.node,10)>=${NODE_MAJOR_FLOOR}?0:1)" 2>/dev/null`;
//
// The clamp avoids `<` and `>` deliberately: Volta's Windows shims run through
// `cmd /C`, which reads an angle bracket in the `-e` payload as redirection, so
// `>=` failed before node ran at all and the guard reported a missing runtime on
// a machine that had a supported one (volta-cli/volta#1791). Newlines break the
// same way, so this payload also has to stay on one line.
const NODE_PROBE = `node -e "process.exit(Math.min(parseInt(process.versions.node,10),${NODE_MAJOR_FLOOR})===${NODE_MAJOR_FLOOR}?0:1)" 2>/dev/null`;
const guardedNode = (hookPath, notice = '') => {
const probe = notice
? `! { ${NODE_PROBE} || { ${notice}; exit 0; }; }`