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
- Pass explicit input when stdin is not the keyboard: tea.WithInput(os.Stdin) or a custom reader
- Disable input entirely for non-interactive runs: tea.WithInput(nil) (or WithAltScreen plus WithoutSignalHandler for pure output mode)
- Detect the environment first with term.IsTerminal(os.Stdin.Fd()) and branch TUI vs plain output
- 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
- Branch on term.IsTerminal(os.Stdin.Fd()) at startup
- In CI/tests, always pass tea.WithInput with a reader or nil
- Use creack/pty to allocate real terminals in tests
- For daemons, never construct a full TUI program
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
- bubbletea: error getting terminal size: %w
- bubbletea: error restoring console: %w
- program experienced a panic
- program was killed
- program was interrupted
AI-assisted analysis of charmbracelet/bubbletea@351d2159f8 (2026-08-15).
Data as JSON: /api/errors/9d675dfae38a7e8c.
Report an issue: GitHub.