How Terminals Actually Work

13 min read
engineeringterminalsdeep-dive

A deep dive into the 60-year-old architecture that still powers every developer tool on your machine, told through the story of a bug that took hours to fix.


The Bug

I use Claude Code as my daily AI assistant. Each conversation runs in its own iTerm window, a split pane setup with a terminal on top and Claude on the bottom. When a new conversation starts, the window title says something generic like local | besowards | chat | claude. Once the conversation gets a name (a "slug" like iterm-title-slug-sync), Claude should update the window title to match.

Simple, right?

It took hours to solve. Not because the code was complex, but because the fix required understanding how terminals actually work: virtual teletypewriters, escape sequences, file descriptors, process I/O, and AppleScript bridging.

This is the story of that fix, and everything I learned along the way.


Part 1: What Is a Terminal?

When you open iTerm (or Terminal.app, or any terminal emulator), you're looking at a program pretending to be a 1960s teletypewriter.

The Original TTY

In the 1960s, computers didn't have screens. You interacted with them through a teletypewriter (TTY), a machine with a keyboard and a printer. You typed a command, the computer processed it, and the teletypewriter printed the response on paper. Character by character. No backspace. No colors. Just text on paper.

The interface was simple: characters go in, characters come out. Two directions, one channel each.

The Virtual TTY

Screens replaced printers. Then personal computers replaced mainframes. But the interface (characters in, characters out) was so clean and universal that it survived. Every terminal emulator today implements it.

When you open a new pane in iTerm, the operating system creates a virtual TTY, a pair of file descriptors that simulate the old teletypewriter connection:

Loading diagram...

The TTY has two ends:

  • Master side: iTerm holds this. It sends your keystrokes in and reads program output out.
  • Slave side: Your shell (zsh, bash) holds this. It reads input and writes output.

The slave side shows up as a file: /dev/ttys013. That file IS the terminal. Programs write to it, iTerm reads from it and renders the characters on screen.

You can see yours right now:

$ tty
/dev/ttys013

Every pane in iTerm gets its own TTY. Open 10 panes, you'll have 10 /dev/ttysXXX files.


Part 2: The Three Channels (stdin, stdout, stderr)

Every program that runs on your computer gets three I/O channels automatically:

ChannelFile DescriptorPurpose
stdinfd 0Where the program reads input
stdoutfd 1Where the program writes normal output
stderrfd 2Where the program writes error messages

When you run a command in your terminal, these channels are connected to the TTY by default:

Loading diagram...

echo "hello" writes "hello" to stdout (fd 1). Since stdout is connected to the TTY, iTerm receives it and displays it on screen.

Redirection Changes the Wiring

The shell lets you rewire these channels:

echo "hello" > file.txt     # stdout goes to file.txt instead of the TTY
echo "hello" | grep "h"     # stdout goes to a pipe, grep reads from the other end

When you redirect stdout to a file, the program has no idea. It writes to fd 1 the same way it always does. The operating system just changed where fd 1 points.

This is fundamental to understanding the bug.


Part 3: Escape Codes, the Terminal's Secret Language

Here's something that might surprise you: every time you see colored text in your terminal, bold text, a cursor moving, or a title changing... that's not special rendering. It's invisible characters in the output stream.

How Colors Work

When a program wants to print red text, it doesn't call a "set color to red" API. It writes invisible bytes into its stdout, mixed in with the visible text:

\033[31m  H  e  l  l  o  \033[0m
^^^^^^^^                  ^^^^^^^^
invisible                 invisible
"start red"               "reset color"

\033 is the ESC character (byte value 27). The terminal sees ESC[31m, recognizes it as "set text color to red," and starts rendering in red. The escape sequence itself is never displayed.

These are called ANSI escape codes, and they control everything:

\033[31m          Set text color to red
\033[1m           Bold
\033[H            Move cursor to top-left
\033[2J           Clear the screen
\033[?25l         Hide the cursor
\033]0;TITLE\007  Set the window title

That last one is the one we care about.

The Title Escape Code

\033]0;My Window Title\007
│   │ │               │
│   │ │               └── BEL character (byte 7), marks the end
│   │ └──────────────── The title text
│   └────────────────── "0" means set both tab and window title
└────────────────────── ESC character (byte 27), starts the sequence

This is an "OSC" (Operating System Command) sequence. There are three variants:

CodeWhat It Sets
\033]0;TEXT\007Both tab title AND window title
\033]1;TEXT\007Tab/icon title only
\033]2;TEXT\007Window title only

When iTerm reads these bytes from a TTY, it intercepts them (never displays them) and updates the window chrome.

The title() Function

Most developers who set terminal titles use a wrapper function. Here's the one from this story:

function title() {
  echo -ne "\033]0;$1\007"
}

echo -ne writes the bytes without a trailing newline (-n) and interprets escape sequences (-e). The \033 becomes the actual ESC byte, \007 becomes the actual BEL byte. iTerm receives them through the TTY and changes the title.

It's one line of code, but it depends on a chain of assumptions:

  1. stdout is connected to a TTY
  2. The TTY is connected to a terminal emulator
  3. The terminal emulator processes OSC escape codes

Break any link in that chain, and the title doesn't change. The escape bytes either go nowhere or get displayed as garbage text.


Part 4: How Claude Code Runs Bash Commands

Claude Code is an AI assistant that runs in your terminal. When it needs to execute a shell command, it spawns a subprocess:

Loading diagram...

Claude captures the command's output by connecting stdout to a pipe, not to the TTY. This is how Claude reads what the command printed. But it means the command's stdout is no longer connected to iTerm.

You can verify this from inside Claude's bash:

$ tty
not a tty

The subprocess literally does not have a terminal. It has pipes.

Why This Breaks Title Setting

When Claude runs echo -ne "\033]0;my-slug\007":

  1. The escape bytes go to stdout (fd 1)
  2. Stdout is a pipe, not a TTY
  3. Claude reads the bytes from the pipe and captures them as text output
  4. iTerm never sees the escape codes
  5. The title doesn't change

The escape code works perfectly. It just goes to the wrong place.


Part 5: The Failed Attempts

Understanding why each approach failed reveals how the system actually works.

Attempt 1: Write to stderr

echo -ne "\033]0;slug\007" >&2

Theory: Maybe stderr is still connected to the TTY even if stdout isn't.

Reality: Claude's bash tool redirects stderr too. All three channels go to pipes. The escape code went to Claude's error capture, not to iTerm.

Attempt 2: AppleScript set name

tell session to set name to "slug"

Theory: Set the session name directly through iTerm's scripting interface.

Reality: The AppleScript name property and the displayed title are different things. The displayed title comes from the escape code history. Setting name changes an internal property that doesn't affect what you see in the tab bar.

Attempt 3: AppleScript write text to the terminal pane

tell terminal_pane to write text "title \"slug\""

Theory: Send the title command to the terminal pane (which has a real shell), changing its title.

Reality: This works for the terminal pane's title, but the tab/window title displays whichever pane has focus. Since the Claude pane has focus (the user is chatting with Claude), the tab still shows the Claude pane's old title.

Attempt 4: AppleScript write text to the Claude pane

tell claude_pane to write text "title \"slug\""

Theory: Send the title command to the Claude pane.

Reality: write text simulates keyboard input. The Claude pane is running Claude Code, not a shell. The text title "slug" gets sent to Claude as a user message. Claude reads it as if you typed it. The shell never sees it.

Attempt 5: Write to the TTY device file

echo -ne "\033]0;slug\007" > /dev/ttys013

Theory: Write directly to the TTY device file from outside.

First try: This appeared to fail. But the problem was subtler. Writing to /dev/ttysXXX does work, but only when do shell script (AppleScript) does it, not when Claude's piped bash does it.


Part 6: The Solution

The fix combines two insights:

Insight 1: Writing to a TTY device file from a real process

When a process writes to /dev/ttys013, the bytes go through the terminal's processing pipeline. iTerm receives them and processes escape codes. But the writing process needs to be a real process with file access, not a piped subprocess.

AppleScript's do shell script runs a command as an independent process (not inside any iTerm session). It has full file system access:

do shell script "printf '\\033]0;my-slug\\007' > /dev/ttys013"

This spawns a small process that opens /dev/ttys013 and writes the escape code bytes. iTerm, which is listening on the master side of that TTY, receives the bytes and processes the title change.

Insight 2: Target by UUID, not by "current window"

AppleScript's current window means "whichever window has focus." With 12 iTerm windows across 3 monitors, that's almost never the right one.

Instead, Claude's environment has ITERM_SESSION_ID, a UUID that uniquely identifies the session Claude is running in. The script walks every window, every tab, every session, looking for the one with the matching UUID. Then it writes to all sessions in that tab.

The Final Code

ITERM_UUID="${ITERM_SESSION_ID#*:}"
osascript -e "
tell application \"iTerm2\"
  repeat with w in windows
    repeat with t in tabs of w
      repeat with s in sessions of t
        if unique ID of s is \"$ITERM_UUID\" then
          -- Found our session. Write title to ALL panes in this tab.
          repeat with p in sessions of t
            set ttyPath to tty of p
            do shell script \"printf '\\\\033]0;my-slug\\\\007' > \" & ttyPath
          end repeat
          return
        end if
      end repeat
    end repeat
  end repeat
end tell
"

Step by step:

  1. Extract the UUID from ITERM_SESSION_ID (format is w0t0p0:UUID)
  2. Walk all windows, tabs, and sessions looking for a matching UUID
  3. When found, iterate all sessions in that tab (both panes)
  4. For each pane, get its TTY path (e.g., /dev/ttys013)
  5. Use do shell script to write the title escape code to that TTY
  6. iTerm processes the escape code and updates the title

We write to ALL panes because iTerm's tab/window title follows whichever pane has focus. If both panes have the same title set, the title is correct regardless of where the user clicks.


Part 7: The Full Data Flow

Here's what happens end-to-end when Claude names a conversation:

Loading diagram...

The whole thing takes milliseconds. The user sees the window title change instantly.


Part 8: Why This Architecture Exists

You might wonder: why is this so complicated? Why can't a program just call setWindowTitle("my title")?

The answer is separation of concerns, and it goes back to the 1960s design.

Programs Don't Know About Terminals

A program writes bytes to stdout. That's it. It doesn't know if stdout goes to a terminal, a file, a pipe, another program, or a printer. This is deliberate:

ls -la              # Output goes to terminal (you see it)
ls -la > files.txt  # Output goes to a file (you don't see it)
ls -la | grep ".md" # Output goes to another program

ls runs the same code in all three cases. It doesn't care where its output goes. This is what makes Unix pipes and redirection so powerful. Programs are composable because they don't make assumptions about their environment.

The Terminal Is Just Another Program

iTerm isn't special. It's a program that:

  1. Creates a virtual TTY (pair of file descriptors)
  2. Launches your shell connected to the slave side
  3. Reads from the master side and renders characters on screen
  4. Takes keyboard input and writes to the master side

Any program can create a TTY and do this. That's how tmux, screen, SSH, and Docker work. They're all terminal emulators of one kind or another, nesting TTYs inside TTYs.

Escape Codes Are In-Band Signaling

The title-setting mechanism is "in-band." The control commands travel through the same channel as the data. There's no separate "control channel" for metadata like titles. This is both elegant (no extra infrastructure needed) and fragile (redirect the output and you lose the controls).

Modern terminal emulators like iTerm add proprietary escape codes for features the 1960s designers never imagined (inline images, clickable links, notifications, profile switching) but they all flow through the same character stream.


The Lesson

The terminal is one of the oldest abstractions in computing, and it's still everywhere. Every Docker container, every SSH session, every CI/CD pipeline, every IDE terminal panel: they all use virtual TTYs, escape codes, and the stdin/stdout/stderr model designed for paper teletypewriters in the 1960s.

Most of the time, you don't need to think about it. The abstraction just works. But when it doesn't (when you need to reach across process boundaries, inject escape codes into foreign sessions, or understand why your output looks like garbage) there's no shortcut. You have to understand the plumbing.

The bug took hours to fix not because the solution was complex (it's 15 lines of AppleScript) but because finding it required understanding six decades of computing history that most tutorials skip over.

Now you know where the bytes go.

← Back to Blog