TerminalVibes / Under the Hood
Part 13

Under the Hood: The Machine You've Been Driving

"Every key you press is a byte on a wire. This is where you meet the wire."

For twelve parts you've driven this machine without once opening the hood. Now you've earned the wrench. This part is the optional deep dive: first how the terminal actually works — ttys, PTYs, escape sequences, and what Ctrl+C really does — and then what that machinery is becoming in the AI era, and the genuinely advanced things you can build now that you understand it. Nothing here is required to use the terminal. All of it makes the everyday mysteries — tty, ^[[A, Ctrl+C — feel ordinary.

13.1 How the Terminal Works

Keyboard to PTY to shell to kernel and back — the machinery behind every prompt

You've used every piece of this machine for twelve parts — typed at prompts, piped bytes, interrupted stuck commands, read exit codes. Time to see inside. If you've ever wondered why it's called a tty, why arrow keys sometimes print ^[[A instead of moving, or what Ctrl+C actually does, this is where those mysteries get solved. It's deliberately the most technical section of the course — and it repays the effort with a working mental model of the machine you've been driving all along.

In the beginning, the terminal was furniture. A teletypewriter — a keyboard fused to a printer — sat at the end of a serial cable, and the computer at the other end typed its replies onto rolling paper. Unix, born in that world, abbreviated the device to tty, and the abbreviation outlived the hardware by half a century. The paper is gone, the cables are gone, but every terminal window on your machine still checks in with the kernel (Stopping Things) as a tty device — ask it yourself:

Your window's device name
tty
# /dev/ttys004        (macOS)
# /dev/pts/0          (Linux)

No real teletype has been wired to a computer in decades — so today, software impersonates one. Three players stand in for that vanished machine:

Terminal emulator

The app you actually open — Terminal.app, iTerm2, Windows Terminal. It draws a grid of character cells, turns your keystrokes into bytes, and paints the bytes that come back. It emulates the old hardware — hence the name.

The PTY pair

A pseudo-terminal: two connected endpoints the kernel provides on request. The emulator holds the master end; whatever runs inside the window is attached to the slave end (newer docs say primary/secondary). It's a pipe wearing a teletype costume.

The shell

Just a process. bash reads bytes from the slave end and writes bytes back — it can't tell whether a human, a 1970s teletype, or an AI agent sits on the other side. That indifference is why the same shell works everywhere.

Keystrokes travel down the chain, output travels back up — and every hop is plain bytes.

One hop in that diagram does more than ferry bytes. Between the two ends of the PTY lives a slice of kernel code called the line discipline — a tiny line editor that decides how much of your typing to hold back and when to hand it over. It has two personalities:

Cooked mode (canonical)

The default. The kernel buffers your keystrokes, handles Backspace itself, and delivers nothing until you press Enter — then the program receives one finished line. This is why the shell never sees your typos: you edit the line before it exists.

Raw mode

Every keystroke is delivered immediately, unedited. Programs that react key by key — vim, less, the arrow-key line editor inside bash itself — switch the terminal into raw mode while they run, and back on exit. (A crash that skips the "back" is how terminals end up garbled — reset fixes it.)

That split personality explains a small mystery. Arrow keys don't send a character — there is no "up" letter — they send a short byte sequence beginning with the Escape character: up arrow is ESC [ A. A raw-mode program recognizes the sequence and moves the cursor. Hand it to a program reading cooked input — press up while cat is waiting — and it echoes as the literal ^[[A. That gibberish is readable once you know the convention: a caret is how a terminal writes a control character on paper, so ^[ is Ctrl+[, which is Escape, followed by the plain characters [ and A. The terminal spoke arrow, and nobody translated. You can inspect your own line discipline's settings any time:

Peek at the line discipline
stty -a
# speed 38400 baud; rows 24; columns 80;
# intr = ^C; erase = ^?; kill = ^U; eof = ^D;
# icanon iexten echo echoe echok ...

The -a earns its place: a bare stty reports a couple of lines, -a reports everything — that's the Linux layout above, and macOS groups the same settings under different headings. icanon is cooked mode's official name. erase = ^? is the kernel handling your Backspace, and ^? is the one token that breaks the caret rule: it's Delete, not Ctrl+?. kill = ^U wipes the line you're part-way through typing — the Ctrl+U from History Superpowers, meeting its kernel name — eof = ^D on an empty line ends it — and neither has anything to do with the kill command from Stopping Things, which talks to other processes entirely. The 38400 baud is a speed for a serial wire this PTY doesn't have, vestigial like the name tty. And intr = ^C is a promise we'll cash in a moment.

Escape sequences run in the other direction too — they're how programs draw. A program attached to a tty can only send bytes, so color, bold, and cursor movement travel in-band, mixed right into the text. Characters have numeric codes underneath (Looking Inside Files), and byte 27 — ESC, written \e — announces "the next few bytes are instructions, not text", so the emulator obeys them instead of printing them. The vocabulary was standardized in 1978 around the VT100, a physical terminal you could put on a desk, sold by Digital Equipment Corporation (DEC) — your gleaming modern emulator is still, at heart, impersonating it. Watch it obey:

Color is just bytes
printf '\e[32mgreen\e[0m\n'
# green        (printed in actual green)

\e[32m means "switch the pen to green"; \e[0m means "back to normal". That's printf and not the echo you've used all course for one reason: echo's handling of backslashes differs from shell to shell, so \e can arrive at the terminal as a backslash and an e. printf behaves the same everywhere, at the price of writing the trailing newline \n yourself. Every colorful ls, every fancy prompt, every full-screen dashboard is built from sequences like these — and the colored output in this site's playground works the same way in spirit: the text carries its own formatting, and whatever paints the glyphs interprets it.

One last piece of magic to demystify: Ctrl+C. It feels like input, but it never reaches the program as text. When Ctrl+C's byte arrives at the line discipline, the kernel intercepts it — that's the intr = ^C setting you just saw — and instead of passing it along, sends the foreground program a signal (Stopping Things) called SIGINT ("interrupt"). The program can catch it and tidy up, or die on the spot. Either way, you get your prompt back.

Why the panic button works
The cheat sheet's panic row promises Ctrl+C will interrupt a stuck command, and now you know why it's reliable: it isn't input the program must get around to reading — it's the kernel tapping the program on the shoulder. That's why it works even when a program is too busy to look at the keyboard. (A few programs catch SIGINT and ask questions first — that's them trapping the signal, not you failing to send it.)

You've already felt this from the other side. In Processes & Ports you sent SIGTERM with kill and watched a stubborn process shrug it off, then sent SIGKILL with kill -9, which it could not refuse. Same machinery, three doors: Ctrl+C is SIGINT to whatever is in the foreground, kill is SIGTERM to any PID you name, and kill -9 is the one signal no program is allowed to catch. What looked like three unrelated tricks is one mechanism you now understand end to end.

Put the whole chain together, and you can narrate the few milliseconds after you press Enter on ls:

  1. The emulator turns Enter into a carriage-return byte — the teletype's "slam the print head back to the left margin", a different byte from the newline that rolls the paper up a line — and writes it to the PTY master, same as any other key.
  2. The line discipline recognizes the end of a line, converts it to a newline, and releases the whole buffered line to the slave end.
  3. The shell, which has been blocked reading the slave — parked by the kernel, using no CPU at all while it waits, which is why a prompt can sit open all day for free — wakes up holding ls and parses it, expanding any $VARIABLES, ~, and globs first.
  4. It forks — makes a copy of itself — and inside that copy runs exec, which throws out the shell's program and loads ls in its place. That's why the two always travel together: fork makes the process, exec decides what it becomes. The copy's input and output are still wired to the same tty.
  5. ls does its work and writes its results — text plus color escape sequences — to the slave end.
  6. The bytes flow back through the PTY to the master; the emulator reads them, obeys the escapes, and paints glyphs into its character grid.
  7. ls exits; the shell collects its exit code (the $? you met in Scripts & Automation) and prints a fresh prompt. Your turn.

That's the entire machine: an app impersonating 1970s furniture, a kernel pipe in costume, a tiny in-kernel line editor, and a shell that just reads and writes bytes. From here on, nothing the terminal does will look like magic — every key press is a byte with a place to go, and now you can name every stop on the way.

And here's the part the museum plaque leaves out: the byte protocol didn't stop evolving in 1978. Modern terminals are still quietly minting new escape sequences. OSC 8 — an Operating System Command, the numbered family of escapes for things that aren't drawing, and despite the name nothing to do with your operating system — makes text in a terminal a real clickable hyperlink, and a family of shell-integration markers lets the shell whisper structure into the byte stream itself. That second one turns out to be the quiet foundation of the whole AI-terminal era — and it's exactly where we're headed next.

13.2 The Terminal, Evolving

Fifty years old — and just getting started: new protocols, agent-aware terminals, and intelligence in the pipeline

Knowing how the machine works is satisfying. Knowing what people are building on it right now is useful — because the terminal is in the middle of its biggest growth spurt since the VT100, and everything driving it is a payoff of something you just learned. This section is the tour: the invisible protocol that lets editors and agents read the terminal, the new generation of terminals built around agents, and the advanced automation you can write yourself now that pipes, signals, and exit codes are yours.

The shell and the terminal are talking behind your back

Remember the trick from How the Terminal Works — instructions traveling in-band, mixed into the text? Modern shells and terminals use the same trick to talk about the conversation itself. A standard called OSC 133 (born in the FinalTerm terminal, now adopted almost everywhere) has the shell emit invisible escape sequences that mark the seams of every command: here the prompt starts, here the command starts, here the output begins, here it ended — with this exit code. VS Code's terminal adds its own richer dialect, OSC 633, which also carries the command line itself. You never see any of it — the emulator swallows the markers like it swallows \e[32m — but the byte stream is now structured:

Invisible bookmarks in the byte stream: every command arrives pre-labeled with where it began, what it was, and how it went.

Those bookmarks are why VS Code's integrated terminal (Terminal in VS Code) can paint a little success or failure dot next to every command you run, let you jump between commands with a keystroke, and pin the running command to the top of the panel while output scrolls. And they matter double in the AI era: an agent watching a terminal through OSC markers doesn't have to guess where your prompt ends and the output begins, or grep the scrollback for the word "error" — it knows the exact command, the exact output, and the exact exit code, machine-readably. The read-before-you-run contract from Terminal for the AI Era works in both directions now: you can read what the agent runs, and the agent can reliably read what happened.

The terminals being built around agents

In Make It Yours you picked a window to live in. Step back and you can see the whole landscape splitting in two. On one side, the classic emulators keep competing on speed and standards: Ghostty 1.3 (March 2026) restructured itself so its entire terminal core is a reusable library — code packaged for other programs to call rather than to run, which is what the lib in libghostty marks. It's the machinery of How the Terminal Works, ready for any app that wants to embed a real terminal. On the other side, a new generation is being designed around agents: Warp now calls itself an "agentic development environment" and open-sourced its core — its pitch is orchestrating whole fleets of local and cloud agents from one window, the split-pane fleet from Many Terminals at Once promoted to a first-class product. And cmux builds terminal panes that agents can drive programmatically, through a Unix socket — a file-like endpoint two programs on one machine use to talk. Panes as an API, not just a view.

Here's the through-line, and it's the whole reason How the Terminal Works was worth your time: every one of these — the speed demons, the agent fleets, the socket-driven panes — still speaks the same protocol. Bytes through a PTY, escape sequences in-band, signals from the line discipline. A fifty-year-old interface turned out to be so simple, so universal, and so automation-friendly that when AI agents needed a body, they moved into the terminal. The machinery didn't get replaced by the AI era; it got adopted by it.

The agent is a shell command now

The adoption runs deeper than windows and panes. Headless agent CLIs make the agent itself a composable Unix tool: run claude -p ("print mode") and the agent reads stdin and writes stdout — the numbered streams from Redirection — and sets an exit code, the exact contract grep and sort have honored since 1973. Which means everything you learned in Text & Pipes and Permissions & Config applies, unchanged, to intelligence itself:

An AI agent in a pipeline — the same rules as any pipe
# Pipe a diff in, get a review out — stdin to stdout, like any tool
git diff main | claude -p "review this diff; list issues as filename:line"

# JSON output makes it pipeline-friendly: hand it to jq like anything else
# --oneline = one commit per line; -20 = only the last 20 of them
git log --oneline -20 \
  | claude -p "summarize this week's work in one paragraph" --output-format json \
  | jq -r '.result'

Read that first line again with Text & Pipes eyes: a program's output flowing through | into another program's input. The second program just happens to be a language model. It sorts into pipelines, redirects into files, chains with &&, and reports success through $? — the Unix philosophy, now with a very well-read tool in the toolbox. (The second block is one command typed across three lines: a trailing backslash tells the shell the line isn't finished, so it keeps reading instead of running what it has. Type that backslash and press Enter yourself and the > that comes back is bash saying "go on, I'm still listening" — not an error.)

Writing scripts that deserve the word "automation"

And that unlocks the last upgrade: scripts that orchestrate agents need to be sturdier than the five-liners from Scripts & Automation, and you finally know enough to write the grown-up kind. Every robust bash script starts with the same three lines of armor — each one cashing in a lesson from this part. set does nothing on its own: it flips switches that stay flipped for the rest of the script, and this particular combination is common enough to have a name — strict mode.

The grown-up script preamble
#!/usr/bin/env bash
set -euo pipefail
# -e  stop at the first failing command (no barreling on after an error)
# -u  treat unset variables as errors (catches $TYPO before it deletes ~)
# -o pipefail  a pipeline fails if ANY stage fails, not just the last

scratch=$(mktemp -d)             # -d = a directory, not a file: a fresh empty one in
                                 # the system temp dir (/tmp on Linux), new name each run
cleanup() { rm -rf "$scratch"; }
trap cleanup EXIT INT            # runs on normal exit AND on Ctrl+C

The line above the trap defines a function: cleanup() names a block of commands, the braces hold the body, and the ; before the closing brace is required. Defining it runs nothing — which is the whole point, because the next line hands the name to something that will run it later.

That something is trap, and it's the SIGINT lesson from How the Terminal Works cashed in: Ctrl+C sends a signal, signals can be caught, and trap is how a script catches one. Its first argument is the handler; the rest are signal names with the SIG dropped, so INT is SIGINT. EXIT isn't a signal at all — it's bash's own invention meaning "on the way out, however that happens", which is what makes the cleanup run on the ordinary path too. Put the armor on a real job and you get something like this: a script that runs an agent review over every file you've changed and collects the results —

review-changes.sh — an agent over every changed file
#!/usr/bin/env bash
set -euo pipefail

scratch=$(mktemp -d)
cleanup() { rm -rf "$scratch"; }
trap cleanup EXIT INT

git diff --name-only main > "$scratch/changed.txt"

while read -r file; do
  echo "reviewing $file ..."
  claude -p "review this file; list issues as line: problem" \
    < "$file" > "$scratch/$(basename "$file").review"
done < "$scratch/changed.txt"

cat "$scratch"/*.review > review-report.txt
echo "done: $(wc -l < "$scratch/changed.txt") files reviewed -> review-report.txt"

The loop is the one piece of shell grammar this course hasn't handed you yet. while read -r file; do done runs its body once per line: read takes the next line and drops it into $file, and -r tells it to leave any backslashes in that line alone rather than reading them as special. When the lines run out, read reports failure — and that failure is what stops the loop. The redirect hangs off done rather than off a command, which is what makes it feed the whole loop: read draws from the file instead of your keyboard. Inside, basename strips the folders off src/lib/foo.ts, because $file arrives as a path and a path can't be a flat filename inside $scratch.

The loop's sibling turns up in agent transcripts even more often: for walks a list instead of a file. for f in *.txt; do wc -l "$f"; done reads: expand the glob (Wildcards — the shell does it before the loop starts), park each name in $f in turn, run the body. Same skeleton — dodone — and the same quoting habit around "$f", for the same spaces-in-filenames reason as ever. You now read both of the loops the shell knows, which is most of the loop-shaped code an agent will ever ask you to approve.

When 'every night at nine' enters the picture
The missing piece between "a script I run" and "a script that runs itself" is cron, the scheduler that has shipped with every Unix since before screens. crontab -e opens your personal schedule in an editor; each line is five time fields, then a command — 0 9 * * 1-5 ~/backup.sh notes means minute 0, hour 9, any day of the month, any month, Monday through Friday. You met its manual pages back in Getting Help: man 5 crontab is the field-format page, and this is where that early example finally pays off. A real-machine tool — the sandbox has no clock ticking overnight — and the reason a server can feel staffed at 3 a.m.

Count the course in that script: redirection and pipes (Text & Pipes), a loop feeding on a file, scripting and variables (Scripts & Automation), signals and cleanup (this part), and an AI agent doing the reading — supervised by a script you can read line by line. If it fails halfway, set -e stops it; if you Ctrl+C it, the trap tidies up. This is what "advanced automation" actually looks like: not longer commands — stronger habits.

Vibe it

"Write me a bash script that runs claude -p over every file changed since main and collects the output into one report — with set -euo pipefail and a cleanup trap"

"Take my deploy script and harden it: strict mode, a trap that cleans up temp files on Ctrl+C, and mktemp instead of fixed /tmp paths — explain each change"

Tip
Take stock of where you're standing. You can narrate a keystroke's journey through the kernel, read the invisible protocol your terminal speaks to your editor, pipe a language model like it's grep, and write scripts that clean up after themselves when signals fly. That's a working model of the machine most day-to-day use never requires — and the tour is nearly over. One part to go: the send-off.
Challenge: What the Transcript Knows

Loading challenge...