JuliusBrussee/caveman · error · Error
ENOENT
ENOENT
Error message
command not found: ${command} What it means
portableProcessInvocation normalizes a command+args pair for cross-platform spawning. On Windows it resolves the bare command name (e.g. 'node') to an absolute executable via resolveWindowsCommand; if nothing resolvable is found it throws an ENOENT-tagged error because spawning would fail anyway.
Solutions
- Install the missing executable or add its directory to PATH so resolveWindowsCommand can find it
- Pass an absolute path to the executable instead of a bare command name
- Verify the env option passed to portableProcessInvocation includes a usable PATH/PATHEXT (it defaults to process.env)
- On non-Windows platforms this branch never fires; check that platform detection is correct if you expected POSIX behavior
Example fix
// before
await spawnTree({ command: "node", args: ["agent.mjs"] }); // throws on Windows without node on PATH
// after
const node = process.execPath; // guaranteed absolute path to node
await spawnTree({ command: node, args: ["agent.mjs"], env: process.env }); Defensive patterns
Strategy: validation
Validate before calling
import { statSync } from "node:fs";
function assertCommandAvailable(command, env = process.env) {
if (process.platform !== "win32" || Path.isAbsolute(command)) return;
const found = env.PATH?.split(";").some((dir) => {
try { statSync(`${dir}\\${command}.cmd`); return true; } catch { return false; }
});
if (!found) throw new Error(`command not on PATH: ${command}`);
} Type guard
function isExecutableAvailable(p) {
try { return statSync(p).isFile(); } catch { return false; }
} Try / catch
try {
const resolved = await portableProcessInvocation({ command, args });
} catch (err) {
if (err.code === "ENOENT") {
console.error(`Executable '${command}' not found; install it or add it to PATH`);
}
throw err;
} Prevention
- Use absolute paths (process.execPath for node) instead of bare command names when spawning
- Verify required CLIs are installed in CI images and Dockerfiles
- Never construct a stripped env object without PATH for spawned processes
- Pre-flight check executables with `where`/`which` during setup
When it happens
Trigger: Calling portableProcessInvocation (or the process-tree launcher built on it) on win32 with a command name that is not on PATH, not resolvable from env (e.g. missing PATHEXT/NODE_PATH style hints), and not an absolute path.
Common situations: Running a spawned subagent/tool on a Windows CI runner or dev box where the CLI dependency (node, npm, npx, git) is not installed or not on PATH; using a custom env object that dropped PATH; typos in the configured binary name.
Related errors
- cannot safely launch non-Node Windows command shim
- cannot safely launch Windows command shim
- cave_transform_registry_unavailable: run caveman setup or…
- non-Node Windows command shim
- unsafe Windows command shim
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/f5e3a040af9d3ad0.
Report an issue: GitHub.
Appendix: source
Thrown at packages/subagent-tax/lib/process-tree.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 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 = /^[A-Za-z]:[\\/]/.test(relativeScript)
? relativeScript
: 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 {View on GitHub (pinned to 3ee70a1026)