JuliusBrussee/caveman · error · Error

unsafe Windows command shim

Error message

unsafe Windows command shim: ${executable}

What it means

On Windows, `portableInvocation` resolves .cmd/.bat shims and, instead of executing them through cmd.exe (a command-injection and arg-mangling hazard), inspects the file to re-spawn node directly. It throws this when the shim file is not a regular file or exceeds 256KB — i.e., it fails the safety checks required to trust the shim's contents.

Solutions

  1. Inspect the reported shim path: verify it is a normal file and reasonably sized (dir /a, attrib)
  2. Reinstall the package that owns the .cmd shim (npm rebuild or npm ci) to restore a clean shim
  3. Remove the offending file from PATH if it is not a legitimate npm shim
  4. Pin a non-shim invocation (call node script.js directly) to bypass the .cmd entirely

Example fix

// before (corrupted/oversized shim at C:\proj\node_modules\.bin\tsc.cmd)
spawn("tsc", ["--build"], opts);
// after
spawn(process.execPath, ["C:/proj/node_modules/typescript/bin/tsc", "--build"], opts);
Defensive patterns

Strategy: try-catch

Validate before calling

import { statSync } from "node:fs";
function shimLooksSafe(p) {
  try { const s = statSync(p); return s.isFile() && s.size <= 256 * 1024; }
  catch { return false; }
}

Try / catch

try {
  const inv = portableInvocation(command, args);
  spawn(inv.command, inv.args, opts);
} catch (e) {
  if (String(e.message).startsWith("unsafe Windows command shim:")) {
    console.error(`Shim at ${e.message.split(": ")[1]} is corrupt/oversized; reinstall the owning package`);
  } else throw e;
}

Prevention

When it happens

Trigger: Spawning a Windows command whose resolved path ends in .cmd/.bat and either statSync reports it is not a regular file (directory, device, broken link) or its size is over 256 * 1024 bytes.

Common situations: A corrupted or hijacked npm shim (e.g. a .cmd replaced by a huge payload or symlinked to something odd) in node_modules/.bin; PATH resolution picking up an unexpected .cmd; antivirus or a bad install producing a non-regular file at the shim path.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/f205988b394d34fe. Report an issue: GitHub.

Appendix: source

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

export function parseWindowsNodeShim(source) {
  for (const line of source.split(/\r?\n/)) {
    if (!/(?:\bnode(?:\.exe)?\b|_prog)/i.test(line) || !/%\*/.test(line)) continue;
    // Shim-relative target (npm cmd-shim, pnpm/yarn-classic @zkochan forms), or
    // a drive-absolute target (pnpm emits one when the global bin dir and the
    // store sit on different drives — path.relative crosses drives as absolute).
    const match = line.match(/"%(?:dp0%|~dp0)\\([^"\r\n]+\.(?:cjs|mjs|js))"\s+%\*/i)
      || line.match(/"([A-Za-z]:[\\/][^"\r\n]+\.(?:cjs|mjs|js))"\s+%\*/i);
    if (match) return match[1];
  }
  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}`);
  const relativeScript = parseWindowsNodeShim(readFileSync(executable, "utf8"));
  if (!relativeScript) throw new Error(`non-Node Windows command shim: ${executable}`);
  const script = /^[A-Za-z]:[\\/]/.test(relativeScript)
    ? relativeScript
    : 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,

View on GitHub (pinned to 3ee70a1026)