Hmbown/CodeWhale · error

credential handoff could not write to stdout

Error message

credential handoff could not write to stdout

What it means

credential_handoff writes a resolved secret as a single line to a writer (stdout) so a parent process can capture it. Broken pipes are tolerated - if the reader exits early (e.g. `head`), that counts as success. This bail fires only when the write fails with a different IO error: closed descriptor, EIO, ENOSPC, or a device error, meaning the secret may not have been delivered.

Source

Thrown at crates/cli/src/credential_handoff.rs:71

    resolved
        .api_key
        .filter(|value| !value.trim().is_empty())
        .context("no usable runtime-effective API key")
}

pub(crate) fn handoff_secret_line(
    writer: &mut impl Write,
    stdout_is_terminal: bool,
    resolve: impl FnOnce() -> Result<String>,
) -> Result<()> {
    prepare_stdout(stdout_is_terminal)?;
    let secret = Zeroizing::new(resolve().map_err(|_| anyhow::anyhow!("unavailable credential"))?);
    ensure!(!secret.trim().is_empty(), "credential handoff was empty");
    let written = writeln!(writer, "{}", secret.as_str());
    if written.is_ok() || written.is_err_and(|error| error.kind() == ErrorKind::BrokenPipe) {
        return Ok(());
    }
    bail!("credential handoff could not write to stdout")
}
#[cfg(test)]
mod tests;

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Ensure the writer's stdout is an open pipe or file with a live consumer for the whole handoff
  2. Free disk space when redirecting output to a file on a full filesystem
  3. Let the reader keep stdout open until the secret line plus newline is consumed; only an early-exit BrokenPipe is OK
  4. Check the exit path of the consuming process and avoid closing inherited FDs before the child writes

Example fix

# before
codewhale credential-handoff ... > /full/disk/out.txt
# after
codewhale credential-handoff ... | consumer   # consumer reads the line and stays alive until EOF
Defensive patterns

Strategy: try-catch

Validate before calling

use std::io::{ErrorKind, Write};

fn write_secret_line(w: &mut impl Write, secret: &str) -> std::io::Result<()> {
    match writeln!(w, "{secret}") {
        Ok(()) => Ok(()),
        Err(e) if e.kind() == ErrorKind::BrokenPipe => Ok(()), // reader done early: fine
        Err(e) => Err(e),
    }
}

Try / catch

if let Err(error) = write_secret_line(&mut stdout, &secret) {
    if error.kind() != std::io::ErrorKind::BrokenPipe {
        anyhow::bail!("credential handoff could not write to stdout: {error}");
    }
}

Prevention

When it happens

Trigger: The consumer process closes stdout in a way that surfaces as EBADF/EIO rather than BrokenPipe; the target disk/tmpfs is full when stdout is a file or a process-substitution redirect; stdout was already closed by daemonization before the handoff call.

Common situations: Wrapper scripts using process substitution that close FDs early; running under a supervisor that closes inherited stdout; disk-full CI runners when redirecting to a log; double-close of a pipe FD in the parent.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/d69aa2ada7ab21d9. Report an issue: GitHub.