sigoden/aichat · error

No TTY for REPL

Error message

No TTY for REPL

What it means

The REPL (interactive mode) was requested but stdout is not a terminal (`IS_STDOUT_TERMINAL` is false), so the interactive prompt cannot run. The library refuses to start an interactive session without a TTY.

Solutions

  1. Pass the prompt as a CLI argument/file (-f) so it runs in non-interactive mode
  2. Allocate a TTY (e.g. `docker run -it`, run in a real terminal)
  3. Check `is-terminal` detection if you believe a TTY exists

Example fix

// before (non-interactive)
mytool | tee out.log
// after
mytool "explain this code" -f prompt.txt
Defensive patterns

Strategy: type-guard

Validate before calling

if !atty::is(atty::Stream::Stdout) {
    eprintln!("stdout is not a TTY; pass a prompt for non-interactive mode");
}

Type guard

fn has_tty() -> bool {
    std::io::IsTerminal::is_terminal(&std::io::stdout())
}

Try / catch

if let Err(e) = cli.run().await {
    if e.to_string() == "No TTY for REPL" {
        eprintln!("Provide a prompt argument or run inside a terminal");
    }
}

Prevention

When it happens

Trigger: Running the binary with no prompt text (REPL mode) while stdout is piped/redirected, or inside a non-interactive environment (CI, cron, docker without -t).

Common situations: `mytool < input.txt` or `mytool | tee log` with no prompt argument; CI job invoking the CLI without a prompt; Docker container run without `-it`.

Related errors


AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09). Data as JSON: /api/errors/2ab7fd91f7200143. Report an issue: GitHub.

Appendix: source

Thrown at src/main.rs:188

    if let Some(name) = &cli.macro_name {
        macro_execute(&config, name, text.as_deref(), abort_signal.clone()).await?;
        return Ok(());
    }
    if cli.execute && !is_repl {
        let input = create_input(&config, text, &cli.file, abort_signal.clone()).await?;
        shell_execute(&config, &SHELL, input, abort_signal.clone()).await?;
        return Ok(());
    }
    config.write().apply_prelude()?;
    match is_repl {
        false => {
            let mut input = create_input(&config, text, &cli.file, abort_signal.clone()).await?;
            input.use_embeddings(abort_signal.clone()).await?;
            start_directive(&config, input, cli.code, abort_signal).await
        }
        true => {
            if !*IS_STDOUT_TERMINAL {
                bail!("No TTY for REPL")
            }
            start_interactive(&config).await
        }
    }
}

#[async_recursion::async_recursion]
async fn start_directive(
    config: &GlobalConfig,
    input: Input,
    code_mode: bool,
    abort_signal: AbortSignal,
) -> Result<()> {
    let client = input.create_client()?;
    let extract_code = !*IS_STDOUT_TERMINAL && code_mode;
    config.write().before_chat_completion(&input)?;
    let (output, tool_results) = if !input.stream() || extract_code {
        call_chat_completions(

View on GitHub (pinned to 82976d349a)