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

  1. Run from an interactive terminal: `docker exec -it …`, `ssh -t …`
  2. Do not redirect stdin/stdout when launching the TUI; for headless prompts use `codewhale exec "…"`
  3. Verify with the `tty` command - it must print a device path
  4. 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

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


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