charmbracelet/bubbletea · error

error entering raw mode: %w

Error message

error entering raw mode: %w

What it means

Bubble Tea failed to put the terminal into raw mode on a Unix-like system while initializing input. initInput() calls term.MakeRaw() on the stdin file descriptor (an ioctl TCGETS/TCSETS on the fd), and the kernel rejected it — typically ENOTTY, EBADF, or EACCES. The wrapped error is returned from Program.Run() via initTerminal(), so the program never starts.

Source

Thrown at tty_unix.go:21

package tea

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/charmbracelet/x/term"
)

func (p *Program) initInput() (err error) {
	// Check if input is a terminal
	if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
		p.ttyInput = f
		p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
		if err != nil {
			return fmt.Errorf("error entering raw mode: %w", err)
		}

		// OPTIM: We can use hard tabs and backspaces to optimize cursor
		// movements. This is based on termios settings support and whether
		// they exist and enabled.
		p.checkOptimizedMovements(p.previousTtyInputState)
	}

	if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
		p.ttyOutput = f
	}

	return nil
}

const suspendSupported = true

// Send SIGTSTP to the entire process group.

View on GitHub (pinned to 351d2159f8)

Solutions

  1. Check the wrapped error's underlying errno: run with the full chain printed (`%+v` or errors.Unwrap) — ENOTTY means the fd is not a real terminal, EBADF means it is closed.
  2. Ensure stdin is a real tty before starting: if term.IsTerminal(os.Stdin.Fd()) is false, run with tea.WithInput(nil) or a non-tty input so Bubble Tea skips raw mode.
  3. If you need a tty in a non-interactive context (tests, CI), allocate a pty (e.g. creack/pty) and pass it via tea.WithInput(ptmx) / tea.WithOutput(ptmx).
  4. For headless rendering, start the program with tea.WithoutRenderer() — initTerminal() returns nil early and raw mode is never attempted.
  5. Reopen /dev/tty explicitly with tea.OpenTTY() and feed the returned files to WithInput/WithOutput when the original stdin fd is stale or closed.

Example fix

// before
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
    log.Fatalf("run: %v", err) // error entering raw mode: inappropriate ioctl for device
}

// after
in, out, err := tea.OpenTTY() // fresh, real terminal handles
if err != nil {
    log.Fatalf("open tty: %v", err)
}
defer in.Close()
defer out.Close()
p := tea.NewProgram(model{}, tea.WithInput(in), tea.WithOutput(out))
Defensive patterns

Strategy: try-catch

Validate before calling

if f, ok := stdin.(term.File); ok && !term.IsTerminal(f.Fd()) {
    // not a real terminal: raw mode will fail
    stdin = nil // or a pipe
}
p := tea.NewProgram(model, tea.WithInput(stdin))

Type guard

func isRealTTY(f term.File) bool {
    return term.IsTerminal(f.Fd())
}

Try / catch

if _, err := p.Run(); err != nil {
    var rawErr error
    if strings.Contains(err.Error(), "error entering raw mode") {
        rawErr = errors.Unwrap(err)
    }
    switch {
    case rawErr == nil:
        log.Fatalf("program error: %v", err)
    case errors.Is(rawErr, unix.ENOTTY):
        log.Fatal("stdin is not a terminal; run interactively or use tea.WithInput(nil)")
    case errors.Is(rawErr, unix.EBADF):
        log.Fatal("stdin fd is closed; reopen with tea.OpenTTY()")
    default:
        log.Fatalf("terminal init failed: %v", err)
    }
}

Prevention

When it happens

Trigger: Running a Program whose p.input implements term.File and passes term.IsTerminal(), but whose fd cannot actually be switched to raw mode: a closed stdin fd, a pty that lost its controlling side, a /dev/tty that is not accessible, or a pseudo-terminal granted by a sandbox/container without full termios permissions.

Common situations: Piping or redirecting stdin in shells or CI (input is a pipe but a wrapping pty still reports a terminal), running under `go test` with no real tty, detached processes or daemons that kept a stale terminal fd, containers/seccomp profiles blocking TCSETS ioctls, and SSH sessions that dropped while the program was starting.

Related errors


AI-assisted analysis of charmbracelet/bubbletea@351d2159f8 (2026-08-15). Data as JSON: /api/errors/6340e0b5aed71a4a. Report an issue: GitHub.