JuliusBrussee/caveman · error · Error

non-Node Windows command shim

Error message

non-Node Windows command shim: ${executable}

What it means

`portableInvocation` parses Windows .cmd/.bat shims looking for a Node script reference it can re-launch via process.execPath. If `parseWindowsNodeShim` cannot extract a relative script path from the shim's contents — the shim is not a recognizable Node wrapper — this error is thrown because running it through cmd.exe would be unsafe and there is no safe fallback.

Solutions

  1. Invoke the underlying tool directly instead of the shim (e.g. `python tool.py` or `node tool.js`)
  2. Regenerate the shim with npm so it is a standard Node wrapper (npm rebuild / reinstall)
  3. Read the .cmd yourself to find the real target and spawn that target via node
  4. Add a non-.cmd entry point for Windows in the package providing the tool

Example fix

// before
spawn("mytool", args, opts); // mytool.cmd is a custom batch file
// after
spawn(process.execPath, ["C:/proj/tools/mytool.js", ...args], opts);
Defensive patterns

Strategy: fallback

Validate before calling

import { readFileSync, statSync } from "node:fs";
function isNodeShim(p) {
  try {
    if (!/\.(?:cmd|bat)$/i.test(p)) return true;
    const s = statSync(p);
    return s.isFile() && s.size <= 256 * 1024 &&
      /\.js\b/i.test(readFileSync(p, "utf8"));
  } catch { return false; }
}

Try / catch

try {
  const inv = portableInvocation(command, args);
  spawn(inv.command, inv.args, opts);
} catch (e) {
  if (String(e.message).startsWith("non-Node Windows command shim:")) {
    // fall back to invoking the real target yourself
    spawn("node", [resolveRealEntry(command), ...args], opts);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling a Windows command whose resolved executable matches /\.(cmd|bat)$/i, is a regular file under 256KB, but whose text does not match the expected Node shim pattern (no "%~dp0\\...js" style target).

Common situations: A hand-written or vendor .cmd batch file that is not an npm-generated Node wrapper; a shim generated by a different tool (e.g. pnpm/yarn with different layout); a shim wrapping a non-Node executable (Python, shell).

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/11f0dc5509ba3c9c. Report an issue: GitHub.

Appendix: source

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

    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,
  platform = process.platform,
  taskkill = spawnSync,

View on GitHub (pinned to 3ee70a1026)