Skip to content

Claude Code driver hides structured stdout errors on nonzero exit #5712

Description

@dingjianhui123-create

Summary

When Claude Code emits a structured error on stdout and exits nonzero, the driver reports only stderr. If stderr is empty, OpenHuman loses the actionable provider error and shows a generic failure such as exit Some(1) stderr=.

Problem

Environment

  • OpenHuman 0.63.12 desktop
  • Windows 11 Pro 10.0.26200 (x64)
  • Claude Code CLI 2.1.207

Reproduction evidence

During a failed Claude Code turn, Claude's structured output contained:

Failed to authenticate. API Error: 403 Request not allowed
error=authentication_failed

The corresponding OpenHuman log and user-visible error retained only:

[claude-code][driver] exit Some(1) stderr=

The current driver awaits stderr, then bails on !status.success() before checking mapper.error:

let stderr_text = stderr_task.await.unwrap_or_default();
if !status.success() {
anyhow::bail!(
"[claude-code][driver] exit {:?} stderr={}",
status.code(),
stderr_text.trim()
);
}
if let Some(err) = mapper.error.clone() {
anyhow::bail!("[claude-code][driver] {}", err);

The event mapper already has a dedicated structured error field:

pub error: Option<String>,
pub session_id: Option<String>,
pub finished: bool,
}
impl EventMapper {
pub fn new() -> Self {
Self::default()
}
/// Process one event and return the deltas to forward to the stream
/// sink (if any).
pub fn handle(&mut self, event: ClaudeCodeEvent) -> Vec<ProviderDelta> {
match event {
ClaudeCodeEvent::System { session_id, .. } => {
if let Some(id) = session_id {
self.session_id = Some(id);
}
Vec::new()
}
ClaudeCodeEvent::Error { message } => {
self.error = Some(message);
Vec::new()
}
ClaudeCodeEvent::Result {
subtype,
usage,
total_cost_usd,
..
} => {
let mut parsed = usage.as_ref().map(parse_usage);
// CC stream emits `total_cost_usd` on the terminal `result`
// event — surface it as `UsageInfo.charged_amount_usd` so
// downstream cost.rs can record it without re-pricing
// tokens × model rates.
if let Some(cost) = total_cost_usd {
let usage = parsed.get_or_insert_with(UsageInfo::default);
usage.charged_amount_usd = cost;
}
self.usage = parsed;
if subtype.as_deref() == Some("error") && self.error.is_none() {
self.error = Some("claude reported `result.subtype=error`".into());

Expected behavior

When a nonzero exit also has a parsed structured provider error, OpenHuman should surface a sanitized, actionable version of that error. Stderr can remain a fallback for process-level failures where no structured error exists.

Solution

Prefer mapper.error on nonzero exit, then fall back to bounded/sanitized stderr. Avoid logging raw payloads, credentials, headers, temporary MCP configuration, or account identifiers.

Acceptance criteria

  • A nonzero Claude process exit with mapper.error = Some(...) returns the structured error rather than a blank stderr message.
  • A nonzero exit without a structured error still reports useful bounded stderr.
  • Tests cover both branches and verify that secrets are not included in logs or UI errors.
  • The existing successful-response path remains unchanged.

Related

Activity

  1. senamakel commented on Oct 9, 2026

    @senamakel
    Member

    Triage: valid — Current review confirms this is a concrete, in-scope engineering issue or request: “Claude Code driver hides structured stdout errors on nonzero exit”. No complete resolution is evident in current main.

  2. added theissue type on Oct 9, 2026
  3. self-assigned this
    on Oct 9, 2026
  4. senamakel commented on Oct 9, 2026

    @senamakel
    Member

    Still reproducible in the current code, but the fix has to land in TinyAgents first.

    The Claude Code driver no longer lives in this repository. It moved to the vendored TinyAgents harness, at crates/tinyagents-harness/src/providers/claude_code/driver.rs. On both the commit OpenHuman pins today (vendor/tinyagents at 33a86887d5 on main) and the current TinyAgents main (b518ea77fb), lines 611-620 still check !status.success() and bail with exit {:?} stderr=... before they look at mapper.error. A nonzero exit with empty stderr therefore still loses the structured stdout error, as reported.

    What a fix needs:

    • In TinyAgents, when the exit is nonzero, return mapper.error (sanitized and bounded) if it is set, and fall back to bounded stderr only when it is not. Add driver_tests.rs cases for both branches and one that checks secrets stay out of the message.
    • In OpenHuman, move the vendor/tinyagents gitlink to that commit once it merges.

    Related: tinyhumansai/tinyagents#360 (for #5648) touches the same block. It retries once on a missing resumed session but does not change the stderr-first ordering, so it does not fix this issue. Leaving this open to track the work.

  5. added a commit that references this issue on Oct 10, 2026
    72c61ea
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

source: externalIssue opened by an outside contributortriage: validReviewed and confirmed as a valid actionable issue

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions