zellij-org/zellij · critical

could not enable raw mode

Error message

could not enable raw mode

What it means

set_raw_mode() is called by the client on startup and expects crossterm::terminal::enable_raw_mode() to succeed: raw mode disables echo, line buffering and canonical signal processing so the client can read every key itself. It fails when the termios attributes cannot be read or written - stdin/stdout is not a tty, there is no controlling terminal, or the tty device is inaccessible. Because every keystroke flows through raw mode, the client treats this as fatal immediately.

Source

Thrown at zellij-client/src/os_input_output.rs:172

    fn env_variable(&self, _name: &str) -> Option<String> {
        None
    }
    /// Returns an async stdin reader that can be polled in tokio::select
    fn get_async_stdin_reader(&self) -> Box<dyn AsyncStdin> {
        Box::new(AsyncStdinReader::new())
    }
    /// Returns an async signal listener that can be polled in tokio::select
    fn get_async_signal_listener(&self) -> io::Result<Box<dyn AsyncSignals>> {
        Ok(Box::new(AsyncSignalListener::new()?))
    }
}

impl ClientOsApi for ClientOsInputOutput {
    fn get_terminal_size(&self) -> Size {
        get_terminal_size()
    }
    fn set_raw_mode(&mut self) {
        crossterm::terminal::enable_raw_mode().expect("could not enable raw mode");
    }
    fn unset_raw_mode(&self) -> Result<(), std::io::Error> {
        crossterm::terminal::disable_raw_mode()
    }
    fn box_clone(&self) -> Box<dyn ClientOsApi> {
        Box::new((*self).clone())
    }
    fn update_session_name(&mut self, new_session_name: String) {
        *self.session_name.lock().unwrap() = Some(new_session_name);
    }
    fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
        let session_name_at_calltime = { self.session_name.lock().unwrap().clone() };
        // here we wait for a lock in case another thread is holding stdin
        // this can happen for example when switching sessions, the old thread will only be
        // released once it sees input over STDIN
        //
        // when this happens, we detect in the other thread that our session is ended (by comparing
        // the session name at the beginning of the call and the one after we read from STDIN), and

View on GitHub (pinned to 98a0837077)

Solutions

  1. Run zellij from a real interactive terminal with a tty on stdin and stdout
  2. Guard scripted launches with `[ -t 0 ] && [ -t 1 ]` or `tty -s` before starting zellij
  3. Wrap non-interactive contexts in a pty allocator such as `script -qec "zellij attach <name>" /dev/null`
  4. Fix tty device permissions or the stale controlling terminal (re-login) if it still fails interactively

Example fix

// before
fn set_raw_mode(&mut self) {
    crossterm::terminal::enable_raw_mode().expect("could not enable raw mode");
}

// after - check the precondition, then make failure non-fatal
use std::io::IsTerminal;
fn set_raw_mode(&mut self) -> std::io::Result<()> {
    if !std::io::stdin().is_terminal() {
        return Err(std::io::Error::new(std::io::ErrorKind::NotConnected, "stdin is not a tty"));
    }
    crossterm::terminal::enable_raw_mode()
}
Defensive patterns

Strategy: validation

Validate before calling

use std::io::IsTerminal;

fn has_usable_tty() -> bool {
    std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
}

// before calling any client API that enters raw mode
if !has_usable_tty() {
    eprintln!("zellij requires a tty on stdin and stdout");
    std::process::exit(1);
}

Prevention

When it happens

Trigger: Launching the client with stdin or stdout redirected (scripts, cron, CI, `zellij < file`), running without a controlling terminal (detached setsid, some docker exec sessions), or a termios ioctl failure on the tty device.

Common situations: ssh -T; docker exec without -t; IDE task runners and watch tools that capture stdio; broken/absent TERM or tty permission problems on multi-user machines.

Related errors


AI-assisted analysis of zellij-org/zellij@98a0837077 (2026-08-16). Data as JSON: /api/errors/d3fbf29130a2fc47. Report an issue: GitHub.