JuliusBrussee/caveman · error

unsafe Windows command shim: ${executable}

Error message

unsafe Windows command shim: ${executable}

What it means

portableInvocation() in agents/delegate/portable-process.mjs rejects a .cmd/.bat shim on Windows when statSync shows it is not a regular file or exceeds 256 KiB. A shim that large or non-file does not match any legitimate npm-generated launcher, so the safe-spawn machinery treats it as untrusted input rather than executing it.

Source

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

    ? [command]
    : pathExt.split(";").map((extension) =>
      `${command}${extension.startsWith(".") ? extension : `.${extension}`}`);
  for (const directory of (envValue(env, "PATH") ?? "").split(";")) {
    if (!directory) continue;
    for (const name of names) {
      const candidate = join(directory, name);
      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,
  };
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Check the printed path: file type, size, and content — if you did not write it, treat it as suspect
  2. Clear the package manager cache and reinstall the package that owns the shim (npm cache clean plus fresh install)
  3. If the shim is legitimate but oversized, replace it with a direct node invocation on the real JS entrypoint

Example fix

# before
where npx   # points at a 2MB .bat

# after
del "C:\\path\\to\\suspect.bat" && npm install   # or bypass:
node .\\node_modules\\pkg\\bin\\cli.js
Defensive patterns

Strategy: type-guard

Validate before calling

const fs = require("node:fs");
function isSafeShim(p) {
  try {
    const st = fs.statSync(p);
    return st.isFile() && st.size <= 256 * 1024;
  } catch { return false; }
}

Type guard

function isTrustableCmdShim(executablePath) {
  if (!/\.(?:cmd|bat)$/i.test(executablePath)) return false;
  let st;
  try { st = fs.statSync(executablePath); } catch { return false; }
  return st.isFile() && st.size <= 256 * 1024;
}

Try / catch

if (!isTrustableCmdShim(cmd)) { spawn(process.execPath, [jsEntry, ...args]); return; }
try { spawn(...getSpawnInvocation(cmd, args)); }
catch (e) { /* reinstall the owning package */ }

Prevention

When it happens

Trigger: The resolved .cmd/.bat is a directory, a symlink or pipe, or a batch file over 256 KiB — a corrupted download, a data file misnamed .bat, or an intentionally bloated script placed on PATH.

Common situations: Corrupted npm cache producing garbage shims; PATH pollution where another program's oversized .bat shadows the intended command; malicious or hand-rolled batch scripts; filesystem damage after disk-full events.

Related errors


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