JuliusBrussee/caveman · error · Error

ENOENT

ENOENT

Error message

command not found: ${command}

What it means

Thrown by portableProcessInvocation() when running on win32 and resolveWindowsCommand() cannot locate the requested command (via PATH lookup). It carries code ENOENT so callers can treat it exactly like a standard spawn ENOENT. The indirection exists because spawning .cmd/.bat shims directly is unsafe on Windows (CVE-2024-27980 class of issues), so the library resolves the shim manually before spawning.

Source

Thrown at packages/subagent-tax/lib/process-tree.mjs:45

}

export function parseWindowsNodeShim(source) {
  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) return match[1];
  }
  return null;
}

export function portableProcessInvocation(
  command,
  args,
  { platform = process.platform, env = process.env, execPath = process.execPath } = {},
) {
  if (platform !== "win32") return { command, args: [...args] };
  const executable = resolveWindowsCommand(command, env);
  if (!executable) throw Object.assign(new Error(`command not found: ${command}`), { code: "ENOENT" });
  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(`cannot safely launch Windows command shim: ${executable}`);
  }
  const relativeScript = parseWindowsNodeShim(readFileSync(executable, "utf8"));
  if (!relativeScript) {
    throw new Error(`cannot safely launch non-Node Windows command shim: ${executable}`);
  }
  const script = resolve(dirname(executable), ...relativeScript.split(/[\\/]+/));
  if (!statSync(script).isFile()) throw new Error(`Windows command shim target is missing: ${script}`);
  return { command: execPath, args: [script, ...args] };
}

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

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Verify the command is actually installed and on PATH: run `where <command>` in the same environment.
  2. If you pass a custom env option, include a valid PATH (spread process.env or set env.PATH explicitly) so the resolver can find the executable.
  3. If the command is only available via a package-manager shim directory, add that directory to PATH or pass the absolute path to the executable as command.

Example fix

// before
const inv = portableProcessInvocation("claude", [], { platform: "win32", env: { SYSTEM_ROOT: "C:" } }); // throws ENOENT

// after
const inv = portableProcessInvocation("claude", [], { platform: "win32", env: process.env });
Defensive patterns

Strategy: validation

Validate before calling

import { accessSync, constants } from "node:fs";
function commandResolvable(command, env, platform) {
  if (platform !== "win32") return true;
  const path = env.PATH ?? "";
  return path.split(";").some((dir) => {
    try { accessSync(join(dir, command + ".cmd"), constants.X_OK); return true; }
    catch { return false; }
  });
}

Try / catch

try {
  const inv = portableProcessInvocation(cmd, args, { platform: "win32" });
} catch (err) {
  if (err?.code === "ENOENT") {
    // report 'CLI not installed / not on PATH' to the user, suggest install instructions
  } else throw err;
}

Prevention

When it happens

Trigger: Calling portableProcessInvocation("some-cli", args) with platform "win32" (or on a real Windows host) where some-cli is not installed, not on the PATH contained in the env object passed in, or is on PATH but resolveWindowsCommand's lookup rules miss it.

Common situations: The agent binary or harness CLI is not installed on the Windows machine; the env passed to the function was constructed from a minimal env without PATH; the command is installed for a different shell/user profile; the PATH entry uses a quoting style the resolver rejects.

Related errors


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