zeroclaw-labs/zeroclaw · error · anyhow::Error

cli-secret-needs-tty

cli-secret-needs-tty

Error message

Secret input requires a terminal on stdin and stderr.

What it means

secret_prompt reads hidden input via cli_input::SecretInput, which needs a real terminal on both stdin (to read the secret) and stderr (to render the prompt and mask feedback). If either stream is not a TTY, the prompt cannot work safely and the call aborts before reading anything (i18n key 'cli-secret-needs-tty', stable code of the same name).

Source

Thrown at src/main.rs:151

    {
        zeroclaw_runtime::i18n::get_required_cli_string_with_args(key, args)
    }
    #[cfg(not(feature = "agent-runtime"))]
    {
        fallback.to_string() // i18n-exempt: English fallback when Fluent (agent-runtime) is disabled
    }
}

/// Interactive secret prompt with pre-submit feedback.
///
/// The value stays hidden, but the prompt shows a bounded mask once the input
/// buffer becomes non-empty.
#[cfg(feature = "agent-runtime")]
fn secret_prompt(prompt_text: &str, allow_empty: bool) -> Result<String> {
    use std::io::IsTerminal;

    if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
        bail!(ta(
            "cli-secret-needs-tty",
            &[],
            "Secret input requires a terminal on stdin and stderr."
        ));
    }

    let value = cli_input::SecretInput::new()
        .with_prompt(prompt_text)
        .interact()?;
    if allow_empty || !value.trim().is_empty() {
        Ok(value)
    } else {
        bail!(ta("cli-secret-empty", &[], "Value cannot be empty."))
    }
}

#[cfg(feature = "agent-runtime")]
fn qta(key: &str, args: &[(&str, &str)]) -> String {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Run the command from an interactive terminal so both stdin and stderr are TTYs.
  2. In containers use `docker exec -it <container> zeroclaw ...`; over SSH drop one-shot mode so a TTY is allocated.
  3. For automation, provision the secret non-interactively (config file or the documented env-var route) instead of relying on the prompt.
  4. If you must automate the prompt, drive it through a pty wrapper such as `expect` or `script`.

Example fix

# before: no tty, prompt aborts
docker exec zeroclaw zeroclaw onboard   # Secret input requires a terminal...
# after: allocate a tty
docker exec -it zeroclaw zeroclaw onboard
Defensive patterns

Strategy: fallback

Validate before calling

use std::io::IsTerminal;
fn can_prompt_secret() -> bool {
    std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
}
// in automation: if !can_prompt_secret(), provision the secret via the
// documented non-interactive route (config/env) instead of invoking the prompt

Try / catch

match run_onboarding().await {
    Err(e) if e.to_string().contains("requires a terminal") => {
        // no TTY: switch to the non-interactive provisioning path or re-run
        // inside `docker exec -it` / an interactive ssh session
    }
    other => other,
}

Prevention

When it happens

Trigger: Invoking any zeroclaw flow that prompts for a secret (e.g. provider API key onboarding) with stdin or stderr redirected: piping answers in (`echo y | zeroclaw ...`), running under CI, daemons, plain `docker exec` without -t, or scripts with stdin from a file.

Common situations: Automating first-run setup in containers or CI where no terminal exists; SSH one-shot commands (`ssh host zeroclaw ...` allocates no TTY); cron-invoked maintenance commands that hit an interactive path.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/4e631815b5e2d0fe. Report an issue: GitHub.