{"record":{"id":"593ce0f3a6101ac1","repo":"JuliusBrussee/caveman","slug":"failed-to-exec-bin-error-as-error-message","errorCode":null,"errorMessage":"failed to exec ${bin}: ${(error as Error).message}","messagePattern":"failed to exec (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":5094,"sourceCode":"        opts.pixelModels,\n        opts.pixelDensity,\n        gw,\n        subscription ? \"codex-subscription\" : \"standard\",\n        false,\n      );\n    }\n  } catch { /* runtime startup is fail-open; the native hook retries at SessionStart */ }\n  if (native === \"hermes\") maybeWarnHermesMissingKey(agent, gatewayURL());\n  const stopProxyKeepalive = startProxyKeepalive();\n  const code = await new Promise<number>((resolve, reject) => {\n    const invocation = portableInvocation(bin, [...agent.args, ...rest.slice(1)]);\n    let child: ReturnType<typeof spawn>;\n    try {\n      child = spawn(invocation.command, invocation.args, { stdio: \"inherit\" });\n    } catch (error) {\n      // macOS reports some exec failures (e.g. ENOEXEC) synchronously — wrap\n      // them like the async 'error' path so the message names the binary.\n      throw new Error(`failed to exec ${bin}: ${(error as Error).message}`);\n    }\n    // tty-generated signals (Ctrl+C / Ctrl+\\) already reach the child through\n    // the shared foreground group — forwarding would double-deliver them. But\n    // process-directed SIGTERM/SIGHUP (timeout(1), supervisors, pkill) only hit\n    // this launcher, so those must be forwarded. Either way the launcher\n    // re-raises on itself after the child exits so callers see a signal death,\n    // not a clean exit.\n    let fatal: NodeJS.Signals | undefined;\n    for (const signal of [\"SIGINT\", \"SIGQUIT\"] as NodeJS.Signals[]) {\n      process.on(signal, () => { fatal = signal; /* the tty delivered it to the child already */ });\n    }\n    for (const signal of [\"SIGHUP\", \"SIGTERM\"] as NodeJS.Signals[]) {\n      process.on(signal, () => {\n        fatal = signal;\n        child.kill(signal);\n        const grace = setTimeout(() => child.kill(\"SIGKILL\"), 10_000);\n        grace.unref();\n      });","sourceCodeStart":5076,"sourceCodeEnd":5112,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L5076-L5112","documentation":"This error wraps a synchronous exception from Node's spawn() when launching an external binary. On macOS some exec failures (notably ENOEXEC, a file that exists but is not executable) are reported synchronously instead of via the child's async 'error' event, so the launcher catches them and re-throws with the binary name and underlying OS message so failures name the tool instead of surfacing a bare spawn error.","triggerScenarios":"Running a CLI subcommand that resolves `bin` and calls spawn(invocation.command, invocation.args, {stdio:\"inherit\"}) where the resolved path is not executable (wrong interpreter/shebang, ENOEXEC), the file exists but lacks +x permission, or the platform reports the exec failure synchronously rather than through the child 'error' event.","commonSituations":"A PATH-resolved `claude`/`codex`/`pi` shim that is a text file without a shebang; a binary downloaded without the executable bit set; a broken wrapper script after a version upgrade; macOS-only synchronous spawn behavior that works on Linux.","solutions":["Make the target binary executable: chmod +x $(which <bin>) and verify it runs standalone","Inspect the OS message after the colon (e.g. EACCES vs ENOEXEC) and fix accordingly — permission vs format","If it is a script without a shebang, add one (e.g. #!/usr/bin/env node) or reinstall the tool","Reinstall or reinstall via its package manager to restore a valid launcher shim","Check PATH for shadowing: an earlier directory may contain a stale binary of the same name"],"exampleFix":"// before (broken shim without shebang)\n$ cat $(which claude)\nconsole.log('cli')\n// after\n$ head -1 $(which claude)\n#!/usr/bin/env node","handlingStrategy":"try-catch","validationCode":"import { accessSync, constants } from 'fs';\nfunction isExecutable(p) {\n  try { accessSync(p, constants.F_OK | constants.X_OK); return true; } catch { return false; }\n}\n// verify isExecutable(resolvedBin) before invoking the CLI subcommand","typeGuard":"function isSpawnExecError(err: unknown): err is Error & { code?: string } {\n  return err instanceof Error && /failed to exec /.test(err.message);\n}","tryCatchPattern":"try {\n  await cli.run(args);\n} catch (err) {\n  if (/^failed to exec /.test((err as Error).message)) {\n    const bin = (err as Error).message.split(' ')[3];\n    console.error(`Binary '${bin}' is missing or not executable; check PATH and +x permission.`);\n    process.exitCode = 127;\n  } else throw err;\n}","preventionTips":["Verify target binaries exist and are executable (chmod +x) before launching","Test harness binaries directly in the shell before routing them through the CLI","Watch for macOS-only ENOEXEC failures when wrappers lack shebangs","Pin tool versions and reinstall after upgrades to keep shims valid"],"tags":["spawn","process-execution","macos","enoexec"],"backgroundTag":"command-not-found","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}