JuliusBrussee/caveman · error

non-Node Windows command shim: ${executable}

Error message

non-Node Windows command shim: ${executable}

What it means

portableInvocation() refuses a Windows .cmd/.bat shim whose source contains no recognizable Node-launch pattern (no node/node.exe/_prog line forwarding %* to a %~dp0 script). Like its caveman-shrink twin, this is the CVE-2024-27980 mitigation: arbitrary batch files are never spawned directly; only proven Node shims are unwrapped to a direct node invocation.

Source

Thrown at agents/delegate/portable-process.mjs:41

      if (existsSync(candidate)) return candidate;
    }
  }
  return null;
}

export function portableInvocation(command, args, platform = process.platform, env = process.env) {
  if (platform !== "win32") return { command, args: [...args] };
  const executable = resolveWindowsCommand(command, env) ?? command;
  if (!/\.(?:cmd|bat)$/i.test(executable)) return { command: executable, args: [...args] };
  const stat = statSync(executable);
  if (!stat.isFile() || stat.size > 256 * 1024) throw new Error(`unsafe Windows command shim: ${executable}`);
  let relativeScript = null;
  for (const line of readFileSync(executable, "utf8").split(/\r?\n/)) {
    if (!/(?:\bnode(?:\.exe)?\b|_prog)/i.test(line) || !/%\*/.test(line)) continue;
    const match = line.match(/"%(?:dp0%|~dp0)\\([^"\r\n]+\.(?:cjs|mjs|js))"\s+%\*/i);
    if (match) { relativeScript = match[1]; break; }
  }
  if (!relativeScript) throw new Error(`non-Node Windows command shim: ${executable}`);
  const script = resolve(dirname(executable), ...relativeScript.split(/[\\/]+/));
  if (!statSync(script).isFile()) throw new Error(`Windows command shim target missing: ${script}`);
  return { command: process.execPath, args: [script, ...args] };
}

export function delegateSpawnOptions(platform = process.platform) {
  return {
    detached: platform !== "win32",
    windowsHide: true,
  };
}

export async function killProcessTree(
  child,
  platform = process.platform,
  taskkill = spawnSync,
  kill = process.kill,
  graceMs = 1500,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Configure the delegate command to invoke node directly with the package's JS entrypoint as the argument
  2. If a native binary is intended, point the command at the .exe itself, not at a .bat wrapper
  3. Regenerate the shim with a current npm so it uses the standard pattern

Example fix

# before
{ "command": "some-cli.cmd", "args": [] }

# after (it is a Node CLI)
{ "command": "node", "args": ["C:\\...\\some-cli\\bin\\cli.js"] }
# after (it is native)
{ "command": "C:\\...\\some-cli.exe", "args": [] }
Defensive patterns

Strategy: fallback

Validate before calling

// Prefer direct-node invocation up front on Windows — avoids the shim path entirely
const useDirect = process.platform === "win32";

Try / catch

try { spawn(inv.command, inv.args); }
catch (e) {
  if (/non-Node Windows command shim/.test(String(e?.message))) {
    spawn(realExecutableExeOrNodeEntry, args); // it is native or non-standard — bypass the shim
  } else throw e;
}

Prevention

When it happens

Trigger: Delegate spawn targets a .cmd/.bat that wraps a non-Node binary (a native exe wrapper), or a batch file in a format npm does not generate (pnpm or yarn custom shims, hand-written launchers).

Common situations: Delegate configured to launch a CLI that ships a native binary wrapped in .bat; package-manager-specific shim formats; older npm templates that predate the _prog pattern.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/11f0dc5509ba3c9c. Report an issue: GitHub.