Hmbown/CodeWhale · critical · anyhow::Error
Failed to enable raw mode: {e}
Error message
Failed to enable raw mode: {e} What it means
crossterm's enable_raw_mode failed inside the startup probe (run on a spawn_blocking task). The OS error is appended: ENOTTY when stdin is not a terminal, EBADF on closed stdin, or a termios ioctl rejection. Raw mode is a hard prerequisite for the TUI, so startup aborts; sibling code prints guidance to run from an interactive terminal and to use `codewhale exec` for headless prompts.
Source
Thrown at crates/tui/src/tui/ui/event_loop.rs:174
// without a controlling TTY (#4716). Without this, enable_raw_mode fails
// with opaque "Device not configured" / "Input/output error" and some
// terminal hosts surface only "[Process completed]".
require_interactive_terminal(io::stdin().is_terminal(), io::stdout().is_terminal())?;
// Terminal probe with timeout to prevent hanging on unresponsive terminals.
//
// The blocking task cannot be cancelled once the timeout fires, so a slow
// `enable_raw_mode` may still succeed *after* we've bailed out, leaking
// raw mode. Both sides run `raw_mode_probe_handshake`; whichever observes
// the other's flag disables raw mode again.
let probe_timeout = terminal_probe_timeout(config);
let probe_abandoned = Arc::new(AtomicBool::new(false));
let probe_enabled = Arc::new(AtomicBool::new(false));
let task_abandoned = Arc::clone(&probe_abandoned);
let task_enabled = Arc::clone(&probe_enabled);
let enable_raw = tokio::task::spawn_blocking(move || {
let result =
enable_raw_mode().map_err(|e| anyhow::anyhow!("Failed to enable raw mode: {e}"));
if result.is_ok() && raw_mode_probe_handshake(&task_enabled, &task_abandoned) {
// The probe timed out while we were blocked; the caller already
// gave up, so undo the late enable instead of leaking raw mode.
let _ = disable_raw_mode();
}
result
});
match tokio::time::timeout(probe_timeout, enable_raw).await {
Ok(inner_result) => {
inner_result??; // propagate both join and raw-mode errors
}
Err(_) => {
if raw_mode_probe_handshake(&probe_abandoned, &probe_enabled) {
// The blocking task finished enabling raw mode right as the
// timeout fired and may have missed the abandoned flag.
let _ = disable_raw_mode();
}View on GitHub (pinned to 0c42157ee5)
Solutions
- Run from an interactive terminal: `docker exec -it …`, `ssh -t …`
- Do not redirect stdin/stdout when launching the TUI; for headless prompts use `codewhale exec "…"`
- Verify with the `tty` command - it must print a device path
- Ensure no other raw-mode owner holds the terminal
Example fix
# before docker exec codewhale-container codewhale tui # stdin not a TTY # -> Failed to enable raw mode: … (os error 25) # after docker exec -it codewhale-container codewhale tui
Defensive patterns
Strategy: validation
Validate before calling
// Rust - refuse TUI startup without a TTY stdin
use std::io::IsTerminal;
if !std::io::stdin().is_terminal() {
eprintln!("codewhale tui needs an interactive terminal; use `codewhale exec` for headless prompts");
std::process::exit(1);
} Try / catch
match enable_raw_mode() {
Ok(()) => run_tui(),
Err(e) => { restore_terminal(); print_run_from_tty_guidance(e); std::process::exit(1); }
} Prevention
- Always allocate a PTY (`docker exec -it`, `ssh -t`) when launching the TUI
- Use the headless/exec entry point for automation instead of the TUI binary
- Never redirect stdin/stdout when starting the TUI
When it happens
Trigger: Launching the TUI with stdin piped, closed, or detached (`codewhale tui < file`, `docker exec` without -t, CI runners), a dead or absent PTY, or another process already owning raw mode on the same TTY.
Common situations: Headless invocations of the TUI binary instead of the exec mode; automation wrappers consuming stdin; broken SSH PTY allocation.
Related errors
- Terminal probe timed out after {}ms
- interactive key entry requires a terminal; use `--api-key-st
- OSC 52 clipboard fallback requires a terminal
- pipeline(): expected an array of items
- pipeline(): max 1000 items per call
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/002b5c38bc89d30f.
Report an issue: GitHub.