Problem
claude --output-format json reports API / auth / model errors in its stdout JSON envelope ({is_error:true, api_error_status:404, result:"…"}) — on a non-zero exit too, with empty stderr. In createClaudeCodeAi.run the order is:
if (code !== 0) throw new Error(`claude_code_exit_${code}: ${redactSecrets(stderr…)}`); // wins first
const errStatus = claudeErrorStatus(stdout); // never reached on exit≠0
if (errStatus) throw new Error(`claude_code_error_${errStatus}`);
So when claude exits non-zero with the error in stdout, the exit-code throw wins and emits claude_code_exit_1: (blank — stderr is empty), discarding the precise status. This is exactly why #1610 (a claude --model claude-code 404) was undiagnosable from logs/Sentry and required deep in-container reproduction to root-cause. #1605 added stderr capture, but stderr is empty for this whole class of failure.
Fix
Check claudeErrorStatus(stdout) before the exit-code throw, so a structured error surfaces as claude_code_error_404 (precise, bounded, secret-free) instead of an opaque claude_code_exit_1: . A genuine crash (non-zero exit with no structured stdout envelope) still falls through to the exit code + redacted stderr. Regression test pins the non-zero-exit + stdout-envelope case.
Problem
claude --output-format jsonreports API / auth / model errors in its stdout JSON envelope ({is_error:true, api_error_status:404, result:"…"}) — on a non-zero exit too, with empty stderr. IncreateClaudeCodeAi.runthe order is:So when claude exits non-zero with the error in stdout, the exit-code throw wins and emits
claude_code_exit_1:(blank — stderr is empty), discarding the precise status. This is exactly why #1610 (aclaude --model claude-code404) was undiagnosable from logs/Sentry and required deep in-container reproduction to root-cause. #1605 added stderr capture, but stderr is empty for this whole class of failure.Fix
Check
claudeErrorStatus(stdout)before the exit-code throw, so a structured error surfaces asclaude_code_error_404(precise, bounded, secret-free) instead of an opaqueclaude_code_exit_1:. A genuine crash (non-zero exit with no structured stdout envelope) still falls through to the exit code + redacted stderr. Regression test pins the non-zero-exit + stdout-envelope case.