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
- Ensure the writer's stdout is an open pipe or file with a live consumer for the whole handoff
- Free disk space when redirecting output to a file on a full filesystem
- Let the reader keep stdout open until the secret line plus newline is consumed; only an early-exit BrokenPipe is OK
- 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
- Keep the reading process alive until it consumes the secret line and EOF
- Treat BrokenPipe as success; classify all other IO errors as delivery failure
- Do not close inherited stdout FDs in supervisors before handoff completes
- Check disk space when redirecting handoff output to files
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
- API key must be {MIN_API_KEY_BYTES}-{MAX_API_KEY_BYTES} UTF-
- {} has no supported external CLI credential source
- managed external credential access is unsupported in v0.9.1:
- external credential consent was not saved: non-interactive u
- external credential path escapes its absolute root: {}
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/d69aa2ada7ab21d9.
Report an issue: GitHub.