Pseudo-Terminals (PTY)

Updated

September 8, 2026

Pseudo-Terminals (PTY)

Overview

A pseudo-terminal makes a program believe it is attached to a real terminal: line discipline, window size, and isatty checks. You need PTYs when automating interactive tools (ssh, sudo, password prompts, TUI apps) from a non-interactive parent.

Stdlib does not ship a full PTY API. Common approach: github.com/creack/pty (or platform x/sys + careful setup).

TTY detection (stdlib)

func isTerminal(f *os.File) bool {
    fi, err := f.Stat()
    if err != nil {
        return false
    }
    return fi.Mode()&os.ModeCharDevice != 0
}

CLIs use this for color, paging, and prompts (chapter 145).

Why pipes are not enough

parent --pipe--> child

Many programs disable interactive features when stdin is a pipe (Not a tty). A PTY pair fixes that:

parent <-> PTY master <-> PTY slave <-> child (thinks it's a terminal)

Start a command under a PTY (creack/pty)

go get github.com/creack/pty@latest
import (
    "io"
    "os"
    "os/exec"

    "github.com/creack/pty"
)

func runInPTY(name string, args ...string) error {
    cmd := exec.Command(name, args...)
    ptmx, err := pty.Start(cmd)
    if err != nil {
        return err
    }
    defer ptmx.Close()

    // copy PTY <-> user terminal
    go io.Copy(ptmx, os.Stdin)
    _, err = io.Copy(os.Stdout, ptmx)
    return err
}
go run . -- bash -l   # interactive shell session sketch

Window size

// propagate terminal resize (Unix)
_ = pty.InheritSize(os.Stdin, ptmx)

// listen for SIGWINCH and call InheritSize again

Without this, full-screen TUIs mis-draw after resize.

Scripted interaction

ptmx, err := pty.Start(exec.Command("mysql", "-u", "root", "-p"))
// carefully write password + "\n" after prompt detection
// prefer non-interactive flags when possible

Security: automating passwords is fragile and audit-hostile—prefer socket auth, env files with strict modes, or API tokens.

When to avoid PTYs

Prefer Avoid PTY when
cmd.Stdin + flags/env App has real non-interactive mode
Expect scripts (npm test) You only need stdout capture
Stdlib pipes Cross-platform simplicity matters

Minimal tool: runtty

runtty -- <command>
# allocates PTY, connects to current terminal

Useful for wrapping tools that refuse pipes.

Rules of thumb

Do Don’t
Prefer non-interactive APIs PTY-scrape fragile prompts in CI
Handle SIGWINCH for TUIs Assume 80x24 forever
Close master FD Leak PTYs (FD exhaustion)
Document platform deps Expect identical PTY behavior on Windows without code

Try next

  1. Run ls --color=auto under pipe vs PTY; compare color.
  2. Automate a simple read shell script via PTY write.
  3. Wire resize handling and test with a TUI (top, htop).