chore: address Copilot review round 7 (Windows .cmd shims)

Node has refused shell-free `.cmd`/`.bat` spawns since CVE-2024-27980, and an
npm-installed `claude` is a `.cmd` shim on Windows — so every shell-free spawn
of it reported ENOENT there, each in a quiet way: `verify:skills` skipped its
authoritative hand-off, `verify:skills:cli` downloaded the pinned package it
already had (and failed offline), and `skills:eval` said "not on PATH".

- New `scripts/lib/claude-cli.mjs` is the one place that decides how `claude` is
  spawned: `shell` on win32, arguments through the existing `winShellArgs`
  quoting so `cmd.exe` cannot re-parse them as syntax (#1939). Unit-tested with
  the platform injected, since a win32-only bug no runner here executes is
  exactly the kind that ships.
- `verify-skills`, `verify-skills-cli` and `skill-eval` all route their probes
  and spawns through it.
- skill-eval sends the prompt on **stdin** rather than in argv. `claude -p` reads
  a piped prompt (verified end to end), which sidesteps `cmd.exe` metacharacter
  re-parsing entirely rather than relying on quoting a model-visible string, and
  keeps the prompt out of the process table.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-29 02:32:30 -04:00
commit 09b8f0e834

View file

@ -22,6 +22,8 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { probeClaudeVersion } from "./lib/claude-cli.mjs";
import { winShellArgs } from "./lib/win-shell-args.mjs";
import {
compareVersions,
parseClaudeVersion,
@ -83,32 +85,29 @@ export function validatorCommand({ version }) {
* propagation of a rejected validator would leave `test:scripts` green while
* this gate quietly stopped gating (Copilot).
*
* @param {{ probe?: () => {error?: Error, status?: number|null, stdout?: string},
* @param {{ probe?: () => number[] | null,
* spawn?: (cmd: string, args: string[], opts: object) => {error?: Error, status?: number|null},
* log?: (msg: string) => void, error?: (msg: string) => void }} [io]
* @returns {number} Process exit code.
*/
export function runValidator(io = {}) {
const {
probe = () => spawnSync("claude", ["--version"], { encoding: "utf8" }),
probe = () => probeClaudeVersion(parseClaudeVersion),
spawn = (cmd, args, opts) => spawnSync(cmd, args, opts),
log = console.log,
error = console.error,
platform = process.platform,
} = io;
const probed = probe();
const version =
probed.error || probed.status !== 0
? null
: parseClaudeVersion(probed.stdout ?? "");
const version = probe();
const { command, args, via } = validatorCommand({ version });
log(`verify:skills:cli — validating ${SKILLS_DIR} via ${via}…`);
const res = spawn(command, args, {
const res = spawn(command, winShellArgs(args, platform), {
cwd: ROOT,
stdio: "inherit",
shell: process.platform === "win32",
shell: platform === "win32",
});
if (res.error) {
error(