Hmbown/CodeWhale · error

interactive key entry requires a terminal; use `--api-key-st

Error message

interactive key entry requires a terminal; use `--api-key-stdin` for piped input

What it means

Hidden (masked) API-key entry requires an interactive terminal on stdin, because it renders a secure no-echo prompt via console::Term. This bail fires when the key-add/login path falls back to the hidden prompt while stdin is not a TTY - typically in pipes, scripts, and CI - and tells you to use the explicit stdin mode instead.

Source

Thrown at crates/cli/src/cloud.rs:974

        .take(MAX_API_KEY_STDIN_BYTES + 1)
        .read_to_end(&mut bytes)
        .context("failed to read API key from stdin")?;
    parse_key_input(bytes)
}

fn parse_key_input(bytes: Vec<u8>) -> Result<String> {
    if bytes.len() as u64 > MAX_API_KEY_STDIN_BYTES {
        bail!("API key input is unexpectedly large");
    }
    let value = String::from_utf8(bytes).context("API key from stdin is not valid UTF-8")?;
    let value = value.trim().to_string();
    validate_api_key(&value)?;
    Ok(value)
}

fn read_key_hidden(provider: &str) -> Result<String> {
    if !io::stdin().is_terminal() {
        bail!("interactive key entry requires a terminal; use `--api-key-stdin` for piped input");
    }
    let term = console::Term::stderr();
    term.write_str(&format!("Enter {provider} API key: "))
        .context("failed to write API key prompt")?;
    let value = term
        .read_secure_line()
        .context("failed to read API key securely")?;
    term.write_line("").ok();
    let value = value.trim().to_string();
    validate_api_key(&value)?;
    Ok(value)
}

fn json_body(value: &impl Serialize) -> Result<Vec<u8>> {
    serde_json::to_vec(value).context("failed to encode Codewhale account request")
}

fn expect_json<T: DeserializeOwned>(response: CloudResponse, statuses: &[u16]) -> Result<T> {

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Use the explicit pipe mode: printf '%s' "$KEY" | codewhale ... --api-key-stdin
  2. Or set the provider's env var (e.g. DEEPSEEK_API_KEY) instead of interactive entry
  3. For real interactive use, allocate a TTY: run without redirection, or ssh -t / docker run -it
  4. Rewrite automation to pass the key via config or secret store rather than a prompt

Example fix

# before
codewhale cloud login --provider deepseek        # stdin not a TTY -> error
# after
printf '%s' "$DEEPSEEK_API_KEY" | codewhale cloud login --provider deepseek --api-key-stdin
Defensive patterns

Strategy: fallback

Validate before calling

use std::io::IsTerminal;

if !std::io::stdin().is_terminal() {
    // fall back to explicit pipe mode instead of the hidden prompt
    let key = read_key_from_stdin()?; // --api-key-stdin path
} else {
    let key = read_key_hidden("deepseek")?;
}

Try / catch

match read_key_hidden(provider) {
    Ok(k) => k,
    Err(e) if e.to_string().contains("requires a terminal") => read_key_from_stdin()?,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Running a command that prompts for the key (no --api-key-stdin) with stdin redirected: `codewhale cloud login ... < file`, inside `curl ... | sh` style pipelines, in CI runners, or under ssh -T where no TTY is allocated.

Common situations: CI/CD jobs provisioning credentials; Docker containers without -t; scripts meant to be interactive being run non-interactively; automation reusing an interactive command.

Related errors


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