charmbracelet/bubbletea · error

bubbletea: error opening TTY: %w

Error message

bubbletea: error opening TTY: %w

What it means

Returned by Program.Run when input is not disabled, no custom input was provided, stdin is not a terminal, and opening the controlling TTY (via OpenTTY -> uv.OpenTTY, i.e. /dev/tty) fails. Bubble Tea tries this fallback so programs launched with redirected stdin still get keyboard input from the terminal. The error wraps the underlying failure (usually 'no such device' or 'permission denied' on /dev/tty).

Source

Thrown at tea.go:1014

	// Initialize context and teardown channel.
	p.handlers = channelHandlers{}
	cmds := make(chan Cmd)

	p.finished = make(chan struct{})
	defer func() {
		close(p.finished)
	}()

	defer p.cancel()

	if p.disableInput {
		p.input = nil
	} else if p.input == nil {
		p.input = os.Stdin
		if !term.IsTerminal(os.Stdin.Fd()) {
			ttyIn, _, err := OpenTTY()
			if err != nil {
				return p.initialModel, fmt.Errorf("bubbletea: error opening TTY: %w", err)
			}
			p.input = ttyIn
		}
	}

	// Handle signals.
	if !p.disableSignalHandler {
		p.handlers.add(p.handleSignals())
	}

	// Recover from panics.
	if !p.disableCatchPanics {
		defer func() {
			if r := recover(); r != nil {
				returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic)
				p.recoverFromPanic(r)
			}
		}()

View on GitHub (pinned to 351d2159f8)

Solutions

  1. Pass explicit input when stdin is not the keyboard: tea.WithInput(os.Stdin) or a custom reader
  2. Disable input entirely for non-interactive runs: tea.WithInput(nil) (or WithAltScreen plus WithoutSignalHandler for pure output mode)
  3. Detect the environment first with term.IsTerminal(os.Stdin.Fd()) and branch TUI vs plain output
  4. In tests, allocate a pty (creack/pty) or always use WithInput(bytes.Buffer)

Example fix

// before
p := tea.NewProgram(m)
_, err := p.Run() // fails in CI: cannot open /dev/tty

// after
var in io.Reader = os.Stdin
if !term.IsTerminal(os.Stdin.Fd()) {
    in = nil // or a bytes.Buffer of scripted keys
}
p := tea.NewProgram(m, tea.WithInput(in))
_, err := p.Run()
Defensive patterns

Strategy: validation

Validate before calling

// Decide interactivity before building the program:
func isInteractive() bool {
    return term.IsTerminal(os.Stdin.Fd()) || canOpenTTY()
}
func canOpenTTY() bool {
    f, err := tea.OpenTTY()
    if err != nil { return false }
    _ = f.Close()
    return true
}

Type guard

func isTTYOpenFailure(err error) bool {
    return err != nil && strings.Contains(err.Error(), "error opening TTY")
}

Try / catch

_, err := p.Run()
if isTTYOpenFailure(err) {
    // retry headless
    p2 := tea.NewProgram(m, tea.WithInput(nil), tea.WithoutRenderer())
    _, err = p2.Run()
}
if err != nil { log.Fatal(err) }

Prevention

When it happens

Trigger: Running with stdin piped/redirected (app < input.txt) or in environments with no controlling terminal (cron, CI, some daemons, `go test` without a pty) where /dev/tty cannot be opened; headless containers or chroots without a TTY device.

Common situations: CI pipelines or unit tests invoking the program; `echo | mytui` or `mytui < file`; running under systemd/cron where there is no controlling terminal; minimal Docker images lacking /dev/tty; programs that are TUI-first but sometimes run non-interactively.

Related errors


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