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
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:
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:
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:
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.
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:
- 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.
- The line discipline recognizes the end of a line, converts it to a newline, and releases the whole buffered line to the slave end.
- 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
lsand parses it, expanding any$VARIABLES,~, and globs first. - It forks — makes a copy of itself — and
inside that copy runs exec, which
throws out the shell's program and loads
lsin 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. lsdoes its work and writes its results — text plus color escape sequences — to the slave end.- The bytes flow back through the PTY to the master; the emulator reads them, obeys the escapes, and paints glyphs into its character grid.
lsexits; 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
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:
# 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.
#!/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+CThe 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 —
#!/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 — do … done — 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.
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.
"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"
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.Loading challenge...