diff --git a/src/agents/bash-tools.exec-runtime.ts b/src/agents/bash-tools.exec-runtime.ts index e342df6232b..f9dbaab1e19 100644 --- a/src/agents/bash-tools.exec-runtime.ts +++ b/src/agents/bash-tools.exec-runtime.ts @@ -482,7 +482,13 @@ export async function runExecProcess(opts: { .then((exit): ExecProcessOutcome => { const durationMs = Date.now() - startedAt; const isNormalExit = exit.reason === "exit"; - const status: "completed" | "failed" = isNormalExit ? "completed" : "failed"; + const exitCode = exit.exitCode ?? 0; + // Shell exit codes 126 (not executable) and 127 (command not found) are + // unrecoverable infrastructure failures that should surface as real errors + // rather than silently completing — e.g. `python: command not found`. + const isShellFailure = exitCode === 126 || exitCode === 127; + const status: "completed" | "failed" = + isNormalExit && !isShellFailure ? "completed" : "failed"; markExited(session, exit.exitCode, exit.exitSignal, status); maybeNotifyOnExit(session, status); @@ -491,7 +497,6 @@ export async function runExecProcess(opts: { } const aggregated = session.aggregated.trim(); if (status === "completed") { - const exitCode = exit.exitCode ?? 0; const exitMsg = exitCode !== 0 ? `\n\n(Command exited with code ${exitCode})` : ""; return { status: "completed", @@ -502,8 +507,11 @@ export async function runExecProcess(opts: { timedOut: false, }; } - const reason = - exit.reason === "overall-timeout" + const reason = isShellFailure + ? exitCode === 127 + ? "Command not found" + : "Command not executable (permission denied)" + : exit.reason === "overall-timeout" ? `Command timed out after ${opts.timeoutSec} seconds` : exit.reason === "no-output-timeout" ? "Command timed out waiting for output"