JuliusBrussee/caveman · error

cannot safely launch non-Node Windows command shim: ${execut

Error message

cannot safely launch non-Node Windows command shim: ${executable}

What it means

getSpawnInvocation() in the caveman-shrink MCP server refuses to run a .cmd/.bat shim on Windows unless it can prove the shim launches a Node script. It scans the shim source for the npm-generated pattern (node/_prog plus %* forwarding to a %~dp0 script target) and, finding none, throws rather than spawn the .bat directly — the direct spawn would be the CVE-2024-27980 command-injection vector Node itself blocks.

Source

Thrown at src/mcp-servers/caveman-shrink/spawn-options.js:52

      const candidate = path.join(directory, name);
      if (fs.existsSync(candidate)) return candidate;
    }
  }
  return null;
}

function getSpawnInvocation(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 source = fs.readFileSync(executable, 'utf8');
  let relativeScript = null;
  for (const line of source.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(`cannot safely launch non-Node Windows command shim: ${executable}`);
  const script = path.resolve(path.dirname(executable), ...relativeScript.split(/[\\/]+/));
  if (!fs.statSync(script).isFile()) throw new Error(`Windows command shim target is missing: ${script}`);
  return { command: process.execPath, args: [script, ...args] };
}

function getSpawnOptions(platform = process.platform) {
  return {
    stdio: ['pipe', 'pipe', 'inherit'],
    windowsHide: true,
  };
}

module.exports = { getSpawnInvocation, getSpawnOptions, resolveWindowsCommand };

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Point the MCP server config directly at node with the JS entrypoint as the first argument instead of the .cmd shim
  2. Reinstall the package with the npm version the shim template comes from so the pattern matches
  3. If you control the shim, regenerate it via npm (npm install/rebuild) so it uses the standard node/%~dp0 forwarding

Example fix

// before (mcp config)
{ "command": "npx", "args": ["caveman-shrink"] } // resolves to a .cmd shim on Windows

// after
{ "command": "node", "args": ["C:\\path\\to\\node_modules\\caveman-shrink\\bin\\cli.mjs"] }
Defensive patterns

Strategy: fallback

Validate before calling

const isWin = process.platform === "win32";
const command = isWin ? process.execPath : "npx";
const args = isWin ? [resolveToRealJsEntry()] : ["caveman-shrink"];

Try / catch

try {
  spawn(cmd, args);
} catch (e) {
  if (/cannot safely launch non-Node Windows command shim/.test(String(e?.message))) {
    spawn(process.execPath, [realJsEntryPoint, ...args]); // fallback: direct node invocation
  } else throw e;
}

Prevention

When it happens

Trigger: Configuring the MCP server command as npx/npm .cmd shims on win32 whose content does not match the standard npm shim template — a custom wrapper .bat, a shim generated by a non-npm package manager (pnpm/yarn custom wrappers), or an edited shim.

Common situations: Package managers generating differently-shaped shims; hand-written .bat wrappers around node scripts; older or newer npm emitting a template the regexes do not recognize.

Related errors


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