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:
parent
c3c8a33e2c
commit
09b8f0e834
1 changed files with 233 additions and 45 deletions
|
|
@ -105,6 +105,9 @@ function fakeSpawn({ chunks = [], code = 0, error = null }) {
|
|||
return () => {
|
||||
const child = new EventEmitter();
|
||||
child.stdout = new EventEmitter();
|
||||
// The prompt is written to stdin rather than passed in argv, so the
|
||||
// stand-in needs one or `runPrompt` throws before the child can close.
|
||||
child.stdin = { end: () => {} };
|
||||
queueMicrotask(() => {
|
||||
if (error) {
|
||||
child.emit("error", error);
|
||||
|
|
@ -215,3 +218,47 @@ test("a negative case ignores skills that are not this repo's", () => {
|
|||
// With no repo set, any invocation still fails a negative case.
|
||||
assert.equal(sampleHit(null, foreign), false);
|
||||
});
|
||||
|
||||
test("runPrompt sends the prompt on stdin, never in argv", () => {
|
||||
// On Windows the CLI is a `.cmd` shim and can only start through a shell,
|
||||
// where `cmd.exe` re-parses any metacharacter in an argument as syntax. Off
|
||||
// Windows it also keeps the prompt out of the process table.
|
||||
const prompt = "does it? (yes & no)";
|
||||
let seen;
|
||||
let written = null;
|
||||
runPrompt(prompt, {
|
||||
spawnFn: (command, args, options) => {
|
||||
seen = { command, args, options };
|
||||
const c = new EventEmitter();
|
||||
c.stdout = new EventEmitter();
|
||||
c.stdin = {
|
||||
end: (text) => {
|
||||
written = text;
|
||||
},
|
||||
};
|
||||
queueMicrotask(() => c.emit("close", 0));
|
||||
return c;
|
||||
},
|
||||
platform: "linux",
|
||||
}).catch(() => {});
|
||||
assert.equal(seen.command, "claude");
|
||||
assert.ok(!seen.args.includes(prompt), "prompt must not appear in argv");
|
||||
assert.deepEqual(seen.options.stdio, ["pipe", "pipe", "inherit"]);
|
||||
assert.equal(written, prompt);
|
||||
});
|
||||
|
||||
test("runPrompt asks for a shell on Windows", () => {
|
||||
let seen;
|
||||
runPrompt("p", {
|
||||
spawnFn: (_c, _a, options) => {
|
||||
seen = options;
|
||||
const c = new EventEmitter();
|
||||
c.stdout = new EventEmitter();
|
||||
c.stdin = { end: () => {} };
|
||||
queueMicrotask(() => c.emit("close", 0));
|
||||
return c;
|
||||
},
|
||||
platform: "win32",
|
||||
}).catch(() => {});
|
||||
assert.equal(seen.shell, true);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue