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
- Pass the prompt as a CLI argument/file (-f) so it runs in non-interactive mode
- Allocate a TTY (e.g. `docker run -it`, run in a real terminal)
- 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
- Always pass a prompt when running non-interactively (CI, scripts, pipes)
- Use `docker run -it` when you need the REPL in containers
- Check terminal detection before invoking the CLI without arguments
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
- Some variable values are not key=value pairs
- Unknown command. Type ".help" for additional help.
- No models
- Unknown client
- Already in a session, please run '.exit session' first to…
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)