TerminalVibes — The Terminal for Vibe Coders

Welcome! Your AI assistant keeps proposing shell commands — this guide teaches you to read, verify, and run them with confidence. From your very first echo to auditing an agent's script, every concept is explained visually, then practiced hands-on. Three companions will follow you through every part:

14 parts35 playgrounds100% freeNo signup

Try it now: Open the — a simulated bash sandbox that runs entirely in your browser. Type anything; nothing here can touch your real files. No install required.

Stuck on anything: this course has its own built into the header — it has read every lesson here, answers from them, and links you back to the exact section. It can run commands in its own sandbox terminal to show you, and it asks permission first every time. Which is the whole course in miniature: the agent proposes, you approve.

Quick reference: Need a command fast? Hit ⌘K / Ctrl+K to search, or open the Terminal Cheat Sheet from the header for a complete command reference.

What Is the Terminal?

No buttons, no menus — you type a command, the machine answers, and everything is possible

The terminal is a text conversation with your computer. Instead of clicking icons, you type a command, press Enter, and the machine answers in text. That's the whole interaction model — and it hasn't fundamentally changed in fifty years, because it doesn't need to.

Three words get used almost interchangeably, but they name different layers:

Terminal

The window — an app that draws text on screen and sends your keystrokes onward. Terminal.app, iTerm2, and Windows Terminal are all terminals.

Shell

The program inside the window that reads your command, runs it, and prints the result. bash and zsh are shells — the language this course teaches.

Console

An older word from the days when the "terminal" was a physical desk of keyboard and screen. Today it's mostly a synonym for terminal.

If you build with AI assistants, the terminal is more relevant to you, not less. Every major AI lab now ships a terminal-native coding agent — Anthropic's Claude Code, OpenAI's Codex CLI, Google's CLI agents, Block's Goose, Sourcegraph's Amp. The CLI in those names is the command-line interface — the same command line this course is about. It's the execution layer where those agents actually work: install this package, run these tests, move those files. The agents speak fluent bash. With the terminal, you can:

  • Read every command an AI proposes before it runs — instead of approving blind
  • Verify what actually happened afterwards, with your own eyes
  • Compose small tools into pipelines that do exactly what you want
  • Automate anything you do twice — a saved command is a script

Plenty of developers use AI tools; far fewer trust what those tools actually run. Being able to read a shell command is what closes that gap — and there's even a benchmark for how well agents do real terminal work, if you want the numbers.

Text in, text out is also how language models work — which is why so many coding agents drive a shell. Learning to read that text is how you supervise them instead of hoping for the best. But where did this interface come from? The story is worth two minutes of your time.

A Brief History

From a spare PDP-7 in 1969 to every server, laptop, and AI agent on Earth

In 1969 at Bell Labs, a giant operating-system project called Multics had just been cancelled. Two researchers — Ken Thompson and Dennis Ritchie — salvaged the good ideas and rebuilt them small on a spare PDP-7 minicomputer. They called it Unix, partly as a joke on Multics.

Unix came with a radical idea: the operating system shouldn't decide what you can do. Instead it gives you dozens of small, sharp tools — each doing one thing well — and a shell to combine them. Type a sentence, press Enter, and the machine obeys. That design won so completely that macOS, Linux, and Android are all Unix descendants.

The shell itself kept evolving: Thompson's original, then Stephen Bourne's 1979 shell whose syntax became the standard, then bash in 1989 — a free rewrite that conquered the world on the back of Linux. Your Mac's default zsh is a bash-compatible cousin; everything in this course works the same in both.

Ken Thompson (seated) and Dennis Ritchie at Bell Labs, creators of Unix — an illustrated homage
1969 At Bell Labs, Ken Thompson and Dennis Ritchie start building Unix on a spare PDP-7 — and with it, the idea of typing commands at a shell.
1971 The first Unix shell ships: Thompson’s "sh". Pipes arrive two years later, and small tools start composing into big ones.
1979 Stephen Bourne’s shell becomes the Unix standard. Its syntax — the one you’ll learn here — is still what shells speak today.
1989 Brian Fox releases bash, the "Bourne Again SHell", for the GNU project — a decades-long effort to build an operating system anyone is free to use, change and pass on. Linux adopts it, and it spreads everywhere.
2019 macOS Catalina switches the default shell to zsh — a bash-compatible cousin. For everything in this course, they behave the same.
Today Servers — computers whose job is answering other computers — all speak bash. So do Docker containers, which wrap a program in a mini-Linux of its own, automated build-and-test runs (CI), and AI coding agents. "Bash-compatible" is the lingua franca of computing.
Why 'bash'?
It stands for Bourne Again SHell — a triple pun. It's a free-software rebirth of Stephen Bourne's classic shell — free as in free to use, change and pass on, not free as in no charge — written by Brian Fox for the GNU project in 1989. Programmers have never been able to resist a good name: Unix itself started as a joke on Multics.

Why does this history matter to you? Because the same design from 1969 — small tools, plain text, composable commands — is what AI agents reach for to act on your machine today. The terminal isn't legacy technology you're stuck with; small, sharp tools and plain text are simply still the most direct way to tell a computer what to do — whether the one typing is you or your agent.

Your Machine's Terminal

Whatever machine you're on, a bash-compatible shell is minutes away

You already own everything you need — every operating system ships a terminal. Pick yours:

You're in the best position: macOS is Unix. The built-in Terminal.app (in Applications → Utilities) is all you need — and it's better than its reputation: macOS 26 "Tahoe" (2025) gave it its first real modernization in about 24 years, with 24-bit true color (roughly 16.7 million shades, where older terminals were stuck with 16 or 256), Powerline font support, and new themes. There's nothing to install on day one. Many developers later upgrade to iTerm2 for extra comfort — same shell inside, nicer window around it.

Since macOS Catalina (2019), the shell your terminal launches for you — its default, until you go and change it — is zsh. For this entire course, zsh and bash behave identically; every command works in both. You can check what you're running: the $ makes the shell swap in the value of SHELL, which is why a path comes back and not the word (Environment Variables):

Which shell am I in?
echo $SHELL
# /bin/zsh
PowerShell is a different language
Windows also ships PowerShell and the older cmd — Command Prompt, cmd.exe. They are real shells — but they speak a different language (dir instead of ls, Remove-Item instead of rm). This course teaches bash, the language of macOS, Linux, servers, and AI agents. On Windows, always make sure you're in a WSL or Git Bash window before following along.

And for now, you don't even need that: the built into this site is a simulated bash sandbox that runs in your browser — identical commands, zero risk.

Anatomy of a Prompt

The prompt answers three questions before you type a single letter: who, where, and whose turn

Open a terminal and you're greeted by a line of cryptic text ending in a blinking cursor. That line is the prompt — the shell saying "your turn." It looks intimidating, but it's just three answers packed into one line:

vibe@sandbox:~/projects$

vibe@sandbox

Who and where: you're logged in as user vibe on a machine named sandbox. Mostly ignorable — until you're SSH'd into a server, secure shell, where your keystrokes land on a computer somewhere else (A Door to Another Machine), and the name is what saves you from running a command on the wrong machine.

~/projects

Your current directory — where commands will act. The ~ is shorthand for your home folder. Moving Around is all about moving this around.

$

"Your turn": the shell is ready for a command. A $ here means you're a normal user; a # in someone else's screenshot means they're running as root — the administrator account (sudo).

The blinking cursor

Where your typing goes. Nothing runs until you press Enter — you can type, stare, and edit as long as you like. Press the up arrow to recall previous commands instead of retyping them. And when this course writes Ctrl+C, that's one motion — hold Control, press C. The capital letter is for legibility; you never add Shift. On a Mac, ⌘ is Command and ⌃ is Control, the glyphs printed on the keys.

Two characters do more than one job, and both are worth pinning down now. A # at the start of a line in this course's code blocks is the machine's reply, not something you type — command and answer share one pane. On a real command line, a # tells the shell to ignore the rest of the line, which is how people leave notes in scripts. And you've already met the third sense a few inches up: as a prompt symbol, it means root.

The dollar sign is the other one, and its two jobs are unrelated. Other people's docs print a leading $ to mean "this is a line you type" — that one you never type. But a $ stuck to a name, like the one in echo $SHELL, is real syntax: the shell replaces the name with its value before the command ever runs (Environment Variables). That one you do type. This course never prints a bare $ prompt, so every $ you meet here is the second kind.

Every interaction with the shell follows the same loop — you'll run it thousands of times, and your AI agents run exactly the same one:

Prompt, command, Enter, output, new prompt. Master this loop and the rest of the course is just vocabulary.

Tip
Prompts vary from machine to machine — yours might show a different name, extra colors, or even a git branch. The parts are always the same: who@where:directory$. In Make It Yours you'll learn to customize it yourself.
Part 1

First Contact

"Every conversation with a computer begins the same way: a prompt, a command, and Enter."

Time to actually touch the thing. In this part you'll open a terminal on your own machine, run your first real commands, and learn how to ask for help when a command confuses you — three skills that carry you through everything else. Nothing in this part can harm your computer, and we'll prove it as we go.

1.1 Opening Your Terminal

The terminal has a fearsome reputation, so let's start by deflating it: it's just an app. It sits next to your browser and your music player, it opens like them, and it closes like them. Here's where to find yours.

Behind the intimidating black window is an ordinary application — open it like any other
Note
The Problem: You've decided to learn the terminal, but you've never deliberately opened one. Where does it even live?

macOS

Press ⌘ Space — hold Command and tap Space — to open Spotlight, the macOS search bar. Type Terminal, press Enter. That's it. (The long way: Applications → Utilities → Terminal.app.) The stock app is a genuinely good place to start — since macOS 26 "Tahoe" it has true color and modern themes, so there's nothing to install today. Pin it to your Dock while it's running (right-click its icon → Options → Keep in Dock) — you'll be back.

Linux

Press Ctrl+Alt+T on most desktops, or find Terminal / Console in your application menu.

Windows

Open Windows Terminal from the Start menu — it's been the default console since Windows 11 22H2, so it's already there — then pick your Ubuntu (WSL) profile from the dropdown next to the new-tab button. If you chose Git Bash instead, open Git Bash from the Start menu. Either way, make sure you're in a bash window — not PowerShell — before following along (the intro explains the difference).

It's just an app
Opening a terminal does nothing to your computer. Closing the window does nothing either (it just ends that conversation). You can open five of them side by side. The terminal only acts when you type a command and press Enter — and you haven't typed anything yet.

Whatever you opened, you should now be looking at a mostly-empty window with a prompt and a blinking cursor — the shell, waiting for your first words.

Vibe it

"How do I open a terminal on my machine, and which shell is it running?"

"Set up Windows Terminal so it opens my WSL Ubuntu shell by default"

1.2 Your First Commands

A command is a word — sometimes followed by more words — typed at the prompt. Nothing happens while you type. Nothing happens while you hesitate. The shell only acts when you press Enter. Let's have your first conversation.

Ask a question, press Enter, get an answer — your first commands are pure conversation
Note
The moment: A blinking cursor is waiting for you, and you have no idea what the machine will understand — or what's safe to try.

Start with a question the machine can't get wrong — who are you? In every code block from here on, the first line is what you type and the # lines are the machine's reply (Introduction has the full legend).

Ask who you are
whoami
# vibe

The shell answered with your username. Now make it say something — echo repeats whatever you give it, and the quotes hold the two words together as one message instead of two separate ones (Paths has the rule):

Make it talk
echo "Hello, terminal"
# Hello, terminal

Ask for the time, and then wipe the slate clean:

Two more essentials
date
# Sat Jul 12 10:15:42 PDT 2026

clear
# The screen empties - the text is still there, just scrolled out of sight

The screen went blank but nothing was deleted: that's the terminal's scrollback, and scrolling up brings every answer back. The shell also keeps its own record of the commands you typed (History Superpowers) — a separate list, and clear leaves that alone too.

Now the important experiment: type something wrong on purpose. Misspell a command and press Enter:

Break it (you can't)
ecoh "hello"
# bash: ecoh: command not found

Read that reply back like a sentence in three parts: bash is who's complaining — the shell, not your command — ecoh is the word it choked on, and command not found is the complaint. Every error you meet from here on has that shape; only the complainer changes.

Errors are feedback, not failure
Nothing broke. The shell didn't recognise the word, said so, and gave you a fresh prompt. Error messages in the terminal are answers. Typos cost you nothing but a retry (press the up arrow, fix the spelling, Enter).

Try It: Say Hello to the Machine

Run these commands in a simulated bash sandbox right here in your browser. Type help in the terminal for the full command list.
Say Hello to the Machine

Loading playground...

Vibe it

"What does the echo command do, and where would I actually use it?"

"Give me five more harmless commands I can try to explore my terminal"

1.3 Getting Help

You will never memorize every command — nobody does. What experienced people memorize instead is how to look things up. The terminal ships with its own documentation, and it's always three keystrokes away.

Every command carries its own manual — you're never more than a man page away from an answer
Note
The Problem: You've met a command you don't understand — maybe one your AI assistant just proposed — and you need to know what it does before you run it.

Two ways in, and you'll use both. --help prints a compact summary of what a command does and which flags it accepts, straight to your terminal. man opens the full manual. Which one answers you depends a little on your machine — we'll get to that — but between them, nothing stays a mystery for long.

Flags first, because that summary is mostly a list of them. A command line is a sentence. The first word is the program. Everything after it is either a flag — a request to behave differently — or an argument, the thing to act on. The shell tells them apart by one character: a leading dash. ls -a means "run ls, and switch on the a behaviour". ls a means "run ls on something called a", which is a different question entirely and usually an error. That dash is doing real work; it is not decoration.

The shell chops your line at the spaces and hands over the pieces one at a time, which is why a filename with a space in it needs quoting. Four rules cover almost everything else you'll meet:

  • One dash, one letter. -a, -l. Short, fast, and what you type day to day.
  • Two dashes, a whole word. --all, --help, --version. Longer to type, far easier to read six months later in a script.
  • A comma joins two spellings of the same flag. When help output or a manual page prints -a, --all, that is one flag with two names. Use either. Never both.
  • Single letters cluster. ls -la is ls -a -l written faster; add a tls -lat — and you've asked for them sorted newest first as well. Only single letters cluster this way; --all --long doesn't squash into anything.
A flag letter belongs to its command, not to the terminal
Every command decides for itself what its own flag letters mean. There is no master list, and the shell never consults one — so the same letter means different things depending on the word in front of it. -r means recursive to cp but reversed to sort. -i means ask me first to rm but edit the file in place, no undo to sed. Capitals are separate flags again — -r and -R are two different requests. So "look it up" is a skill rather than an admission: the only thing that settles what a letter means here is --help or man for this command.
The quick summary — on Linux, WSL and Git Bash
ls --help
# Usage: ls [OPTION]... [FILE]...
# List information about the FILEs (the current directory by default).
#   -a, --all        do not ignore entries starting with .
#   -l               use a long listing format
# ...

That Usage: line has a shorthand of its own: square brackets mean optional, ALL-CAPS words are placeholders you swap for your own, and a trailing ... means "as many of these as you like". OPTION is the older word for a flag and you'll see both, often on the same page. So ls [OPTION]... [FILE]... reads: any number of flags, then any number of files, none of it required. Nothing in brackets is ever typed literally.

On a Mac, that same command is an error — and that's fine

Mac and Linux carry two different lineages of the same veteran tools. macOS ships BSD's versions; Linux — and WSL and Git Bash with it — ships GNU's. GNU tools nearly all answer to --help. Most of the BSD ones have never heard of it, and say so:

The same command on macOS
ls --help
# ls: unrecognized option `--help'
# usage: ls [-@ABCFGHILOPRSTUWXabcdefghiklmnopqrstuvwxy1%,] [--color=when] [file ...]

Same three parts as ecoh earlier, and the same reassurance: nothing broke, and you weren't punished for trying. Here the complainer is ls rather than the shell — it names the one thing it didn't understand, then prints its usage line anyway — so you still got the flag list, just terser and stripped of the descriptions. That's the shape of most terminal errors. Read the words; they usually say precisely what tripped them up.

ls, du, head, cut, wc, stat and date all behave this way on a Mac. So does grep, which quietly prints its usage line without even calling it an error. The tools you install yourself later — curl, git, tar, jq, brew — take --help everywhere, Macs included. For everything else on a Mac, the answer is the manual.

This course uses angle brackets for the same job — when you see man <command>, you type man ls, never the brackets. That matters more in a shell than elsewhere: < is a real operator in its own right, so typing the brackets produces an error that points nowhere near your mistake.

For the full story — and, on a Mac, for most of the built-in commands — there's man — the manual, installed with your system since before the web existed. It comes in numbered sections, because one name can mean two things: crontab is both a command you run and a file format that command reads, so man 1 crontab and man 5 crontab are two different pages. Section 1 is commands, section 5 is file formats. Leave the number off and you get the first match, which is nearly always the one you meant:

The full manual
man ls
# Opens the complete manual page for ls

man -k download
# wget(1) - The non-interactive network downloader.
# ...

The second is for when you don't know the command's name yet. -k is for keyword: it searches every manual page's one-line description and prints each match with its section number in brackets. Your machine will list many more than this one.

Worth knowing where that search stops, though, because it will bite you. It reads only that one-line description, not the page. curl downloads files for a living, and man -k download does not find it — its description reads transfer a URL, and the word "download" appears nowhere in it. So a -k search that comes back thin means your keyword didn't match anyone's phrasing, not that no such command exists. Try a synonym before you conclude anything.

Press q to quit
man opens in a pager — a full-screen reader that takes over your terminal, and the one you'll get is less (Looking Inside Files). Scroll with the arrow keys or Space, search by typing /word and pressing Enter, and press q to get your prompt back. Everyone gets "stuck" in a pager once — now you won't.

Man pages are thorough but dense. For a friendlier take, the community-written tldr pagestoo long; didn't read — show each command as a handful of practical examples. tldr doesn't come with your system, though: it's a third-party command you install first (Package Managers). Nothing to install to read it, happily — the web version is at tldr.inbrowser.app, which is where the output below comes from.

tldr: examples first
tldr tar
# tar
#   Archiving utility.
#   - Create an archive from files:
#     tar cf target.tar file1 file2 file3
#   - Extract an archive to the current directory:
#     tar xf source.tar
# ...

Those cf and xf look like they've lost their dash, and they haven't: tar is old enough to accept its letters bare, so tar cf and tar -cf are the same command. It's one of the few commands in this course that gets away with it — assume the dash everywhere else.

A third way to get help

You have a third source of help the man-page authors never imagined: paste any command into your AI assistant and ask "what does this do, flag by flag?" It's the fastest explanation you'll get — and it works in reverse too, when the AI is the one proposing the command. The AI vendors know it: Anthropic's Claude Code docs now ship their own "never opened a terminal before" guide — if you use a coding agent, being able to read its commands is part of using it safely. But language models can be confidently wrong about flags, so close the loop:

Ask the AI, verify with the manual
Let the assistant explain the command in plain English, then skim the man page (or --help, where it works) to confirm the flags do what it claimed. Explanation from the AI, ground truth from the machine — that habit is the heart of Scripts & Automation, and you can start it today.

Try It: Read the Manual First

Before you run an unfamiliar command, look it up. Open head's manual with man head, then use what it taught you to save the first three lines of a log file — the running record a program writes as it works (Searching Text).
Read the Manual First

Loading playground...

Vibe it

"Explain this command flag by flag: ls -laht"

"What is crontab, and why does man 5 crontab show something different from man 1 crontab?"

Challenge: Look It Up First

Loading challenge...

Part 2

Moving Around: Your Mental Map of the Machine

"In the terminal you are always standing somewhere. The first skill is knowing where."

Every command you run — and every command an AI suggests — executes somewhere: inside one specific folder on your machine. Half of all beginner terminal confusion is really just location confusion: the command was fine, but you were standing in the wrong place. This part builds your mental map. You'll learn to ask where you are, read a path like an address, move around freely, and create and inspect files — all without touching a mouse.

Important
This three-beat rhythm — pwdlscd — is the terminal equivalent of looking up from your phone to check the street signs. Run it any time you feel lost, and always before running a command an AI wrote for you: most AI-suggested commands quietly assume you're standing in the project folder.

2.1 Where Am I?

A fresh terminal window is quiet: a prompt, a blinking cursor, and no other clues. But you're not floating in a void — the shell has already placed you inside a folder, called your working directory. Two commands turn the void into a map.

pwd tells you where you're standing; ls shows what's around you
Note
The scenario: Your AI assistant says "run npm install in the project folder." Node is the program that runs JavaScript outside a browser, and npm is how a Node project fetches its dependencies — the code other people wrote that it leans on rather than rewriting. All of it lands in a node_modules folder, routinely hundreds of megabytes of it. Which folder is your terminal actually in right now?

pwd ("print working directory") answers the question in one line:

Ask the shell where you are
pwd
# /home/vibe

That answer — /home/vibe — is a path, the full address of the folder you're standing in (paths get their own section next). It also names your home directory — the folder that belongs to your account, where your files, settings and downloads live. Every account on the machine gets one, and this one is the home of the account named vibe — you. Now look around with ls ("list"):

See what's in the current directory
ls
# documents  notes.txt  projects

pwd

"Where am I?" — prints the full path of your working directory. Instant, harmless, run it constantly.

ls

"What's here?" — lists the files and folders in the current directory (or any path you give it).

ls -l

"Tell me more" — one file per line with sizes, dates, and permissions: who's allowed to read or change each file.

The Hidden Layer: Dotfiles

Plain ls is holding out on you. Any file or folder whose name starts with a dot — a dotfile — is hidden by default. That's where configuration lives: .bashrc, your shell's own settings file (Your Shell Config); .gitignore; and the .env file holding your API keys — an API being a door a service opens for other programs rather than for people, which is why that file is worth hiding (curl — Ask the Network). Add -a ("all") to see them:

Reveal hidden files
ls -a
# .  ..  .bashrc  .config  documents  notes.txt  projects
Tip
Notice the first two entries: . and ... They're not files — they're built-in nicknames for "this directory" and "the parent directory," and they show up in every folder. They're about to become very useful in the next section.

A First Peek at ls -l

The long listing packs a lot into each line. For now, read just three things: the leading d means "directory," the number before the date is the size in bytes, and the name is at the end. A byte is one character's worth of storage, so a 118-byte file holds roughly 118 characters. The rest of that cryptic string is the permissions code, and we decode it fully in Permissions & Config.

The long listing — details per file
ls -l
# total 12
# drwxr-xr-x  3 vibe staff 4096 Jul 10 09:14 documents
# -rw-r--r--  1 vibe staff  118 Jul 12 08:30 notes.txt
# drwxr-xr-x  4 vibe staff 4096 Jul 11 17:02 projects

Three things in that block mislead on sight. total 12 is not a count of the files — it's how much disk they take up, measured in blocks rather than bytes. The small number just after the permissions is a link count, not a second size. And the two names beside it, vibe staff, are the file's owner and its group. All three get taken apart in Reading ls -l.

Note
Single-letter flags cluster: ls -la is ls -l -a typed faster — "long listing, including hidden files," the most common ls invocation in the wild. Only single letters cluster this way, and only behind one dash (Getting Help). (On Windows, dir is the Command Prompt cousin of ls — but inside WSL, or inside Git Bash, which bundles a bash shell into Windows, ls works exactly as shown.)
Vibe it

"What directory is my terminal in right now, and what project files are here?"

"List everything in this folder including hidden files, and explain what each dotfile is for"

2.2 Paths

Every file on your machine has an address, and that address is called a path. Learn to read paths and the whole filesystem snaps into focus — it's one big upside-down tree, and you can point at any leaf from anywhere.

The filesystem is a tree growing down from / — a path is directions through its branches
Note
The Problem: Your AI says "add your key to ~/projects/app/.env". Is that a file? A folder? Where is it? You can't follow directions written in a language you can't read.

Everything starts at the root, written as a single slash / — the one folder every other folder lives inside. Everything else hangs somewhere beneath it, and a path is the walk from one point to another, with / between each step. Fair warning, because the word gets reused: this root is a place. The other root you'll meet is a person, the administrator account in sudo, and the two have nothing to do with each other.

Reading the tree: the file .env lives at /home/vibe/projects/app/.env — root, then home, then vibe, then projects, then app.

The branches sitting beside home at the top belong to the machine rather than to you, and a handful come up often enough to name. /bin and /usr/bin hold binaries — the programs themselves. /etc holds system-wide configuration — that's what /etc/hosts in the table below is, the short list of names your machine resolves itself before asking the network. /tmp is scratch space anyone can write to and the machine may clear without asking. And /usr, despite the spelling, is where the system's own software lives — it has nothing to do with users.

Absolute vs. Relative

An absolute path starts at the root with / and works from anywhere — a full street address. A relative path starts from wherever you're currently standing — "two doors down." Both name the same file:

Same file, two addresses
# Absolute: works no matter where you are
cat /home/vibe/projects/app/README.md

# Relative: works if you're standing in /home/vibe
cat projects/app/README.md

The Four Shortcuts Every Path Uses

SymbolMeansExample
/The root — the very top of the tree/etc/hosts
~Your home directory (here, /home/vibe)~/projects/app
.The current directory — "right here"./script.sh
..The parent directory — one level up../other-project

And they compose. Standing in ~/projects/app, the path ../../documents means "up two levels, then into documents." Now you can re-read the AI's instruction: ~/projects/app/.env is a hidden file named .env, inside app, inside projects, inside your home directory.

Important
Treat TAB completion as a habit, not a convenience. Type the first few letters of any path and press TAB: the shell finishes the name for you. Nothing happens? Press TAB twice to see every possible completion. A completed path is a path that exists, spelled exactly right, with spaces and special characters escaped for you. Most people who look fast in a terminal are completing, not typing. Try it: cd pro, TAB, watch it become cd projects/. That trailing slash is a marker meaning "this one's a folder" — the shell mostly doesn't care either way, but the habit stops you misreading your own commands later.
Tip
Spaces are the classic path trap: cd My Projects tells the shell to cd into "My" and hands it a mystery second word. Wrap it in quotes (cd "My Projects") — or just TAB-complete, which escapes the space automatically. Better yet, name your own folders with dashes: my-projects.

Those quotes are worth understanding properly, because you'll be reaching for them for the rest of the course. There are two kinds, and one difference that matters. Single quotes are literal — nothing inside them means anything to the shell, so '$HOME' stays five characters instead of turning into the path to your home directory, and '*.txt' stays five characters instead of becoming a list of files. Double quotes still let $NAME and $(...) expand into their values (Environment Variables) while still protecting the spaces. Reach for single quotes when you want the shell to keep its hands off, double quotes when you want a value in the middle of a phrase.

A backslash does the same job for exactly one character: My\ Projects is one filename, not two words — which is what TAB completion is doing when it "escapes" a space for you. Anything the shell would otherwise read as punctuation — a space, a *, a $, a quote mark — can be defused by putting a backslash in front of it.

Vibe it

"Explain this path to me piece by piece: ~/projects/app/.env"

"Give me the absolute path to my current directory and a relative path from here to my home folder"

Try It: Mind the Gap

Two folders here have spaces in their names — the classic trap. Quote the path to cd "My Projects", then leave a shipped.txt behind.
Mind the Gap

Loading playground...

2.3 Changing Directories

You can read the map; now walk it. cd ("change directory") moves you to any path you can name — and since you just learned to name every path on the machine, you can now go anywhere.

cd moves you through the tree; cd - is the 'back' button
Note
The setup: Your project lives at ~/projects/app but your terminal opens in ~. Every command the AI gives you assumes you've made the trip.
The moves you'll use every day
cd projects/app   # Walk down into a folder (relative path)
pwd               # /home/vibe/projects/app — always verify
cd ..             # Up one level, to projects
cd ~              # Jump home from anywhere (plain 'cd' does the same)
cd -              # Bounce back to wherever you just were

That last one is a gem most tutorials skip: cd - toggles between your two most recent locations — perfect when you're hopping between a project and a config folder. It's also one of the rare places a lone dash is a value rather than a flag (Getting Help) — here it stands in for "the last place I was," not for a setting. And remember the habit from 2.2: type cd, a few letters, then TAB your way down the tree.

Tip
You can't fall off the map. cd never creates, changes, or deletes anything — it only moves your point of view. Mistype a name and the worst you get is no such file or directory, a completely harmless error. Genuinely lost? cd ~ teleports you home from anywhere. Wander boldly.
Note
One subtlety worth knowing early: your location belongs to this terminal window. Each tab or window keeps its own working directory, and where you are affects nothing outside the shell — your editor, your files, and other terminals don't move when you do.
A real (simulated) filesystem runs in your browser. Somewhere in the nested folders below your home directory hides a lost API key — use pwd, ls -a, and cd to hunt it down. Type help for the full command list.
Find the Lost API Key

Loading playground...

Vibe it

"Take me to my project folder and confirm where I ended up"

"I'm lost in some deep directory — get me back home and show me the path I took"

2.4 Making Things

So far you've been a tourist — looking, never touching. Time to build. Two commands create the skeleton of every project you'll ever start: mkdir for folders and touch for empty files.

mkdir grows new branches; touch sprouts empty files
Note
The scenario: The AI proposes a project layout — "create a src folder with a components subfolder, plus a README." You could click through a file manager… or type one line.
Create folders and files
mkdir notes            # One new folder in the current directory
mkdir src tests docs   # Three at once — src is short for "source"
touch README.md        # One new, empty file
ls                     # Verify — trust, but verify

Plain mkdir has one rule: the parent must already exist. Ask it for a nested path and it refuses:

Nested folders need -p
mkdir src/components/buttons
# mkdir: cannot create directory 'src/components/buttons': No such file or directory

mkdir -p src/components/buttons   # -p = parents: create every missing one on the way
# (no output — in the shell, silence means success)
Tip
mkdir -p is also re-runnable: if the folders already exist, it succeeds quietly instead of erroring. That's why AI-generated setup scripts always reach for -p — the command is safe whether it's the first run or the fifth.

touch has a funny origin story: its real job is updating a file's "last modified" timestamp — touching it. But if the file doesn't exist, touch creates it, empty. That side effect became its main job: it's the standard way to say "make me a blank file right here."

A whole project skeleton in three lines
mkdir -p my-app/src/components my-app/tests
touch my-app/README.md my-app/src/main.js
ls -R my-app   # -R walks into every folder inside, and those folders' folders too

Nothing on the machine requires that shape — src, tests and docs are a convention other people will recognize, not a rule anything enforces. And that walking-into-everything behaviour has a name, recursive, which comes back in Copying as the flag that lets copying and deleting reach a whole tree at once.

Vibe it

"Set up a folder structure for a small Python project — show me the mkdir commands before running them"

"Create the standard src/tests/docs skeleton in this directory and list the result"

2.5 Looking Inside Files

An AI agent just wrote a config file into your project. Before you trust it, you want to read it — without the ceremony of opening an editor. The terminal has a whole toolkit for exactly this, graded by file size.

cat for a glance, less for a read, head and tail for the edges
Note
The question: "What did the agent actually put in config.yaml?" Reading files is how you verify work — an instinct this whole course keeps returning to.
cat — dump the whole file to the screen
cat config.yaml
# port: 8080
# debug: false
# database: ./data/app.db

That file is YAML — a config format built to be read by humans, one key: value to a line. (A config file being any file that changes how a program behaves without changing the program itself.)

A few other extensions keep turning up and work the same way. .csv is comma-separated values: one record per line, the fields split on commas, the first line usually naming the columns. .md is Markdown — plain text with light formatting marks, like # for a heading. .sh is a shell script. Every one of them is ordinary text underneath: a file of characters, each character stored as a number. That's why the commands below read all of them equally well, and why an extension is a convention for humans and editors rather than the thing that makes a file work.

cat (short for "concatenate") is perfect for short files — it prints everything and returns your prompt. But cat a 10,000-line log and it firehoses past you. For anything long, you want a pager:

less — read at your own pace
less server.log
# Opens a full-screen reader:
#   space / b     page down / up
#   arrow keys    scroll line by line
#   /error        search for "error" — press Enter to run it (n = next match)
#   q             QUIT — back to your prompt
Important
Remember q. Getting "trapped" in a full-screen pager is every beginner's rite of passage: the prompt vanishes, typing does strange things, panic sets in. Press q and you're free. This matters double because man pages (First Contact) open in less too — same keys, same escape hatch.

While we're here, four escape hatches worth memorizing now — because each one has stranded somebody for an embarrassingly long time. Two are characters you type; two are chords, held down together in one motion. The comments say which is which:

How to get out of things
q            # type it — any pager: less, or a man page. One keypress, no Enter
:q!          # type it — vim. Press Esc FIRST, then these three, then Enter (:wq saves instead)
Ctrl+X       # chord — nano. It says so at the bottom, but nobody reads it
Ctrl+C       # chord — a running command, or a line you typed but haven't run

:q! is the famous one. Git — the tool that records snapshots of a project as you work (Your Shell Config) — drops you into vim on most machines to write the note that goes with a snapshot. The keyboard stops behaving, there's no visible way out, and the answer is Esc then :q! then Enter, which leaves without saving. You are not expected to learn vim today; you're just expected to be able to leave.

The one editor you can walk into on purpose
Those escape hatches treat editors as places you fell into. nano is the one worth entering deliberately: nano notes.txt opens the file, you type the way you'd type anywhere, and the only two chords that matter are printed at the bottom of its screen the whole time — Ctrl+O then Enter to save ("write out"), Ctrl+X to leave. It ships on nearly every Linux machine and every Mac, and the day A Door to Another Machine lands you on a server with a config file to fix, this is the tool you'll reach for. (Not in the playground — its files are edited with echo and friends; nano is a real-machine comfort.)

Sometimes you only care about the edges of a file — the header row of a CSV, or the most recent lines of a log. That's head and tail:

head and tail — just the edges
head server.log        # First 10 lines
head -n 3 server.log   # First 3 lines
tail server.log        # Last 10 lines — where the newest log entries live
tail -n 20 server.log  # Last 20
Tip
Out in the real world (beyond this playground), tail -f ("follow") keeps the file open and streams new lines as they're written — the classic way to watch a server log live while your app runs. Press Ctrl+C to stop following.

cat

Short files. Whole thing, instantly.

less

Long files. Scroll, search, q to quit.

head

The beginning. Headers, first impressions.

tail

The end. Fresh log lines, latest entries.

Try It: Build Your Workspace

Put the whole part together: navigate with cd, build a project skeleton with mkdir -p and touch, then verify your work with ls and cat.
Build Your Workspace

Loading playground...

Vibe it

"Show me the last 30 lines of the newest log file in this project"

"Read the config file the agent just created and explain each setting to me"

Challenge: Scaffold It From Here

Loading challenge...

Part 3

Copy, Move, Delete — and the Habits That Keep You Safe

"The terminal assumes you mean exactly what you say. Deleting is the moment to be sure you do."

In Moving Around you learned to look and to create. Now come the tools that change things: copy, move, rename, delete. They're wonderfully fast — one line does what fifty drag-and-drops would — and they come with a responsibility the graphical world hid from you: there is no confirmation dialog, and for rm, no trash can. This part teaches the commands and, just as deliberately, the habits that make them safe.

Important
The safety net in the terminal isn't a feature — it's a ritual: look before you act (ls first), prefer interactive flags (-i) while learning, and preview every wildcard with echo before letting it loose. Every habit in this part exists because someone, somewhere, lost a day's work not having it.

3.1 Copying

Copying is the gentlest of the three power tools — the original is never touched. It's also your cheapest insurance policy: one cp before an experiment means there's always a way back.

cp duplicates: the original stays put, the copy goes where you point
Note
The stakes: You're about to let an AI agent rewrite config.yaml. If its "improvement" breaks everything, you want the original back in one command.

cp always reads the same way: source first, destination second — "copy this, to there." What "there" means depends on what the destination is:

The two shapes of cp
# Destination is a new name: copy + rename in one step
cp config.yaml config.yaml.bak

# Destination is an existing folder: copy INTO it, same name
cp config.yaml backups/
Tip
That first line is the backup-before-experiment pattern, and it's worth making a reflex: before any risky edit — yours or an agent's — cp file file.bak. Restoring later is just the reverse: cp config.yaml.bak config.yaml. (a Git-free time machine, until you have Git.)

Copying Folders Needs -r

Ask cp to copy a directory and it declines — a folder can contain thousands of files, and cp wants you to say you mean it:

cp -r for directories
cp projects backup-projects
# cp: -r not specified; omitting directory 'projects'

cp -r projects backup-projects   # -r = recursive: the folder and everything inside
Warning
cp overwrites silently. If the destination file already exists, cp replaces it — no question asked, old contents gone. While you're learning, add -i ("interactive"): cp -i config.yaml backups/ stops and asks overwrite? before clobbering anything — type y or n and press Enter, and a bare Enter counts as no. That -i belongs to cp, though, not to the terminal: sed takes the same letter and means something far sharper by it (Editing in Place). This same silent-overwrite rule returns with mv in the next section — it's a theme.
Vibe it

"Back up my config file before you change anything, and tell me the restore command"

"Copy the whole src folder to src-backup so we can experiment safely"

3.2 Moving & Renaming

Here's a small mind-bender: the terminal has no "rename" command, because it doesn't need one. Renaming is moving — moving a file to a new name in the same place. One command, mv, does both.

mv relocates or renames — same command, and the original doesn't stay behind
Note
The Problem: The agent scaffolded your project but named the main file untitled.py, and dropped three data files in the project's top folder that belong in data/. Tidy it up.

Like cp, it's always source first, destination second — and the same two shapes:

mv — rename or relocate
# Destination is a new name: RENAME
mv untitled.py main.py

# Destination is an existing folder: MOVE into it
mv sales.csv users.csv logs.csv data/

# Both at once: move AND rename
mv draft.md docs/chapter-1.md

The middle line is the fine print behind "source first, destination second": once you name more than two things, the last one has to be a folder, and it has to exist already. Neither cp nor mv will make one for you — with no docs/ yet, the last line stops at No such file or directory, and you fix it with mkdir docs and run it again.

Two nice surprises compared to cp: mv moves whole folders without needing -r (on the same disk it just relabels the folder's address — instant, even for gigabytes), and since nothing is duplicated, there's no copy lying around to get stale. Move to a different disk, though — an external drive, a USB stick — and mv quietly becomes a full copy followed by a delete: slow, and interruptible halfway through.

Warning
The overwrite danger, part two. If the destination name already exists, mv replaces it silently — and unlike cp, you lose both versions of the story: the destination's old contents are gone, and the source no longer exists under its old name. mv notes.txt ideas.txt when ideas.txt already exists destroys ideas.txt. The fix is the same reflex: mv -i asks before overwriting, and ls the destination first when in doubt.
Vibe it

"Rename untitled.py to main.py and move every CSV in this folder into data/"

"Reorganize these files into sensible folders — show me the mv commands before running them"

3.3 Deleting (Carefully)

This is the most important safety lesson in the entire course. Everything else in the terminal is forgiving — errors are harmless, cd can't hurt you, cp leaves originals alone. rm is the exception. Read this section twice.

rm doesn't move files to a trash can — it erases them, immediately and permanently
Caution
rm has no trash can. When your file manager "deletes" a file, it moves it to the Trash, where it sits recoverable for weeks. rm does no such thing: the file is gone the instant you press Enter. No confirmation, no undo, no recovery. Treat every rm as permanent, because it is.
The rm family
rm old-notes.txt        # Delete one file. Permanently.
rm draft1.md draft2.md  # Delete several
rmdir empty-folder      # Delete a folder — only works if it's EMPTY
rm -r old-project       # Delete a folder and everything inside it
rm -f missing.txt       # -f = force: no complaints if it doesn't exist

Each flag is reasonable alone. -r recurses into folders; -f suppresses prompts and "no such file" errors so scripts run clean. Combined as rm -rf, they mean: delete everything at this path, recursively, without asking, without stopping. Now add the two ingredients from your recent lessons and you have the terminal's most famous disaster recipe:

rm -rf — no questions

Recursive and forced: nothing slows it down, nothing asks "are you sure?"

+ a wildcard

* expands to "everything here" — however much that turns out to be (next section).

+ the wrong directory

You thought you were in ~/tmp. You were in ~/projects. The pwd habit exists for this moment.

A single mistyped space does it too: rm -rf old-app / (note the stray space) is not "delete old-app/" — it's "delete old-app, and also the entire filesystem starting at root." This exact typo has destroyed production servers. It's why modern systems refuse plain rm -rf / without an extra flag — but they won't save you from ~ or a wrong folder.

Files that start with a dash
A file named -old.txt jams the grammar from Getting Help: rm -old.txt reads as a fistful of flags, not a filename, and the error complains about an invalid option instead of the real problem. Two ways out: rm -- -old.txt — the double dash means "everything after this is a name, no matter what it looks like," and nearly every command honors it — or point at the file with a path, rm ./-old.txt. Worth knowing before some download names itself something dashed.

The Ritual: ls First, Then rm

Here's the habit that makes rm boring instead of scary. Never aim rm at anything you haven't just looked at. List first, confirm with your eyes, then recall the command (up-arrow) and swap ls for rm — same targets, guaranteed:

Look, confirm, then delete
ls old-project          # 1. LOOK: what exactly is in there?
pwd                     # 2. CONFIRM: am I where I think I am?
rm -r old-project       # 3. Same target you just inspected — up-arrow, edit, Enter
Important
The AI angle — this is a stop-and-read moment. When a coding agent proposes a command containing rm -rf, do not rubber-stamp it. Read three things before approving: the exact path (absolute? relative to what?), any wildcard (what could it expand to?), and the working directory the agent is running in. Agents make location mistakes exactly the way beginners do — they just make them faster. Deletion is the one category of command you always read in full — and Terminal for the AI Era later turns that instinct into a complete auditing method.
Tip
Training wheels while you learn: rm -i asks before every single deletion, and rm -I (capital i) asks once when deleting more than three files — a good permanent default. And for anything precious, the calmer move is often mv: relocate to a scrap folder today, delete the scrap folder next week. The exception is build artifacts — whatever the project's build leaves behind, the step that turns your source files into something you can run. Delete those freely; running the build again writes them straight back.

Try It: Clean the Downloads Mess

A cluttered downloads folder awaits: inspect what's there with ls and cat, make folders, sort keepers into them with mv, and rm the junk — practicing the ls-first ritual where deletion can't hurt you.
Clean the Downloads Mess

Loading playground...

Vibe it

"You suggested rm -rf — walk me through exactly what that path resolves to before I approve it"

"Delete the build artifacts in this project, but list everything you plan to remove first"

3.4 Wildcards

Everything so far has operated on files one name at a time. Wildcards — also called globs — let one pattern stand for many names: "every .log file," "all the photos from January." This is where selecting files by hand in a file manager starts to feel slow.

A glob is a net: the shell casts it over the directory and hands your command whatever it catches
Note
The mess: The agent's test run left 40 .tmp files littered through this folder. Deleting them one by one would take ten minutes. One pattern does it in a second — if you can trust what the pattern matches.
PatternMatchesExample
*Any run of characters (including none)*.logapp.log, errors.log
?Exactly one characterpage?.htmlpage1.html, not page12.html
[abc]One character from the set (ranges like [0-9] work too)report-[12].txtreport-1.txt, report-2.txt

The Big Idea: the Shell Expands the Glob, Not the Command

This is the one concept that makes wildcards predictable instead of magical. When you type rm *.tmp, the rm program never sees a star. The shell expands the pattern against the current directory first, then runs the command with the resulting list of plain filenames:

Two consequences follow directly. First, what a glob matches depends entirely on where you are — the same *.tmp is three files in one folder and three hundred in another (this is the wildcard half of the rm -rf disaster from Deleting (Carefully)). Second, since expansion happens before any command runs, you can ask a harmless command to show you the expansion:

The habit: echo the glob first
echo *.tmp
# cache1.tmp cache2.tmp scratch.tmp    <- exactly what rm would receive

rm *.tmp                               # Now you KNOW what this deletes
Important
Echo the glob first. echo just prints its arguments — so echo <pattern> is a free, safe preview of any wildcard. (ls <pattern> works too; the angle brackets mark the spot you fill in and are never typed — the convention from Getting Help.) Make it a reflex before every destructive glob, and apply it to AI-proposed commands: when an agent suggests a command containing a wildcard, echo that pattern in the target directory before you approve the real thing.

Globs compose with everything you already know — they're just a way of writing file lists:

Wildcards with cp, mv, ls
cp *.md drafts/           # Copy every Markdown file into drafts/
mv photo-0?.jpg january/  # Move photo-01.jpg through photo-09.jpg
ls report-[12].txt        # List just report-1.txt and report-2.txt
ls src/*.js               # Globs work inside a path too — but only one folder deep

That last line is where globs stop: one never crosses a /. src/*.js looks inside src and gives up there — it will not find src/lib/util.js, and no arrangement of stars will make it. When a mess is spread down through subfolders the pattern is the wrong tool, and walking a whole tree is find's job: find . -name '*.tmp' would have caught those 40 files wherever they'd landed. It has a section waiting in Finding Files.

Warning
Two classic surprises: * does not match hidden dotfiles (a small mercy — rm * spares your .env), and a pattern that matches nothing is passed to the command literally, star and all — which is why a typo'd glob often produces the baffling error cannot access '*.tpm'. Both surprises are caught instantly by the echo habit.
Braces look like a glob — and aren't one
One more pattern-shaped thing, so it never fools you: mkdir -p src/{components,lib}. Those braces don't match anything — they expand, unconditionally: the shell rewrites the line into mkdir -p src/components src/lib before mkdir ever runs. A glob asks "what's already here?"; braces say "make me these." That's why braces work for files that don't exist yet — exactly where a glob would come back empty-handed — and why coding agents lean on this shape every time they scaffold a project. You'll read it far more often than you'll type it.

Try It: Select Exactly the Right Files

A folder full of similar names — your job is to write patterns that catch exactly the right ones and nothing else. echo every glob before you commit to it; that's the skill being tested.
Select Exactly the Right Files

Loading playground...

Vibe it

"Write a glob that matches all the 2024 log files but not the 2025 ones, and echo it first"

"Move every image file in this folder into photos/ — show me what the wildcard matches before moving"

Challenge: Clean Up After the Agent

Loading challenge...

Part 4

Text & Pipes: How Small Tools Combine

"Every command speaks plain text. Pipes let them talk to each other."

So far you've run one command at a time and read its output on screen. Here you'll capture output into files, feed one command's output into another, and search, count, and reshape text the way experienced people do. This is where the terminal stops feeling like a museum piece: once output can move into files and into other commands, small tools multiply — and you'll recognize the same style in a lot of AI-suggested one-liners.

Important
The big idea of this whole part: in the terminal, everything is text. Logs, file listings, error messages, command output — all plain text. Once you can redirect text and pipe text, every small tool you know multiplies every other one.

4.1 Redirection — Point the Output Somewhere Else

Every command writes its results to something called standard output — which is normally your screen. Redirection says: "don't print that, put it in a file instead." One character does it: >.

Redirection reroutes a command's output from the screen into a file
Write, overwrite, append
echo "hello" > greeting.txt      # Create the file (or OVERWRITE it!)
cat greeting.txt
hello

echo "hello again" >> greeting.txt   # >> APPENDS to the end
cat greeting.txt
hello
hello again
Caution
> truncates. The instant you press Enter, the target file is emptied — before the command even runs. Redirect into an existing file and its old contents are gone, no confirmation, no trash can. When in doubt, use >> (append) or redirect to a new filename. And when an AI hands you a command containing >, check what's on the right side of the arrow before you run it.

Standard output isn't the only channel — every command is born with three. Standard input (stdin) is where it reads from, normally your keyboard; standard output (stdout) is where its results go; and standard error (stderr) is a separate channel for its complaints, kept apart so a failure never gets mixed in with the results. They're numbered 0, 1 and 2 — which is the whole story of the 2 in 2>. A plain > captures stream 1 only, so errors still land on your screen. To catch those too:

Capturing errors with 2>
ls reports/ missing/ > listing.txt 2> errors.txt

cat listing.txt      # The successful part
q1-summary.md
q2-summary.md

cat errors.txt       # The complaint went to its own file
ls: missing/: No such file or directory

You'll also meet > all.log 2>&1 in AI-generated commands, and it reads left to right: send stream 2 to wherever stream 1 is currently pointing, which the first half of the line has just set to all.log. The &1 is what makes it a reference to a stream rather than a filename — drop the & and you create a file called 1. Finally, the arrow points the other way too: < feeds a file into a command as its input, as in sort < names.txt. It's rarer — most commands happily take a filename argument — but it completes the picture: arrows move text in and out of files.

One member of the family points both ways at once. tee — named for the T-shaped pipe fitting, and it is exactly that — sits in the middle of a stream, writes everything flowing through it into a file, and passes the same text along untouched. It's the answer to a want the plain arrow can't satisfy: "show me this and keep a copy."

tee — watch it and save it
npm run build | tee build.log
# The build scrolls past as usual — AND lands in build.log

npm test 2>&1 | tee test.log     # errors included, still watching live

With a plain > you trade the live view for the file; tee refuses the trade. That makes it the receipt habit for anything long-running — let an agent's build talk to the screen while tee keeps the transcript, and "what did it actually say?" always has an answer. (tee -a appends instead of overwriting, the same courtesy >> extends.)

Vibe it

"Run the build and save all output — including errors — to build.log so I can read it later"

"Append today's date and a one-line status to my notes.txt without overwriting what's there"

Try It: Catch the Red Text

List one real file and one missing one, then split the streams — the useful listing into found.txt, the scary error into errors.txt — with > and 2>.
Catch the Red Text

Loading playground...

4.2 Pipes — Small Tools, Composed

Redirection sends output into a file. The pipe — | — sends it into another command. Whatever the left command prints becomes the right command's input, no temporary file needed. This one character is the reason terminal users never left.

A pipe connects the output of one command to the input of the next
Your first pipes
ls ~/Downloads | wc -l        # How many things are in Downloads?
47

history | tail -5             # The last 5 commands you ran

cat server.log | grep "ERROR" | wc -l   # How many errors in the log?
12

Two small things in there. history prints the shell's own record of what you've typed — it keeps one, whether or not you asked, and History Superpowers puts that record to work. And tail -5 is shorthand for tail -n 5: for head and tail, a bare dash-and-number is a line count. Not every dash-and-number is: the 9 in kill -9 is a signal number, which Stopping Things unpacks.

Read a pipeline left to right, like an assembly line: the text flows through each station and comes out transformed. Here's the last example as a picture:

This is the Unix philosophy, and it's older than most programming languages you know: "Write programs that do one thing and do it well," as Doug McIlroy — the inventor of the pipe — put it. grep only searches. wc only counts. sort only sorts. None of them is impressive alone — but because they all speak plain text, any of them can feed any other, and a handful of tiny tools becomes thousands of combinations.

Tip
Build pipelines one stage at a time. Run the first command alone and look at its output. Then add one | and look again. This isn't just how you write pipelines — it's how you audit the ones your AI writes. A five-stage pipeline you can't follow is really five one-stage commands you haven't run yet.
Vibe it

"Explain this pipeline stage by stage before I run it: cat access.log | grep POST | wc -l"

"Write me a pipeline that counts how many files in this folder end in .png"

4.3 Searching Text — grep

A server just crashed and the log is 10,000 lines long. Nobody scrolls that. grep prints only the lines that match a pattern — it's the terminal's search box, and probably the most-used tool in this course.

grep keeps only the lines that match — the haystack goes in, the needles come out

That 10,000-line log is a log file — a plain text file a program keeps appending to as it works, one line per event, oldest at the top. Most programs tag each line with a severity word: DEBUG for chatter, INFO for normal, WARN and ERROR for trouble. That convention is why searching for the bare word ERROR works at all.

grep basics
grep "ERROR" server.log          # Lines containing ERROR
2026-07-11 14:02:11 ERROR db connection refused
2026-07-11 14:02:15 ERROR retry limit reached

grep -r "TODO" src/              # Search every file under src/
src/app.js:14:  // TODO: handle empty cart
src/utils.js:3: // TODO: remove this hack

Both patterns above are plain words, which is why nothing surprising happened. The moment you reach for a wildcard, though, you've changed languages. grep doesn't use globs. It uses regular expressions — a second, older pattern language that looks deceptively similar and means different things. Here * means "zero or more of whatever came just before it", . matches any single character, and ^ and $ anchor a match to the start and end of a line. The trap isn't hypothetical: ERR* as a glob means "starts with ERR", but as a regular expression it means "ER followed by any number of Rs" — so it matches ER anywhere in any line, and quietly hands you the wrong answer.

CharacterIn a glob (filenames)In a regular expression (grep)
*Any run of charactersZero or more of the character before it
?Exactly one characterAn ordinary question mark
.An ordinary dotAny single character

The glob column is the one you drilled in Wildcards, and it still governs every filename you type. The regex column governs what's inside the quotes you hand to grep, and later to sed and awk. Same characters, different room.

Five flags cover 95% of real-world grep:

-i — ignore case

Matches error, Error, and ERROR. Use it by default when hunting in logs.

-n — line numbers

Prefixes each match with where it lives — essential when you're about to open the file and fix it.

-v — invert

Keep the lines that don't match. Perfect for filtering noise out: grep -v "DEBUG".

-c — count

Print how many lines matched instead of the lines themselves — a built-in | wc -l.

-r — recursive

Search a whole directory tree instead of one file. grep -rn "api_key" . is how developers answer "where in this codebase is that used?"

And because grep reads standard input, it slots into any pipeline as a filter. This is the pattern you'll type every single day:

grep as a pipeline filter
history | grep "cd"              # Every cd you've ever run
tail -50 server.log | grep -i "error"   # Only recent errors
grep "ERROR" server.log | grep -v "retry"  # Errors, minus the noisy ones

Try It: Find the Crash

A server died last night and server.log has hundreds of lines. Use grep — with -i, -n, and -v to cut the noise — and pipes to wc -l to pin down exactly when and why it crashed.
Find the Crash

Loading playground...

Vibe it

"Search server.log for errors, ignoring case, and show me the line numbers"

"Find every file in this project that still mentions the old function name 'fetchUser'"

4.4 Counting & Shaping — wc, sort, uniq, cut

grep finds lines. These four tools reshape them: count them, order them, de-duplicate them, and slice out columns. Individually they're almost boring — together they answer real questions, like "who's hammering my website?"

Four tiny tools: count with wc, order with sort, collapse with uniq, slice with cut
The cast, one line each
wc -l server.log        # Count lines (-w words, -c bytes)
843 server.log

sort names.txt          # Alphabetical order (-n numeric, -r reversed)

uniq names.txt          # Collapse REPEATED ADJACENT lines into one

cut -d',' -f2 users.csv # Slice column 2, using ',' as the delimiter

That last line hides a rule: some flags take a value. -d',' is one flag — d for delimiter — with its value jammed straight onto the letter, and -f2 is f for field, value 2. Other commands want a space between the two, as in -n 3, and many take either. So -f2 is not two clustered flags the way ls -la is — where the letter ends and its value begins is each command's own decision, and man cut is what tells you.

Warning
The uniq gotcha: uniq only collapses duplicates that are next to each other. Given a b a, it removes nothing. That's why it practically always appears as sort | uniqsort herds the duplicates together first, then uniq collapses them.

Try It: Count Before You Fix

Before ranking anything, answer the simplest question: how many? Pipe grep ERROR server.log into wc -l to count the error lines, and save the total to a file.
Count Before You Fix

Loading playground...

Now the payoff. Your web server writes one line per visit to access.log, and you want to know your top visitors. This is the most famous pipeline in Unix history, and we'll build it one stage at a time — exactly the way you should build every pipeline.

Stage 0 — look at the raw material
head -4 access.log
203.0.113.9 GET /home
198.51.100.4 GET /pricing
203.0.113.9 GET /docs
192.0.2.55 GET /home

Two things to read before you start slicing. The number at the front is the visitor's IP address — each machine on a network has one, and What Is localhost? takes it apart. And /home and /pricing are URL paths — the part after the site's address — not folders on this machine. Same slashes, different tree.

Stage 1 — cut: keep only the IP column
cut -d' ' -f1 access.log
203.0.113.9
198.51.100.4
203.0.113.9
192.0.2.55
...

# -d' ' = columns are separated by spaces; -f1 = give me field 1
Stage 2 — sort: herd the duplicates together
cut -d' ' -f1 access.log | sort
192.0.2.55
198.51.100.4
198.51.100.4
203.0.113.9
203.0.113.9
203.0.113.9
...
Stage 3 — uniq -c: collapse and count
cut -d' ' -f1 access.log | sort | uniq -c
   1 192.0.2.55
   2 198.51.100.4
   3 203.0.113.9
...

# -c = count: each collapsed group carries a tally of how many lines it stood for.
# (Not grep's -c, which prints one total and no lines at all.)
Stage 4 — sort -n: rank by the count
cut -d' ' -f1 access.log | sort | uniq -c | sort -n
   1 192.0.2.55
   2 198.51.100.4
   3 203.0.113.9

# Biggest number last — your top visitor is 203.0.113.9
Tip
Read the finished pipeline aloud and it's a sentence: "take column one, group the duplicates, count each group, rank by count." When you can narrate a pipeline like that, you own it — whether you wrote it or an AI did.

Try It: Build a Pipeline

access.log is waiting in the playground. Build the classic pipeline stage by stage — cut, then | sort, then | uniq -c, then | sort -n — and identify your site's top visitor. Run each stage before adding the next.
Build a Pipeline

Loading playground...

Vibe it

"From access.log, show me the top 5 most-requested pages with their counts"

"How many unique visitors are in this log file? Walk me through the pipeline you use"

4.5 Finding Files — find

grep searches inside files. But sometimes the question is "where is that file?" — a config you know exists, every Markdown file in a project, that one script you wrote last month. find walks an entire directory tree and prints every path that matches your criteria.

find walks the whole tree and reports every path that matches
find essentials
find . -name '*.md'            # Every Markdown file below here
./README.md
./docs/setup.md
./docs/notes/ideas.md

find . -name '*.md' -type f    # -type f: files only
find . -type d -name 'test*'   # -type d: directories only
find ~ -name '.zshrc'          # Start the search from your home folder
Caution
Quote the pattern. Write '*.md' with quotes, not bare *.md. Remember Copy, Move, Delete: the shell expands wildcards before the command ever runs. Unquoted, *.md becomes a list of the Markdown files in your current folder — and find receives that list instead of the pattern, searching for the wrong thing entirely. Single quotes are the fully literal kind, so the pattern reaches find intact and gets applied at every level of the tree — the rule for both kinds of quote is in Paths.

Keep the two search tools straight — they're a team, not rivals:

find — searches filenames

"Where are the files called *.md?" It never opens a file; it only looks at names, types, and locations.

grep — searches contents

"Which lines contain TODO?" It reads inside files but doesn't care what they're named.

And naturally, they compose. Find the files by name, then grep inside exactly those:

find + grep, together
find . -name '*.js' | xargs grep -n "TODO"
./src/app.js:14:  // TODO: handle empty cart
./src/utils.js:3: // TODO: remove this hack

# (For a whole tree, grep -rn "TODO" . is the simpler everyday version.)

xargs is there because grep wants filenames as arguments, not on standard input. Pipe a list of paths straight into grep and it dutifully searches the text of those paths instead of the files they name — a pipeline that runs, returns nothing, and looks like an answer. xargs collects the lines arriving on standard input and lays them out as arguments, which is the shape grep was waiting for. Reach for it when you want to narrow the file set first — by name, type or depth — and reach for grep -rn when you want everything under a folder.

Try It: Hunt Down Every TODO

A project tree is scattered with leftover TODOs. Use find . -name '*.js' -type f to locate the source files (quote that glob!), then combine with grep to list every TODO with its file and line number.
Hunt Down Every TODO

Loading playground...

Vibe it

"Find every Markdown file in this project, including ones in nested folders"

"List all the TODO comments left anywhere in src/ with their file names and line numbers"

Notice what all of these have in common: they find and count — none of them changes a single character. That's deliberate, and it's why they're safe to experiment with. When you're ready to make text different — swap a word across a file, drop every DEBUG line, pull one column out of a table — that's sed and awk, waiting in Text Surgery.

Challenge: Who Is Actually Visiting?

Loading challenge...

Part 5

Permissions & Environment: How the Machine Decides

"Permission denied and command not found aren't errors. They're the system explaining its rules — once you can read them."

Every terminal novice hits the same two walls: a script that refuses to run, and a command the shell claims doesn't exist. Both have one-line fixes — but only if you understand the two systems behind them. This part decodes permissions (who may read, write, and run each file) and the environment (the invisible settings every command inherits). After this, those two error messages become friendly signposts.

Note
Unix was built for many people sharing one computer, so every file carries a tiny rulebook about who can do what. Fifty years later you're probably the only human on your laptop — but the AI agents running commands on it are "users" too, which makes these rules more relevant than ever.

5.1 Reading ls -l — The 10-Character Rulebook

You met ls -l back in Moving Around and politely ignored the cryptic string at the start of each line. Time to decode it — those 10 characters are the entire permission system in miniature.

Ten characters, three audiences: the file's complete rulebook at a glance
A long listing, line by line
ls -l
-rw-r--r--  1 vibe  staff   214 Jul 10 09:12 notes.txt
-rwxr-xr-x  1 vibe  staff   512 Jul 10 09:30 deploy.sh
drwxr-xr-x  4 vibe  staff  4096 Jul  9 18:02 projects

Before those ten characters, the two names in the middle: they decide who the rules apply to. vibe is the owner, and staff is the group — a named set of accounts the system can grant permission to all at once. On a shared server that's your team; on your own laptop it's usually staff or a group named after you with nobody else in it, which is why the group's rules rarely bite until someone else is on the machine. id -gng for group, n for the name rather than the number — prints the name of yours. Owners can be reassigned: chown ("change owner") hands a file to a different account, and it's a root-only move you'll mostly meet on servers and around Docker, where files created inside a container come out owned by somebody who isn't you. Recognize it when an agent proposes it; on your own laptop you'll almost never type it.

Take -rwxr-xr-x apart and it's four pieces: one type character, then three groups of three — the same three questions asked of three audiences:

- type
- file, d dir, l link
rwx user
the owner (you)
r-x group
your group
r-x other
everyone else

In each triad: r = read, w = write, x = execute, and - = "no, this one's off."

So -rwxr-xr-x reads as: a regular file; the owner can read, write, and run it; the group can read and run it; everyone else can read and run it. And -rw-r--r-- is a typical document: only the owner can edit, everyone can read, nobody can execute. projects opens with a d because it's a directory; the third type character you'll meet is l, a symbolic link (Links & Where Things Live).

Tip
For a directory, the letters shift meaning slightly: r = list its contents, w = create or delete files inside it, and x = enter it with cd. That's why directories almost always carry the x.
Vibe it

"Run ls -l in this folder and explain what each permission string allows"

"What does -rw-r--r-- mean, and why can't I execute a file with those permissions?"

5.2 chmod — Changing the Rules

Here's the classic novice moment: you save a script, try to run it, and the terminal snaps back Permission denied. Nothing is broken. The file simply doesn't have its x bit yet — files are never executable by default. The .sh on the end isn't what runs it either; that's a convention for humans and editors (Looking Inside Files), and the file would run just as well named backup. chmod ("change mode") flips the switch that does.

chmod +x flips the execute switch — the one-line fix for 'Permission denied'
Note
The situation: You (or your AI) created backup.sh, but running it fails with permission denied.
The fix, start to finish
./backup.sh
bash: ./backup.sh: Permission denied

ls -l backup.sh
-rw-r--r--  1 vibe  staff  164 Jul 11 10:04 backup.sh   # no x anywhere

chmod +x backup.sh          # Grant execute permission
ls -l backup.sh
-rwxr-xr-x  1 vibe  staff  164 Jul 11 10:04 backup.sh   # x appears

./backup.sh                 # Now it runs
Backing up projects/ ... done.

+x is a small grammar rather than a magic word. + adds a permission and - removes it, and you can put an audience in front: u for you, g for your group, o for everyone else, a for all three. A bare +x means a+x, which is why the line above became -rwxr-xr-x and not just -rwxr--r--. When you only want it for yourself, say so: chmod u+x backup.sh.

And the leading ./ isn't decoration. Typed bare, backup.sh sends the shell hunting through the short list of folders it keeps for programs — Environment Variables names that list — and the folder you're standing in is deliberately not on it, so nothing dropped into your current directory can impersonate a real command. ./ means "the file right here": you saying you meant this one.

You'll also see chmod used with numberschmod 755, chmod 644 — in every tutorial and AI suggestion. Each digit is one audience (user, group, other), built from r=4, w=2, x=1. But you don't need the arithmetic — in practice there are exactly two recipes worth knowing:

755 — scripts & directories

rwxr-xr-x: you do everything; everyone else may read and run it — or, on a directory, go inside it. The standard for anything executable.

644 — regular files

rw-r--r--: you edit, others read, nobody executes. The standard for documents, configs, and code.

Caution
If a tutorial or an AI ever suggests chmod 777 — everyone may read, write, and execute — treat it as a red flag, not a fix. It "solves" permission errors by turning the rulebook off entirely, and chmod -R 777 does it to a whole tree. There is almost always a narrower fix — usually +x on one file.

Try It: The Script Won't Run

A deploy.sh script is sitting in the playground refusing to run. Diagnose it with ls -l, grant the missing permission with chmod +x, then run it with ./deploy.sh.
The Script Won't Run

Loading playground...

Vibe it

"My script says permission denied when I run it — what's wrong and what's the safest fix?"

"Make setup.sh executable and run it, but show me its contents first"

5.3 sudo — Borrowed Superpowers

Some things your everyday account simply may not touch: installing system-wide software, editing files outside your home folder, managing services. For those, there's sudo — "superuser do." It runs one command as root, the administrator account that no rule applies to.

sudo lends you root's powers for exactly one command — handle with respect
What sudo looks like in the wild
apt install htop                 # Linux: htop is a live view of what's running
E: Permission denied             # E: is apt marking its own error, the way bash: marks bash's

sudo apt install htop            # Same command, run as root
[sudo] password for vibe:        # Type your password — nothing appears. Normal!
Setting up htop ... done.

Two things to know before you ever need it. First, the password prompt shows nothing while you type — no dots, no asterisks. Every beginner assumes their keyboard broke; it's just a privacy habit from 1970 that stuck. Second, you need sudo far less than the internet implies: everything inside your home folder is already yours, and macOS users installing via Homebrew usually don't need it at all. If a command fails inside ~, the answer is almost never sudo — it's a wrong path or a missing +x.

Note
Windows note: Windows 11 (24H2 and later) now ships a real built-in sudo command — turn it on under Settings → System → For developers → "Enable sudo". So everything in this section transfers to native Windows too, not just WSL; inside WSL, sudo has always worked exactly as described here.
Caution
Never sudo a command you don't understand — especially one an AI wrote. Every safety net you've learned assumes the system can say "no" to you. sudo removes the "no." An AI-suggested sudo rm -rf with the wrong path doesn't delete your project — it can delete your operating system. This isn't hypothetical: 2025 and 2026 saw repeated, well-documented incidents of AI coding agents running destructive commands on real systems — Terminal for the AI Era dissects them. The rule: when a command fails with "permission denied," don't reflexively re-run it with sudo. First ask why it was denied — read the command, check the path, ask your AI to explain each flag. Escalate only when you can narrate what the command does.
Note
The playground deliberately has no real sudo — try it there and it will politely explain itself. Everything in this course's sandbox already runs inside your own pretend home folder, where you don't need superpowers. Exactly like real life.
Vibe it

"This install command failed with permission denied — explain why before suggesting sudo"

"You proposed a command with sudo in it. Walk me through every flag and what could go wrong"

5.4 Environment Variables — The Invisible Settings

Every command you run inherits a set of named values called the environment: your username, your home folder, your preferred editor, and — most importantly — where the shell should look for commands. You've been using them all along without noticing; cd with no arguments reads $HOME to know where to take you.

The environment: a backpack of named values every command carries
Reading and setting variables
echo $HOME                # $NAME reads a variable
/home/vibe

echo $USER
vibe

env | head -4             # env lists everything currently set
HOME=/home/vibe
USER=vibe
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash

export EDITOR=nano        # Set one, and hand it down to what this shell starts
echo $EDITOR
nano

The $ is what does the work: write $EDITOR and the shell swaps in the value before the command ever sees it, which is why echo $SHELL prints a path and not the word SHELL. Note the asymmetry — you write export EDITOR=nano with no dollar sign and read it back with one. And export is the word that decides who else can see it: a bare EDITOR=nano stays in this shell, while export EDITOR=nano hands the setting down to every program the shell launches from here on. Either way it evaporates when you close the terminal — making it stick is the next section's job.

$HOME and the ~ from Moving Around name the same folder but aren't the same kind of thing. ~ is shell shorthand, expanded only at the start of a word and never inside quotes; $HOME is a real variable and expands anywhere a $ does. So cp report.md "~/backups/" never reaches your home folder: the quotes turn the ~ into an ordinary character, and cp goes looking for a folder literally named ~ in the directory you're standing in. Write "$HOME/backups/" and it lands where you meant. (This is one of the few places the playground is kinder than a real shell: its paths expand ~ either way, so the quoted version quietly works there and bites you on your own machine.)

The variable that explains a thousand error messages is PATH: a colon-separated list of directories — /usr/bin, /usr/local/bin and friends, where bin is short for binaries, the programs themselves. When you type python, the shell walks that list in order, looking in each directory for an executable file with that name. First match wins; no match anywhere means the infamous:

command not found, demystified
echo $PATH
/usr/local/bin:/usr/bin:/bin

pyhton --version
bash: pyhton: command not found      # 1. Or it's just a typo...

python --version
bash: python: command not found      # 2. ...or it's not installed...
                                     # 3. ...or it's installed somewhere
                                     #    that isn't on PATH.

which ls                             # 'which' shows where a command lives
/bin/ls
which python                         # Silence/nothing = not on PATH

--version earns its place there: it prints the version number and exits without doing anything, so it's the harmless way to ask whether a program exists at all. which answers the other PATH question — when two copies of a tool are installed, the one it prints is the one you're running. Because the shell stops at the first match, a copy in /usr/local/bin quietly shadows the system's in /usr/bin, which is how you end up on a version you didn't mean to run.

Tip
command not found always means exactly one of three things: a typo, a tool that's not installed, or a tool installed outside PATH. Check them in that order — which and echo $PATH settle it in seconds. This is also why installers keep telling you to "add something to your PATH": they put the tool in a folder your shell doesn't search yet.
Caution
One kind of variable needs extra care: secrets. API keys are usually handed to programs as environment variables, and the tempting move — typing export API_KEY=sk-... straight into your terminal — writes the key into your shell history, where it stays. Keys & Secrets covers the safe pattern: keep keys in a .env file, lock it with chmod 600, and reference $API_KEY instead of the value.

Try It: command not found

A tool called greet is installed somewhere in the playground, but typing greet only gets you command not found. Inspect echo $PATH, hunt the executable down with find, and run it — by its full path, or by fixing PATH.
command not found

Loading playground...

Vibe it

"I installed a tool but the terminal says command not found — diagnose it step by step"

"Show me my PATH as a readable list and tell me which directory each command I use comes from"

5.5 Your Shell Config — Make It Stick

Everything you've customized so far — exported variables, the aliases you're about to meet — dies with the terminal window. The fix is the shell config file: a plain text file of commands your shell runs automatically every time it starts. It's ~/.bashrc if your shell is bash, and ~/.zshrc for zsh — the macOS default. (A hidden dotfile, which is why ls -a from Moving Around is how you spot it.)

Your .bashrc / .zshrc runs at every shell startup — customizations live here

The customization you'll use most is the alias — a personal shorthand for a longer command:

Aliases: your own shorthand
alias ll='ls -lh'         # Define it — -l long listing, -h human-readable sizes
ll                        # Use it — the shell expands ll to ls -lh
-rw-r--r--  1 vibe staff 1.2K Jul 11 09:14 notes.txt
-rwxr-xr-x  1 vibe staff  512 Jul 10 09:30 deploy.sh

alias                     # List every alias currently defined

That 1.2K is -h doing its job: the raw byte counts Moving Around showed you, rounded into units you can read at a glance. Typed at the prompt, an alias lasts only for that session — put it in your config file to keep it forever. Here's a battle-tested starter set to paste into ~/.zshrc (or ~/.bashrc):

Starter aliases for your config file
# --- TerminalVibes starter aliases ---
alias ll='ls -lh'               # Detailed listing, human-readable sizes
alias la='ls -lah'              # ...including hidden dotfiles
alias ..='cd ..'                # Hop up one level
alias ...='cd ../..'            # Hop up two
alias gs='git status'           # You will type this constantly
alias grep='grep --color=auto'  # Highlight what matched

One of those needs a word. Git is the version-control tool that records snapshots of a project as you work — a whole subject of its own, and deliberately not this course's (Keep Learning). You're pasting gs now because it's the first command anyone reaches for the moment they meet git; nothing between here and the end depends on understanding it.

The other line you'll end up pasting into this file is the one installers keep asking for. When a tool lands in a folder your shell doesn't search, "add it to your PATH" means exactly one line here:

The PATH line every installer wants
export PATH="$PATH:$HOME/.local/bin"
# Read it right to left: take PATH's current value, add one folder, save it back

The $PATH on the right-hand side is the read/write asymmetry from Environment Variables doing real work: it expands to the old list first, so you're appending, not replacing. Drop it — export PATH="$HOME/.local/bin" — and every command on the machine goes command not found at once, because you just told the shell to search one folder and nothing else. No lasting harm done (the broken value dies with the terminal window, and the file isn't re-read until a new shell starts), but it's a memorable minute.

One last piece: the config file only runs at startup, so a freshly edited file changes nothing in your current terminal. Either open a new window — or reload it in place with source:

Apply config changes to the current shell
source ~/.zshrc     # Re-run the config file right here, right now
# (bash users: source ~/.bashrc)

source is doing something more general than reloading a config, and it's worth seeing what. Run a file of commands the ordinary way — ./deploy.sh, or bash deploy.sh — and you start a child shell: a fresh copy of the shell that reads the lines, does them, and exits, taking its variables and its cd with it. Nothing it changed survives. source does the opposite, running the lines in the shell you're sitting in, so everything sticks. It's also why a script full of cd never seems to move you anywhere.

One more place changes hide: ~/.zshrc and ~/.bashrc are read every time you open a terminal window, while ~/.zprofile and ~/.bash_profile are read only at login. If an edit stubbornly refuses to take, you probably edited the other one.

Tip
Your shell config is just a shell script — every line in it is a command you could type by hand. That means everything in this course applies to it: read it with cat, search it with grep, and when an installer says "we added a line to your .zshrc," you can open it and see exactly what changed. No magic, ever.

Try It: Make Your Shortcuts

Time to build your own shorthand. Define an alias with alias, use it, list your aliases, and append your favorite to ~/.bashrc with echo >> so it would survive a restart.
Make Your Shortcuts

Loading playground...

Vibe it

"Suggest five aliases based on the commands I run most, and add them to my .zshrc"

"Something in my shell config is slowing down my terminal startup — help me find it"

Challenge: Nothing in the Kit Will Run

Loading challenge...

Part 6

Scripts & Automation: Teach the Machine Your Routine

"Anything you have typed twice is something the machine should be doing for you."

You have been writing one-line programs since Text & Pipes without calling them that. A script is only the next step: those same commands, saved in a file, run whenever you like. This part turns you from someone who types commands into someone who keeps them — and teaches the exit-code logic that decides whether the next command runs at all. It is also the part that makes agent-written scripts stop being mysterious, because you will have written the same shapes yourself.

Note
This is the last piece of ordinary bash you need. Everything after it is a specific toolkit — text surgery, processes, the network — and every one of those parts assumes you can read a small script and follow a && chain.

6.1 Your First Script

A script is just commands you saved — teach the file once, run it forever
Note
The chore: You keep typing the same three commands to back up your notes. And your AI agents keep leaving mysterious .sh files in your projects. Both problems have the same answer: a shell script is nothing more than commands saved in a file.

That's the idea. Everything you've typed at the prompt this entire course could be pasted into a file and replayed. Let's build one — a tiny backup script — and hit every ingredient along the way.

backup.sh
#!/usr/bin/env bash
# Back up the notes folder with today's date in the name.

BACKUP_NAME="notes-backup-$(date +%F)"

mkdir -p backups
cp -r notes "backups/$BACKUP_NAME"
echo "Backed up notes to backups/$BACKUP_NAME"

Three new things in eight lines:

The shebang: #!/usr/bin/env bash

The first line of every script tells the system which program should interpret the rest of the file. #!/usr/bin/env bash means "run this with bash, wherever bash lives on this machine" — which is why it's preferred over hard-coding a path like /bin/bash. There's no magic in it: env is itself a program — the one that printed your variables in Environment Variables — and handed a name, it looks that name up on PATH and runs it. Everything after that first line is what you'd type at the prompt, and the # lines are notes to yourself, using the comment character from Introduction.

Variables

BACKUP_NAME="..." creates a variable (no spaces around the = — bash is strict about that), and $BACKUP_NAME uses it — the same dollar-sign expansion you met with environment variables in Environment Variables. There's no export in front of it, so this one stays inside the script and no program the script launches will ever see it.

Quoted paths

"backups/$BACKUP_NAME" — double quotes, by the rule in Paths: the variable still expands, and any spaces stay put. Quoting variables is the habit that separates scripts that work from scripts that work until a filename has a space in it.

Now make it runnable. Two steps, both from chmod: give the file execute permission, then run it with the explicit ./ path. The .sh on the end is convention for humans and editors — the execute bit and that first line are what actually run it.

Make it executable, then run it
chmod +x backup.sh
./backup.sh
# Backed up notes to backups/notes-backup-2026-07-12

The ./ is there for the reason that section gave: a bare backup.sh sends the shell hunting through PATH, and this folder deliberately isn't on it. Running it this way also starts a child shell, so the script's variables and any cd inside it are gone the moment it exits — exactly the distinction Your Shell Config drew.

Arguments: $1 makes it reusable

Inside a script, $1 is the first argument the script was handed — the words after its name (Getting Help), numbered in the order they arrived, so $2 is the second. One change turns our notes-only script into a back-up-anything script:

backup.sh, take two — back up anything
#!/usr/bin/env bash
# Usage: ./backup.sh <folder>

TARGET="$1"
BACKUP_NAME="$TARGET-backup-$(date +%F)"

mkdir -p backups
cp -r "$TARGET" "backups/$BACKUP_NAME"
echo "Backed up $TARGET to backups/$BACKUP_NAME"
Same script, any folder
./backup.sh notes
# Backed up notes to backups/notes-backup-2026-07-12

./backup.sh recipes
# Backed up recipes to backups/recipes-backup-2026-07-12
Note
What's $(date +%F)? The $( ... ) around a command means "run this, and drop its output right here" — command substitution. So $(date +%F) becomes today's date, and the backup name carries it. Agents lean on this constantly, so it's worth recognizing on sight. One caveat for the sandbox just below: this in-browser playground doesn't run $( ... ) yet, so build your backup.sh there with a plain fixed name (the audit habit is the point). On your real machine, the dated version works exactly as shown.

How agents deliver a file: the here-doc

Watch a coding agent create a file and you'll see one shape over and over — a whole file, delivered in a single command:

A file in one command
cat <<'EOF' > backup.sh
#!/usr/bin/env bash
mkdir -p backups
cp -r notes "backups/notes-$(date +%F)"
EOF

Read the first line in three parts. cat you know. <<'EOF' is a here-document: "feed the lines that follow straight in as input, until a line that says exactly EOF." And > backup.sh is the redirection from Text & Pipes, catching all of it in a file. The delimiter is any word you like — EOF ("end of file") is convention, not a keyword. The quotes around it are the single-quote rule from Paths in a new costume: quoted, every $ between the markers travels into the file literally — which is what you want when the script's own $(date +%F) should run later, not now. Unquoted, the shell expands them all before the file is even written. One habit to take from this: the lines between the markers are the file — audit them exactly the way you'd audit the script they're about to become. (The playground doesn't speak here-docs; this one is for reading agent transcripts, and for your real machine.)

Important
Agents write scripts constantly. Ask an AI to "set up the project" or "automate the deploy" and odds are it produces a .sh file. Until today that file was a black box you ran on trust. Now it's a short text file you can cat, read line by line, and audit the way you would any command: name it, read every flag, check what it touches, ask whether it's reversible. That routine gets its own section in Read Before You Run — because a script is just commands, and you read commands now.
Tip
A script has a shape, and it reads in that order. The shebang tells you the language, the variables tell you the moving parts, and the verbs tell you the risk — cp copies, rm deletes, and curl fetches something off the network to run on your machine.

Try It: Automate the Backup

Build backup.sh right in the sandbox — write it line by line with echo >> (this playground has no full-screen editor), then chmod +x it and run it with ./backup.sh. Check your work with cat and ls backups.
Automate the Backup

Loading playground...

Now make it reusable. A hard-coded path backs up one folder; $1 backs up whatever you name. Same audit habit applies — read the script before you run it.

Try It: One Script, Any Folder

One Script, Any Folder

Loading playground...

Vibe it

"Write a bash script that backs up a folder I pass as $1, and explain every line before I run it"

"Here is a script an agent generated — walk me through what each line does and flag anything risky"

6.2 Exit Codes & Chaining

Every command files a report when it finishes — 0 means success, anything else means trouble
Note
The goal: You want "run the tests, and deploy only if they pass" — but you've been eyeballing the output and deciding by hand. The shell has a built-in way for commands to report success or failure, and a grammar for acting on it.

Every command, when it finishes, hands the shell a number called its exit code: 0 means success, and anything else (1 to 255) means some flavor of failure. Which number it picks is each command's own business and means nothing outside that command, so there's no master table to go hunting for — only zero-or-not-zero travels between commands. You never see it unless you ask — the special variable $? holds the exit code of the last command:

Asking how the last command went
ls notes
# recipes.md  todo.md
echo $?
# 0            <- found it, success

ls no-such-folder
# ls: cannot access 'no-such-folder': No such file or directory
echo $?
# 2            <- non-zero: it failed, and it said so

On its own that's a curiosity. It earns its keep with the three chaining operators, which decide whether the next command runs based on the last one's exit code:

a && b — "and then" (only on success)

Run b only if a exited 0. The workhorse: "do this, and if it worked, do that."

a || b — "or else" (only on failure)

Run b only if a failed. The fallback: "try this, or else do that."

a ; b — "and regardless"

Run b no matter what happened to a. Just two commands on one line — no safety logic at all.

The truth table, live
true && echo "ran"        # ran        (true exits 0)
false && echo "ran"       #            (nothing - && skips after failure)
true || echo "ran"        #            (nothing - || skips after success)
false || echo "ran"       # ran

# The pattern you'll use every day:
npm test && npm run deploy
# tests pass  -> deploy runs
# tests fail  -> deploy never happens

# And the three-operator shape, read left to right:
npm test && npm run deploy || echo "something failed"

true and false in the first four lines are real little programs whose entire job is to exit 0 and 1 — which is what makes that truth table runnable rather than illustrative. The last line has a catch. The chain runs left to right and neither operator outranks the other, so the || is not paired off against the && — it fires whenever the command immediately before it failed. Tests pass, deploy fails, and the message prints anyway. Useful here, but it is not the one-branch-or-the-other shape it looks like. (Package Managers covers what npm run deploy is running.)

One number decides which branch runs — the same mechanism behind every CI pipeline (continuous integration: servers that build and test your code for you, automatically).

The classic horror story: ; where && belonged

Why does the choice of connector matter so much? Because of lines like this one, which has genuinely destroyed home directories. The target sits in /tmp, the machine's shared scratch space (Paths), so emptying a folder inside it is a perfectly ordinary thing to want:

Two characters between routine and disaster
# THE TRAP - a semicolon runs the rm NO MATTER WHAT:
cd /tmp/build ; rm -rf *
# If /tmp/build doesn't exist, cd FAILS... and rm -rf * runs
# anyway - in whatever directory you were standing in. Maybe ~.

# THE SAFE VERSION - && only deletes if the cd succeeded:
cd /tmp/build && rm -rf *
# cd fails -> the chain stops -> nothing is deleted.
Caution
Audit the connectors, not just the commands. When an AI proposes a chained one-liner, add a fifth question to those four: what happens if the first command fails? A ; says "I don't care" — which is almost never true when the next command is destructive. If you see cd <anywhere> ; rm, send it back and ask for &&.

And here's why this little number matters beyond one-liners: the entire automated world runs on exit codes. CI pipelines decide pass-or-fail by the exit code of your test command — and GitHub, which stores projects and runs those tests every time someone sends up new code, draws its green checkmark to mean "everything exited 0." Coding agents watch exit codes the same way: run a command, read $?, and decide whether to continue, retry, or fix. When your agent says "the tests failed, let me look" — it didn't read your mind. It read an exit code. Scripts join the same game: a script's own exit code is that of its last command, so scripts can chain scripts, and the whole tower stands on one convention. Zero means go.

Try It: Deploy Only on Green

The tests here fail on the first run. Use $?, &&, and || to build a one-liner that deploys only when the tests pass — then fix the failing check and watch the same line take the other branch.
Deploy Only on Green

Loading playground...

Vibe it

"Rewrite this chained command so the destructive step only runs if everything before it succeeded"

"Explain what this one-liner does if the first command fails — trace it connector by connector"

Challenge: Ship All Three

Loading challenge...

Part 7

Text Surgery: Find, Change, Extract

"grep asks: which lines? sed answers: make them different."

Text & Pipes taught you to find text — grep it, count it, sort it. This part teaches you to change it. sed is the single most common file-mutating command AI agents propose — "let me just update that config" is almost always a sed one-liner — and awk is how you pull one column out of anything shaped like a table. Learn to read these two and a huge class of agent-suggested commands stops being line noise.

Important
The idea that makes sed safe to learn: it edits the stream, not the file. Like every Text & Pipes tool, sed reads text, transforms it, and prints the result — the input file is untouched unless you explicitly say otherwise (that's Editing in Place, and it has a safety rule). You can experiment freely: the original survives every mistake.

7.1 Find & Replace — the s Command

Every editor has find-and-replace. sed is find-and-replace unplugged from the editor — it works on anything that flows: files, pipes, command output. One tiny script does it all, and it reads like a sentence once you know the grammar:

s/old/new/g — substitute, find this, replace with this, every match on the line
Your first substitution
cat menu.txt
mango smoothie  mango, ice, lime

sed 's/mango/kiwi/' menu.txt      # replace the FIRST match on each line
kiwi smoothie  mango, ice, lime

sed 's/mango/kiwi/g' menu.txt     # g = every match on the line
kiwi smoothie  kiwi, ice, lime

Read s/mango/kiwi/g in four beats: s means substitute; mango is find this; kiwi is replace with this; g means every match on the line, not just the first. That find half is a regular expression rather than a plain string, so ., *, ^ and $ carry the meanings they had in Searching Text — a dot matches any character, even where you meant a literal one. Two more pieces complete the grammar: I ignores case, and the slashes are just delimiters — any character works, which saves you from escaping paths:

Delimiters are your choice; & echoes the match
sed 's|/usr/local|/opt|' paths.txt     # | instead of / — no escaping
sed 's/error/[&]/I' app.log            # & = whatever matched; I = any case
[ERROR] payment timeout                # "error", "Error", "ERROR" all wrapped

That | is doing sed's job, not the shell's — inside the quotes it's a delimiter, and the same character outside them is the pipe from Text & Pipes. The single quotes are the whole difference: they hand sed its script as one untouched word (Paths), which is why every sed script in this part wears them.

Tip
Preview to the screen, then redirect. The same habit as echo-the-glob from Copy, Move, Delete: run the sed command bare and read its output. Happy? Add > new-file.txt and run it again. Because sed doesn't touch the input file, the preview is always free.
Vibe it

"Write me a sed one-liner that replaces every "staging" with "production" in deploy.txt — preview only, no file changes"

"Explain what sed 's|http://|https://|g' does, character by character"

Try It: Rebrand the Menu

Marketing renamed the mango everything. Rewrite menu.txt with s/mango/kiwi/g into kiwi-menu.txt — and notice the original survives untouched. Line 1 has two mangos: you'll need g.
Rebrand the Menu

Loading playground...

7.2 Line Surgery — Addresses, d and p

Substitution is only one of sed's commands. Picture your file as a film strip: every line passes through sed, one frame at a time, and a command decides its fate — keep, drop, or print. An address in front of the command selects which frames it applies to:

Every line passes through; addresses pick the frames, commands decide their fate
d drops lines; addresses choose them
sed '/DEBUG/d' app.log        # drop every line matching DEBUG
sed '3d' notes.txt            # drop line 3
sed '2,5d' notes.txt          # drop lines 2 through 5
sed '$d' notes.txt            # drop the last line

Those four are most of the address vocabulary, and two of them are worth a second look. /DEBUG/ is a regular expression tried against every line, not a plain word — the same rules as the find half of s///. And $ here is sed's name for the last line: an address, not a variable, with nothing to do with the $NAME expansion from Environment Variables. awk hands the same character a third job in Columns & awk.

The mirror image of dropping is printing only what you ask for. That's the p command paired with -n, which silences sed's default echo. It's the precision tool for "show me lines 40 through 55 of that huge log" — no pager, no scrolling:

-n + p — print only the selection
sed -n '40,55p' server.log     # just lines 40–55
sed -n '/ERROR/p' server.log   # only ERROR lines (like grep!)
sed -n '/start/,/stop/p' run.log   # from a /start/ match to a /stop/ match

Addresses compose with any command — '2s/beta/B/' substitutes on line 2 only, '/alpha/s/a/A/' substitutes only on lines matching alpha. One grammar, every combination. That's the Unix philosophy again, folded inside a single tool.

Vibe it

"Show me lines 100 to 120 of build.log without opening an editor"

"Write a sed command that strips every blank comment line starting with # from config.txt — preview first"

Try It: Silence the Debug Noise

app.log is drowning in DEBUG chatter. Drop those lines with '/DEBUG/d' and save the readable story as clean.log — the original stays intact for the postmortem.
Silence the Debug Noise

Loading playground...

7.3 Editing in Place — the -i Footgun and the .bak Rule

Everything so far printed to the screen or a new file. But the command agents actually propose is usually sed -iedit the file in place. Same substitution, except now it rewrites the real file, silently, with no preview and no undo. -i was the letter that stopped and asked before overwriting back in Copying; here it means the opposite of careful, which is exactly what Getting Help meant by flag letters belonging to their command. It's also the one sed flag worth carrying into Terminal for the AI Era, where auditing becomes a routine.

-i rewrites the real file — .bak keeps the original in the drawer
Caution
sed -i with no backup is a red-flag pattern. File it with rm -rf and curl | bash on the list of commands to read twice — Terminal for the AI Era makes that list a method. But unlike those, the fix isn't refusing — it's amending: one suffix turns the risky command into a safe one.
The house rule: -i.bak
sed -i.bak 's/http:/https:/g' config.yml
                    # config.yml      — rewritten
                    # config.yml.bak  — the original, untouched

diff config.yml.bak config.yml   # exactly what changed, nothing else
mv config.yml.bak config.yml     # instant rollback if it went wrong

The suffix goes directly after the flag — -i.bak, no space — and sed saves each original as file.bak before rewriting. It costs nothing, it works on many files at once (sed -i.bak 's/<old>/<new>/' *.yml backs up every one), and it turns "I hope that was right" into something you can check. That's the diff in the block above: hand it two files and it prints only the lines where they disagree. When an agent proposes a bare -i, don't approve or reject — edit the command and add the .bak.

macOS enforces the house rule anyway. Mac and Linux carry different lineages of the same veteran tools — BSD's on the Mac, GNU's on Linux — and BSD sed insists on that suffix: leave it off and the command fails with an error pointing nowhere near the real problem, while GNU sed takes the bare -i and keeps no copy at all. ps, ls and du split the same two ways, which is why a command that worked on a colleague's machine sometimes doesn't on yours.

Vibe it

"You proposed sed -i without a backup — rewrite that command with -i.bak and tell me how I'd undo it"

"Mass-rename a function across all .py files here, with backups, and show me how to verify the change afterwards"

Try It: The Agent's Mass Edit

The agent's plan switches the site to https — worth doing, since the s is what makes the connection encrypted (What Is localhost?) — but its sed -i keeps no backup. Read agent-plan.txt, amend the command to -i.bak, run it on both config files, then open a .bak to admire your safety net.
The Agent's Mass Edit

Loading playground...

7.4 Columns & awk — Pull the Field You Need

A lot of terminal output is secretly a table: log lines, CSV exports, process listings. awk splits every line into numbered fields and prints the ones you name — $1 is the first column, $2 the second, $0 the whole line:

Tables are just text — awk pulls the column you ask for
Fields, separators, guards
awk '{print $2}' table.txt          # second column (splits on spaces)
awk -F, '{print $1, $3}' data.csv    # -F, = split on commas; comma joins with a space
awk '/error/ {print $1}' app.log     # /pattern/ runs the action on matching lines only

The braces are awk's action block — the work it does on every line that reaches it, with the /pattern/ guard outside and in front. The $1 inside belongs to awk, not the shell: it has nothing to do with the numbered script arguments in Your First Script, and that collision is why every awk program here sits in single quotes. Swap them for double quotes and the shell empties $1 before awk ever sees it: {print $1} arrives as {print }, which awk reads as "print the whole line" and obeys without complaint — the wrong answer, delivered confidently. Ask for two columns and it's a syntax error instead. Neither one mentions quotes.

You already know cut from Text & Pipes — so which one? Honest answer: cut when a single character separates every field — a comma in a CSV, a colon or a tab elsewhere — and awk when the spacing is ragged. awk's default split treats any run of spaces as one separator, which is exactly what column-aligned output needs — cut would see every space as its own empty field and hand you garbage. When you meet process listings in Everything Is a Process, awk is the tool that reads them.

Tip
Real awk is an entire programming language — BEGIN blocks, variables, printf. What you've just learned is the field-printing dialect, and it covers the vast majority of awk you'll ever see an agent propose. When one shows up wearing more syntax than this, that's not a reading failure — that's your cue to ask the agent to explain it line by line.
Vibe it

"This CSV has columns name,email,plan — give me just the emails, using awk and using cut, and tell me when each tool is the better pick"

"Explain this command an agent suggested: awk '/FAIL/ {print $3}' test-output.log"

Try It: Pull the Column

signups.csv has three columns; the launch email needs one. Pull the email column with awk -F, or cut -d, — your choice — and save it as emails.txt.
Pull the Column

Loading playground...

Your text toolbox is complete: grep finds, sed changes, awk extracts — and pipes chain them into anything. Every one of them previews to the screen before it touches a file, which means every one of them rewards the habit this course keeps drilling: read first, run second.

Challenge: Promote the Stack

Loading challenge...

Part 8

Processes & Ports: When Things Won't Stop

"Port 3000 is already in use." — every web developer, weekly

Until now every command you ran started, did its job, and finished. But some programs are meant to keep going: dev servers, watchers, builds — and your AI agent starts them constantly. This part is about the programs that are still running: how to see them, how to stop them, and how to fix the single most common error in modern web development, when yesterday's server is still squatting on the port today's server needs.

Important
This is the part that turns "I'll just restart my laptop" into a ten-second fix. Every technique here is the same three-beat move: find the process, get its number, act on that number.

8.1 Everything Is a Process

Every running program on your machine — the dev server, your editor, the shell you're typing into, the agent itself — is a process: one copy of some code, loaded into memory and kept alive by the operating system. Open three terminal windows and you have three shell processes, each with its own idea of where it's standing. Each one gets a PID — short for process ID — the moment it starts, a number unique on this machine for as long as it lives. That number is the handle you use to do anything to it.

ps on its own lists only the processes attached to this terminal — usually two or three, and never the dev server you're hunting. Add aux and you get the whole machine:

Every running program is a row with a number
Reading the census
ps aux
USER       PID %CPU %MEM START   COMMAND
vibe      1024  0.0  0.1 09:10   bash
vibe       400  0.4  1.8 08:41   node server.js
vibe       437 97.4  0.6 09:07   spinner.sh --forever

The three letters earn their place: a widens the list to every user's processes, u adds the human-readable columns you're about to read, and x includes the ones with no terminal attached — which is precisely where background servers hide. (They carry no dash: ps takes its options in an older style that predates the convention, the same reason tar cf works without one.) Your real output is wider than this too — macOS and Linux both add memory sizes, a terminal name and a state column. Ignore those; the ones that matter are here.

USER is the account that owns the process — vibe is you, which is why you can stop your own dev server and get Operation not permitted on one of the system's. PID is the number you'll pass to kill. %CPU is how hard it's working, measured against one core, and your machine has several: separate workers that genuinely run at the same time, so a process using three of them honestly reads 340%. A runaway of yours sits near 100, and your fans tell you before ps does. %MEM is the share of the machine's memory it's holding, the column to check when everything goes sluggish rather than hot.

START is the wall-clock time it launched, the one column that separates the server you started a minute ago from yesterday's, still running. COMMAND is what it actually is — though that's the interpreter's name, not your project's: a JavaScript server shows up as node, a Python one as python, and that's what you grep for. Notice bash in that list too: the shell you're typing into is just another process, no more special than the rest.

Tip
ps output is just text — which means Text & Pipes and Text Surgery already taught you how to search it. ps aux | grep node finds a process by name, and awk '{print $2}' pulls the PID column out of the result. This is why the column-shaped output of old Unix tools is worth putting up with: every tool composes. There's a shortcut for the common case too — pgrep node prints matching PIDs directly.
Note
ps is a photograph. For the movie, top repaints the same table live, a few times a second, worst offenders first — and q gets you out, the same escape as every pager (Looking Inside Files). Its nicer cousin is htop, the very tool sudo's install example happened to pick. Between them: ps answers "what exactly is running?", top answers "what is my machine doing right now?"
Vibe it

"What's using all my CPU right now? Show me how to check, and explain the columns"

"Find the process id of my running dev server without me having to read the whole ps output"

8.2 Stopping Things — Ask Nicely, Then Insist

kill has a violent name and a polite default. Plain kill PID sends SIGTERM — a signal, which is a short fixed message the operating system delivers to a running process from outside, nothing to do with whatever the program normally reads. They come in a small vocabulary and all share a prefix: SIG for signal, then what it asks for, so SIGTERM is "terminate" — please finish up and stop. The program gets to react: save its work, close its files, remove its lock, and exit cleanly. It's a letter, not a bullet.

SIGTERM asks. SIGKILL removes the floor. Ask first.
The escalation, in order
kill 437                 # SIGTERM, signal 15 — "please stop when you can"
(no response  spinner.sh --forever is still running)

kill -9 437              # SIGKILL, signal 9 — the floor opens, no cleanup
[killed] spinner.sh --forever (PID 437)

The 9 is a signal number, not a count: every signal has one, kill -N means "send signal N", and a bare kill is kill -15. That's how you read kill -HUP or kill -2 in an agent's command — same verb, different message.

A program can catch SIGTERM and decide to ignore it — that's a feature, not a bug: it's how servers finish serving the request they're mid-way through instead of dropping it. But it also means a polite kill can bounce. kill -9 sends SIGKILL, which no program can catch, refuse, or prepare for. The kernel — the core of the operating system, the part that owns the hardware and creates every process, including your shell — simply removes it. Nothing gets saved and no cleanup runs, so any lock file it was holding stays behind: the marker a program leaves to say "I'm already running". The next copy finds it, believes it, and refuses to start.

Caution
kill -9 is a last resort, not a default. Plenty of guides (and plenty of AI answers) reach for it first because it always works. It always works the way an axe always opens a door. Ask nicely, give it a second, and escalate only if it refuses — and treat kill -9 as a red flag when an agent reaches for it without trying the polite version first — one more entry for the audit routine in Terminal for the AI Era.

One thing you already knew, renamed: Ctrl+C is a signal too — SIGINT, "interrupt" — sent to whatever is running in the foreground — the process attached to this terminal right now, the one your keystrokes reach and the reason your prompt hasn't come back. That's why it stops a stuck command but does nothing to a server running in another window; that one needs its PID. How the Terminal Works opens up the machinery underneath all three signals.

Vibe it

"A process is ignoring kill — walk me through what to try, in order, and what each step costs"

"What actually happens to unsaved work when I use kill -9 versus a plain kill?"

Try It: Stop the Runaway

Your fans are screaming. Find the process burning 97% of a core with ps aux, try kill first and read what happens — then escalate. Leave the innocent server running.
Stop the Runaway

Loading playground...

8.3 Who's on Port 3000?

A port is a numbered door on your machine. A server opens one and waits there for visitors; your browser knocks on it when you visit localhost:3000localhost being your machine's name for itself (What Is localhost?). The numbers themselves are convention rather than law: 3000 and 5173 are what particular tools happen to pick, and everything here works the same for 8080.

The rule that generates all the pain is simple: one program per port. Try to open a door someone's already standing in — here with serve, a tiny dev server you install with npm, playing the part of whatever starts your project — and you meet the error:

One ship per pier — that's the whole rule
The error, and the fix
serve
Error: listen EADDRINUSE: address already in use :::3000

lsof -i :3000            # who's holding the door?
COMMAND    PID  USER   TYPE NODE NAME
node       400  vibe   IPv4 TCP  *:3000 (LISTEN)

kill 400                 # ask yesterday's server to leave
serve                    # the pier is free
serve: listening on http://localhost:3000

That's the same three beats from the start of this part, with the error that sent you looking at the front and your own server at the end: read the error, find the PID, stop the squatter, start yours. Memorize it and one of the most common blockers in web development becomes routine. lsof stands for "list open files"; the -i flag narrows it to network connections. Silence from lsof means the port is free.

That one output line is worth reading properly. node is the program and 400 is its PID — the only field you actually need. The rest describes the connection it's holding, and a real lsof prints a few more of those columns than you see here. IPv4 TCP says what kind of connection it is; *:3000 is the door number with a wildcard host, where * means "any address on this machine" rather than the filename glob from Wildcards — and the :::3000 in the error above is that same wildcard, spelled the IPv6 way. (LISTEN) is the confirmation: that process is sitting at the door waiting for visitors, which is exactly why yours can't have it.

Tip
Where do stale servers come from? Usually an agent that started one in a background shell — a whole separate session you never saw, which is not the same thing as the background jobs in Background & Foreground below. When a port is mysteriously busy right after an AI session, that's the first suspect — and lsof -i :3000 tells you its name.
Vibe it

"My dev server says EADDRINUSE on port 5173 — give me the exact commands to find and stop what is holding it"

"How do I start my app on a different port instead of killing what is already there?"

Try It: Free Port 3000

Yesterday's server never died. Run serve to meet the error, then work the ritual: lsof -i :3000 for the PID, kill it, and start your own.
Free Port 3000

Loading playground...

8.4 Background & Foreground

A dev server holds your terminal hostage: it runs until you stop it, and the prompt never comes back. One answer is more terminals (the tabs and splits in Many Terminals at Once). The other is job control — telling this shell to keep the program running backstage while you get your prompt back.

& sends it backstage · jobs lists it · fg brings it into the spotlight
Backstage and back
./slowbuild.sh &         # & = start it, give me my prompt back
[1] 400                  # job number, then PID

jobs                     # who's backstage?
[1]  Running    ./slowbuild.sh

fg %1                    # bring job 1 into the spotlight

Two numbers appear here and they're easy to confuse: the job number in [1] is small and belongs to this shell — you use it as %1. The PID is large and belongs to the whole machine. kill %1 and kill 400 stop the same program here.

A background job is still yours and still tied to this shell: close the window and it goes with it, unless you take extra steps. The step has a name — nohup, "no hangup," from the days when closing a terminal literally hung up a phone line: nohup ./slowbuild.sh & keeps running after the window is gone and parks its output in nohup.out. It works, but it's a patch; the comfortable answer to work that must outlive your window is a terminal that survives on its own, which is tmux in Many Terminals at Once. Either way, backstage is not the same as somewhere safe — don't send a long build back there bare and then walk away from the terminal.

There's one more move, and it's the rescue you'll actually reach for: you started something long, forgot the &, and now you're stuck watching it. Ctrl+Z suspends it and hands your prompt back — suspended meaning stopped dead, making no progress at all, which is why jobs reports it as Stopped rather than Running. bg is what resumes it, backstage: the place you'd have been with &, arrived at the hard way.

Four ampersands, no relation. 2>&1 is a reference to a stream (Redirection). && runs the next thing only if this one worked (Exit Codes & Chaining). & inside a sed replacement means whatever was just matched (Find & Replace). And a bare & at the end of a line is the one this section is about — reading npm run build & as a half-typed && is a mistake worth not making.

Tip
Honest advice: use tabs. Job control is a genuinely useful escape hatch — and mostly a rescue for the terminal you're already in. For an everyday setup (server in one pane, logs in another, a free shell for you), the split terminals in Many Terminals at Once are nicer to live with than juggling %1 and %2.
Vibe it

"I started a long command and forgot the & — how do I get my prompt back without losing the work?"

"What's the difference between a job number and a PID, and when does each one matter?"

Try It: Two Things at Once

Send the slow build backstage with &, confirm it's there with jobs, then bring it forward with fg %1 and check what it left behind.
Two Things at Once

Loading playground...

You can now see what's running, stop it politely or firmly, and free a port that's been taken hostage — the three moves that unstick a machine. Next: the server you just started is waiting on port 3000. Time to talk to it.

Challenge: Clear the Agent's Processes

Loading challenge...

Part 9

Talking to the Network

"The server is running." — prove it.

In Processes & Ports you took a port back and started a server on it. Now let's talk to it. This part is about asking questions over the network and reading the answers: what localhost actually means, how to check a server yourself instead of trusting a claim, how to pull one value out of a wall of JSON — and how to handle the API keys that make all of it work without leaving them lying around in your shell history.

Important
The habit this part builds is the one Terminal for the AI Era will formalise, aimed here at a new target: verify with your own command. When an agent says the server is up, the deploy worked, or the API returned success, there is a one-line way to check — and now you'll know it.

9.1 What Is localhost?

localhost is the name your machine uses for itself. Ask for localhost and the request never touches the network — it leaves one program on your computer and goes straight into another. That's why a dev server announcing "listening on http://localhost:3000" is visible to you and to nobody else. A URL packs several answers into one line:

localhost = this very machine · the port is which door
Reading a URL
http://localhost:3000/api/health
└─┬─┘  └───┬───┘ └─┬┘ └────┬────┘
                        └── which room inside  the path
                 └── which door  the port
          └── which machine  here, this one
  └── the language  http

The first part names the language: HTTP, the hypertext transfer protocol — the rules a program and a server use to ask and answer. On the public web you'll see https instead, the same conversation encrypted so nobody in between can read it. And the tail end is a URL path, not a folder on your disk: /api/health exists only because someone wrote that route into the server. Ask for one nobody wrote and you get a 404 — the server saying it has no such path — not a broken machine.

You've met ports already: they're the numbered doors from Who's on Port 3000?, and the same rule applies — one program per port. That's why the numbers recur. 3000 is the Node convention, 5173 is Vite's — the tool that runs the dev server for a great many JavaScript projects — and 8080 is the classic alternate web port. If your agent starts a project and mentions a number in that neighborhood, it's telling you which door to knock on.

Tip
127.0.0.1 is the same place as localhost — the numeric version of "here." Machines find each other on a network by IP address — internet protocol, the numbering scheme every networked machine answers to — and 127.0.0.1 is the one number reserved to mean this machine. You'll see both spellings in error messages and config files; they mean the same computer.
Vibe it

"My app says it is running on localhost:5173 but the browser shows nothing — how do I check from the terminal?"

"Explain the difference between localhost and a real domain like example.com"

9.2 curl — Ask the Network

curl sends a request and prints the reply. That's it — and that's enough to settle most arguments with an agent. Instead of trusting "the server is running," you ask the server:

Send a request. Read the reply. Believe nothing else.
Asking, and saving the answer
curl localhost:3000/health
{"status":"ok"}

curl localhost:3000/health          # with nothing listening
curl: (7) Failed to connect to localhost port 3000: Connection refused

curl -s -o status.json localhost:3000/health   # save it instead of printing
curl -I localhost:3000/health                  # just the headers

Four flags cover almost everything you'll need. -o FILE saves the reply instead of printing it. -s ("silent") hides the progress meter — always use it inside pipelines and scripts. -I shows you just the headers a server sends back, which is the quickest "is this thing alive and what does it serve?" And -H adds one to what you send.

A header is a name/value line travelling alongside the request, carrying everything that isn't the content — what format you want back, who you are, what software is asking. (Nothing to do with the header row of a spreadsheet; the word just gets reused.) An API — an application programming interface, a door a service opens for other programs rather than for people — knows which requests are yours because your key rides in one. That's the whole subject of Keys & Secrets.

Caution
Now you can read the red-flag pattern. You will meet curl ... | bash everywhere, and it decodes with what you already have: curl fetches a script off the internet, and the pipe feeds it straight into a shell that runs it — unread, unreviewed, with your permissions. The two-step version is always available: curl -o install.sh <url>, read the file, then run it. When it's the right call to skip that step — and it sometimes is — is the subject of Read Before You Run.
Vibe it

"Check whether my local API is responding on port 8000 and show me the raw response"

"You said the deploy succeeded — give me a curl command that proves the new version is live"

Try It: Is It Alive?

The agent says the server is up. Ask it yourself with curl, then save the reply to status.json with -s -o so you have the receipt.
Is It Alive?

Loading playground...

9.3 Reading JSON with jq

APIs answer in JSON — labelled boxes inside labelled boxes, built out of nested { "name": value } pairs. Printed raw it's a wall of braces; what you usually want is one value out of the middle. jq is the tool that reaches in and takes it, and since it reads from a pipe, it joins the pipelines you built in Text & Pipes.

One thing the sandbox hides: jq is not standard equipment. It's a third-party tool you install first (Package Managers), so your first | jq on a fresh machine may well answer command not found rather than a version number.

APIs answer in nested boxes — jq holds the keys
From wall of braces to one value
curl -s api.vibecloud.dev/releases
{"latest":"2.1.0","downloads":48213,"items":[{"tag":"2.1.0"}]}

curl -s api.vibecloud.dev/releases | jq .latest
"2.1.0"

curl -s api.vibecloud.dev/releases | jq -r .latest    # -r = raw, no quotes
2.1.0

curl -s api.vibecloud.dev/releases | jq -r .items[0].tag
2.1.0

A filter is just a path: . is the whole document, .latest is one key, .server.port walks deeper, and .items[0] takes the first element of a list. The -r flag matters more than it looks: without it a string keeps its JSON quotes, which breaks the next command in the pipeline. Reach for -r whenever the value is going somewhere else.

Tip
If jq says parse error, look at the raw reply. It means one thing only: jq tried to read what arrived as JSON, and the bytes weren't JSON. Nine times out of ten what arrived is an HTML error page — the tag-based format web pages are written in, so you'll see angle brackets, a leading <!doctype html> and no braces anywhere. The real problem is upstream: a wrong URL, or a dead server. Drop the | jq and read what actually arrived, the same build-the-pipeline-one-stage-at-a-time habit from Text & Pipes.
Vibe it

"Pull just the version number out of this API response and explain the jq filter you used"

"This jq command returns null and I do not know why — help me inspect the JSON structure first"

Try It: Question the API

Ask api.vibecloud.dev/releases for the latest version, pull it out with jq -r, and leave just the number in version.txt.
Question the API

Loading playground...

9.4 Keys & Secrets

Every API you talk to wants a key, and keys are the one kind of text you must never type casually into a terminal. Here's the uncomfortable reason: your shell writes down everything you type. A key pasted into a command lives on in ~/.zsh_history — or ~/.bash_history if you're on bash — a plain text file that anyone who sits down at your machine can read, and search with the tricks in History Superpowers.

Keys live in files — never in commands
Caution
Never put a secret directly in a command. Not in curl -H, not in an export you type by hand, not in a script you commit. It lands in your history, in your terminal scrollback — the output your window is still holding, which is what a screen-share shows — and often in logs you don't control. Unlike a password, an API key is usually a straight line to a bill.

The standard answer is a .env file — "env" for environment, and nothing inside it but plain-text NAME=value lines, one per key. The file is locked down, and your commands reference the variable instead of the value. That's the environment variables and permissions from Permissions & Config doing real work together:

Keys in a file, not in a command
echo 'API_KEY=sk-vibe-9c2f10ab' > .env    # the key lives here
chmod 600 .env                          # only you can read it
source .env                             # run it in THIS shell, so the value sticks

curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy
#                                 ↑ the variable, never the key itself

echo ".env" >> .gitignore               # and never commit it

That header line has more moving parts than it looks. Authorization is the header's name, the colon separates name from value, and Bearer is a fixed keyword saying what kind of credential follows — only the token at the end is yours. An API that wants X-API-Key: sk-vibe-9c2f10ab is the same line with a different name and no keyword at all.

The quotes change style mid-block on purpose. Single quotes are fully literal, which is what you want when writing the key into a file; double quotes still expand $API_KEY, which is what you want when handing its value to curl. That's the rule from Paths, and getting it backwards sends the server the eight characters $API_KEY instead of the key. source is what makes the variable available to that curl at all, for the reason in Your Shell Config.

chmod 600 is the Permissions & Config permission lesson with the stakes turned up: read and write for you, nothing at all for anyone else. And .gitignore keeps the key out of your repository — because a key pushed to GitHub is a key that belongs to the internet, usually within minutes.

One catch before you lean on that last line: a .gitignore entry stops git starting to track a file and does nothing at all for one it already tracks — which is exactly where you are if the key is already committed. Then the only fix is to revoke and reissue the key, which you do on the provider's website rather than in the terminal. It works because revoking kills the key on their servers; deleting the commit leaves it alive.

Tip
Agents leak keys by accident. An assistant that can read your files can read your .env, and one that writes code may inline a key it found "to make the example runnable." When you review agent-written code, grep for the shape of a key — grep -rn 'sk-' . — before you commit anything.
Vibe it

"Move the hard-coded API key in this script into a .env file and show me the safest way to load it"

"I accidentally committed an API key — what do I actually need to do, in order?"

Try It: Keep the Key Secret

deploy.sh has a key typed straight into it. Move it into .env, lock the file with chmod 600, and point the script at $API_KEY instead — with a .bak, naturally.
Keep the Key Secret

Loading playground...

9.5 A Door to Another Machine

One last idea, and it reframes everything you've learned: with ssh — secure shell, logging into another machine over the network — the same terminal can drive a different computer: a server in a data center, a Raspberry Pi in your closet, a cloud box that runs your app. Every command in this course works there, unchanged.

Same conversation, different machine
Stepping through the door
vibe@laptop:~$ ssh vibe@garden.example.com
vibe@garden:~$                  # the prompt changed — you are THERE now

vibe@garden:~$ ls               # this lists the server's files
vibe@garden:~$ exit             # back to your own machine
vibe@laptop:~$

This is where Anatomy of a Prompt from the introduction earns its keep: the prompt tells you which machine you're on. That is not a cosmetic detail. A rm -rf typed on the wrong side of that door is a very different afternoon, and the habit of glancing at the hostname before running anything destructive is one experienced people never drop.

You log in with a key pair rather than a password: two matching files in ~/.ssh. The public half is id_ed25519.pub — the .pub is what tells them apart — and it goes onto the server, where it acts as a lock anyone may see; the private half, id_ed25519, never leaves your machine. "Goes onto the server" is a concrete act: that public half gets appended to ~/.ssh/authorized_keys on the far side. Same rule as Keys & Secrets, in a different costume: the secret stays in a file, on your computer, with tight permissions.

Moving files through the door

ssh carries your keystrokes through the door; its sibling scp ("secure copy") carries files — same door, same keys. It reads exactly like the cp you know from Copy, Move, Delete, source first, destination second, except either side may live on another machine, and a colon marks which:

scpcp, across machines
scp status.json vibe@garden.example.com:~/reports/   # up: here -> there
scp vibe@garden.example.com:~/logs/server.log .      # down: there -> here
scp -r site/ vibe@garden.example.com:~/www/          # -r for folders, like cp

That colon is load-bearing: leave it off the destination and nothing crosses the network — scp status.json vibe@garden.example.com just makes a local file named like an address, quietly, in the folder you're standing in. The ls-your-target habit from Copy, Move, Delete catches it.

The heavy-duty version — and the one agents reach for in deploy scripts — is rsync: it compares the two sides first and sends only what changed, so the second run of a big copy takes seconds, and an interrupted one picks up where it stopped.

rsync — send only what changed
rsync -avz site/ vibe@garden.example.com:~/www/
# -a archive: keep permissions and timestamps    -v narrate    -z compress on the wire

rsync -avz --dry-run site/ vibe@garden.example.com:~/www/   # rehearse first

One sharp edge worth respecting: on the source, a trailing slash means "the contents of" while no slash means "the folder itself" — site/ fills ~/www/ directly, site creates ~/www/site inside it. The difference between the two is a nested folder you didn't want, which is exactly why --dry-run — the rehearsal flag from Read Before You Run's audit routine — earns a place in the command before the real run. Echo-the-glob, at server scale.

Tip
This is a real-machine lesson — the sandbox has no other computer to visit. When you do have a server to reach, ask your assistant to walk you through generating a key with ssh-keygen and installing it; it's a five-minute setup you do once per machine.
Vibe it

"Walk me through setting up an ssh key so I can log into my server without a password"

"What is the difference between my public and private ssh key, and which one goes where?"

You can now start a server, prove it's alive, read what it says, keep its keys safe, and step onto another machine entirely. One more part of tooling to go — how to get new tools, unpack what arrives, and find out what's eating your disk.

Challenge: Prove the Release

Loading challenge...

Part 10

The Toolshed: Getting, Unpacking, Finding

"First, install X." — every setup guide ever written

This part is about owning your machine: fetching new tools, unpacking what arrives, knowing where things actually live, and finding out what's eating your disk. Four everyday chores that every guide assumes you already know — and that your agent will hand you commands for without explaining.

Important
The thread running through this part: none of it is magic, it's all just files in places. Installing puts a file where your shell looks. An archive is one file holding many. A symlink is a file that points at another. Disk pressure is files being bigger than you thought.

10.1 Package Managers — Getting New Tools

When a command doesn't exist, your shell says command not found — which, as Environment Variables taught you, means "nothing on your $PATH has that name." A package manager is a program whose entire job is installing other programs. You name a tool; it finds the right build for your system, puts it somewhere your PATH already looks, and remembers enough to update or remove it later.

Installed = placed where $PATH can see it
One idea, several spellings
brew install cowsay            # macOS (Homebrew)
sudo apt install cowsay        # Debian/Ubuntu Linux
npm i -g serve                 # i = install, -g = global
brew install jq                # jq isn't preinstalled either — same one line

which cowsay                   # where did it land?
/usr/local/bin/cowsay          # ...somewhere on $PATH. That's all "installed" means.

That last pair of lines is the whole concept. Before the install, which cowsay finds nothing and the command fails. After it, there's a file at /usr/local/bin/cowsay, that directory is listed in your $PATH, and so typing cowsay works. Installing a command-line tool is mostly just putting a file somewhere the shell already looks.

Two spellings of one idea, and Windows has a third in winget, which pulls from its own catalogue the same way. apt needs sudo because it writes into folders that belong to the machine rather than to you; Homebrew owns its own directory, so it doesn't.

One thing no setup guide mentions: Homebrew isn't on a stock Mac. brew is itself third-party software, installed once by following the instructions at brew.sh — and until you do, the first line of that block answers command not found, which is a confusing way to be told you're missing the thing that installs things.

npm i -g serve is the odd one out. i is install; -g is global, meaning put it where PATH can see it rather than inside this project's node_modules folder, where which serve would never look. And npm installs JavaScript tools and nothing else, so if what you want isn't JavaScript, npm can't help you. It also runs things: npm run <name> executes a script the project defined for itself in its package.json file, which is all npm test && npm run deploy in Exit Codes & Chaining was ever doing.

Caution
Read what an install pulls in — especially from an agent. Three things deserve a second look: sudo npm install (a global npm install should rarely need root, and needing it usually means something is misconfigured); curl | bash installers (curl — Ask the Network decoded exactly why — download, read, then run); and any package whose name is one typo away from a popular one. That last one has a name — typosquatting — and a fix: copy the install command from the project's own documentation rather than from a search result.
Vibe it

"I'm on macOS and this guide says apt install — what's the equivalent for me?"

"Before I run this install command, tell me what it downloads and where it puts things"

Try It: Install a Tool

Watch cowsay fail, install it, and watch the same command start working — then use which to see exactly where the file landed.
Install a Tool

Loading playground...

10.2 Archives — tar & zip

tar -xzf release.tar.gz is probably the most-copied command in computing, and almost nobody can explain the letters. They're not magic — they're four separate instructions jammed together:

Many files, one crate — peek before you unpack
Decoding the flag soup
tar -xzf release.tar.gz
     │││
     ││└── f: "the next word is the archive's filename"
     │└─── z: it's gzip-compressed (that's the .gz)
     └──── x: extract

tar -tzf release.tar.gz     # t: LIST what's inside — don't unpack yet
tar -czf backup.tar.gz notes/   # c: create an archive from notes/

Swap one letter and you change the verb: x extracts, c creates, t lists. The z and f stay put, unless something other than gzip did the squeezing: -xjf for a .tar.bz2, -xJf for a .tar.xz — same idea, different squeezer. Once you can read the letters, the command stops being an incantation you paste and becomes a sentence you understand — which is the entire thesis of this course applied to one very ugly command.

tar and zip split the job differently. tar only bundles — it glues many files into one and stops there, which is why compression is a separate z and why you end up with two extensions in .tar.gz. A .zip does both jobs in one format, which is what a browser download or a Windows machine usually hands you; .tar.gz is the Unix default. Opening one is unzip archive.zip, and unzip -l lists what's inside without extracting anything — l for list, the zip spelling of tar -tzf.

Tip
Peek before you unpack. An archive decides where its own contents land, and a badly-built one scatters files all over your current folder instead of tidying them into one directory. tar -tzf archive.tar.gz lists everything without extracting anything — it costs a second and it's the same "look before you leap" habit as echo-the-glob in Wildcards.
Vibe it

"Explain this command letter by letter before I run it: tar -xjf archive.tar.bz2"

"Make me a compressed backup of my notes folder with today's date in the filename"

Try It: Peek, Then Unpack

A release archive just landed. List what's inside with tar -tzf before extracting it — then unpack it for real.
Peek, Then Unpack

Loading playground...

10.3 Links & Where Things Live

You've been seeing these for nine parts without being told what they are. Run ls -l in the right folder and some entries have an arrow:

A signpost, not a copy
The arrow ls has been showing you
ls -l /usr/local/bin
lrwxr-xr-x  node -> /opt/homebrew/Cellar/node/22.3.0/bin/node
                └── a symbolic link: a signpost pointing somewhere else

ln -s releases/v2.1 current    # plant your own signpost
ls -l
current -> releases/v2.1

Those lines are trimmed — a real ls -l puts owner, group, size and date between the permissions and the name — but the character doing the work is the first one. That l is the type character from Reading ls -l's legend, the one entry that legend pointed forward to here.

A symlink — symbolic link, which is what the -s stands for — is a tiny file whose entire content is "look over there instead." It is not a copy: there's still one real thing, and the link is a pointer to it. That's why deleting the link leaves the original alone — and why deleting the original leaves a broken link, an arrow pointing at nothing, which is the source of a whole genre of confusing errors.

A link's target can be absolute or relative, and the two behave differently. /opt/homebrew/Cellar/… above is absolute: it means the same thing from anywhere. releases/v2.1 is relative, worked out from the link's own folder rather than from yours — so moving the link somewhere else breaks it while the file it pointed at sits untouched.

This is how a surprising amount of your machine is wired. Homebrew installs the real files deep in its own folders and drops symlinks into /usr/local/bin; node_modules/.bin is full of them. A version managernvm for Node, pyenv for Python — lets one machine hold several versions of the same language, and switches between them by re-pointing a single link. When a tool insists it's installed but the command "isn't found" — or points at the wrong version — a symlink is usually the culprit, and ls -l plus which is how you catch it.

Tip
This one is a real-machine lesson — the sandbox has no symlinks to practise on. Next time you're in a terminal, run ls -l /usr/local/bin (or ls -l node_modules/.bin in any JavaScript project) and read the arrows. It's a good five-minute tour of how your tools are actually assembled.
Vibe it

"Why does which node show one path but node --version report a different version than I expect?"

"Explain what ln -s does and when I would want a symlink instead of a copy"

10.4 Disk Detective

"Your disk is almost full" arrives at the worst possible moment, and the panic response — deleting things that look big — is how people lose work. Two commands turn the guess into a measurement.

Measure before you delete
Where did it all go?
du -sh *              # how big is each thing HERE?
204M    dist
3.1M    docs
1.9G    node_modules
12M     src

df -h                 # how full is the whole disk?
Filesystem  Size  Used Avail Capacity
/dev/disk1  494G  405G   89G     82%

du is "disk usage" — -s gives one summary line per item instead of every nested folder, and -h is the same human-readable flag you aliased onto ls in Your Shell Config. It's what turns the raw byte counts from Looking Inside Files into K, M and G, each rung roughly a thousand of the one below — so 1.9G dwarfs 12M by far more than the digits suggest. df is "disk free", the whole-disk view; a real one also prints where each disk is mounted, plus a few extra columns on macOS you can ignore.

The everyday move is du -sh *: one number per folder, sorted out by eye, and the hog is immediately obvious. It's usually node_modules, a build cache, or old container images — things that regenerate, which is exactly why they're safe to delete. One catch: * never matches a name that starts with a dot, so a .cache folder stays invisible until you name it yourself: du -sh .cache.

Tip
Measure, then delete — in that order. It's the same discipline as ls before rm in Deleting (Carefully): make the invisible visible before the irreversible step. Deleting the wrong 200 MB is annoying; deleting the wrong 200 MB because you never checked is avoidable.
Vibe it

"My disk is nearly full — walk me through finding what is using the space, biggest first"

"Is it safe to delete node_modules and .cache? What regenerates and what does not?"

Try It: Find the Space Hog

The disk is filling up. Size every folder with du -sh * first, then remove the one that's actually large — measurement before deletion.
Find the Space Hog

Loading playground...

That's the toolshed stocked. You can install what you're missing, open what arrives, follow the arrows to where things really live, and find what's eating your disk — the last of the everyday chores that used to require someone else's help.

Challenge: Hand It Over, Not the Bloat

Loading challenge...

Part 11

Terminal for the AI Era: Read, Verify, Run

"The AI writes the command. You decide whether it runs. That decision is the whole job."

Everything so far was building toward this. Your AI assistant proposes shell commands all day long — install this, delete that, rewrite those files, restart the server. Ten parts ago none of it was readable. Now every piece is: paths and wildcards, pipes and redirection, permissions, scripts, sed -i, kill -9, curl, installers. This part puts them together into the one skill the whole course exists for — reading a command you didn't write and deciding, with confidence, whether to let it run.

Note
This part comes late on purpose. Auditing a command means recognising every piece of it, so it had to wait until you had met the dangerous ones. You are not learning new commands here — you are learning what to do in the seconds before you press Enter.

11.1 Read Before You Run

Every AI-proposed command gets the same inspection before it earns your Enter key
Note
The Problem: Your agent says "run this to fix it" and hands you a line of symbols you can't read. Pressing Enter on faith works fine — right up until the day it doesn't. You need a routine that turns "looks fine, I guess" into an actual verdict, in under a minute.

And "the day it doesn't" is not hypothetical — this actually happens, to teams full of experts. In July 2025, a hacker slipped a destructive prompt into version 1.84 of Amazon's AI coding extension, instructing the agent to wipe users' systems and cloud resources. The same month, Replit's agent deleted a live production database during an explicit code freeze. And in April 2026, a Cursor-driven agent deleted a company's production database and its backups in about nine seconds. In every case the missing safeguard wasn't more expertise on the team — it was a human who read the command before approving it.

Here's the routine. Four steps, same order, every time. It feels slow for the first week; after that it takes fifteen seconds and runs in your head automatically.

1

Name the command

The first word is the program. Do you know what it does? rm deletes, curl downloads, chmod changes permissions. If the first word is a stranger, man <command> or a quick "what does this do?" to your AI comes first.

2

Read each flag

Flags change behavior, sometimes drastically: rm and rm -rf are different animals. And a letter means whatever its own command decided it means (Getting Help) — so look each one up with man or --help. Never wave through a flag you can't explain.

3

Identify what it targets

The most important question: what does this act on? A path? A wildcard? The current directory? Check where you are with pwd, and remember that * means "everything here" — whatever "here" happens to be.

4

Rehearse it

Before the real thing, run a harmless preview: echo the command to see what the wildcards expand to, or ls the target to see what would be hit. Many commands have a built-in rehearsal flag — --dry-run or -n — that shows what would happen without doing it.

Here's the routine applied to a real suggestion. The agent proposes rm -rf build/* — deleting is irreversible (Copy, Move, Delete), so it earns the full treatment:

Auditing before deleting
# Step 1-2: rm deletes; -r recurses into folders, -f skips confirmation.
# Step 3: the target is build/* - everything inside build/, wherever I am.
pwd
# /home/vibe/projects/webapp        <- good, I'm where I think I am

# Step 4a: rehearse the glob - what does build/* actually expand to?
echo rm -rf build/*
# rm -rf build/assets build/index.html build/main.js

# Step 4b: or just look at the target directly
ls build/

# Verdict: only generated files. NOW it earns the Enter key.
rm -rf build/*
Tip
explainshell is your second opinion. Paste any command into explainshell.com and it dissects every piece — command, flags, arguments — with the matching lines from the actual man pages. It's the fastest way to check an AI's explanation against the documentation, because unlike the AI, explainshell only quotes the manual.

The Red-Flag List

Some patterns should make you slow down every time, no matter how confident the agent sounds. None of these are forbidden — they all have legitimate uses — but each one is a "stop and run the full audit" trigger:

rm -rf — recursive, forced deletion

No trash can, no confirmation, no undo (Copy, Move, Delete). The danger scales with the target: rm -rf build is routine, rm -rf * depends entirely on where you're standing, and anything involving ~ or / deserves a hard stop.

sudo — anything

sudo removes every guardrail the system has. The rule from sudo doubles here: never sudo a command you don't understand — especially one an AI wrote. Understand first, elevate second.

curl ... | bash — run code straight off the internet

You decoded this pattern in curl — Ask the Networkcurl fetches a script off the internet, and the pipe feeds it straight into a shell that runs it, sight unseen. What's left is the judgement call, and honesty requires a 2026 update: this is now an official install method. Claude Code's documented installer is curl -fsSL https://claude.ai/install.sh | bash, and Codex CLI ships the same way. Four letters ride along there: f fails on a server error instead of saving the error page as your script, s silences the progress bar, S puts the real error messages back, and L follows redirects — so the script that reaches your shell can come from a different host than the address you read. Which is why the real rule is about the source: piping a script over HTTPS from the documented domain of a vendor you chose is a normal install path; piping one from a snippet someone pasted in a forum thread, a README, or an agent's suggestion is not. When in doubt the two-step version is always available — curl -o install.sh <url>, read it, then run it — and a package manager (brew, apt, or Windows' wingetPackage Managers) remains the more auditable alternative. Downloading first unlocks one more check, too: vendors publish a SHA-256 fingerprint beside their installers, and shasum -a 256 install.sh prints the fingerprint of the file you actually received (Linux spells it sha256sum). If the two differ, the file is not what they shipped — stop.

> — onto a file you care about

From Text & Pipes: > truncates first — the old contents are gone before the new ones arrive. An agent "updating" your .bashrc with > instead of >> just erased it. Check the arrow count.

chmod 777 — everyone may do everything

The "make the error go away" hammer (Permissions & Config). It works by giving every user on the system full control of the file — which is almost never what the situation actually needs. If an agent reaches for 777, ask it what the minimal permission would be; the answer is usually 755 or 644.

sed -i — a silent mass edit with no undo

From Text Surgery: -i rewrites the real files in place, often across a whole glob of them, with no preview and no backup. Unlike the flags above, the fix isn't refusal — it's amendment. Ask for -i.bak, which keeps every original beside its edited version, and you get an undo button for free.

kill -9 — reached for first, not last

From Processes & Ports: plain kill asks a program to stop and lets it save its work; -9 removes it mid-sentence, skipping every cleanup. It's a legitimate last resort and a poor default — when an agent opens with it, ask whether the polite version was tried.

The newest red flag: commands the agent was tricked into proposing

One more pattern, unique to the AI era: prompt injection. Any text an agent reads — a README, a GitHub issue, a dependency's docs — can contain instructions that steer the commands it proposes next. Mozilla warned in June 2026 about exactly this kind of indirect injection through repositories, and Microsoft's "prompts become shells" research (May 2026) showed injected text escalating all the way to remote code execution in agent frameworks — someone else, somewhere else, running whatever commands they like on your machine. The consequence for you: an agent's proposed command deserves the full audit even when you didn't write the prompt that produced it — the instruction may not have come from you at all.

Important
You are the approval step. Every major agent — Claude Code, Codex CLI, Copilot's agent mode — asks "allow this command?" before running it, and OS-level sandboxing is becoming the default posture underneath — Seatbelt on macOS, bubblewrap on Linux, the operating system itself fencing in what a command can reach, a harder wall than the practice sandbox you've been running commands in. That permission prompt is the modern read-before-you-run: the skill this course teaches is what you do in the seconds it's on screen. Don't approve on autopilot — an agent can propose commands faster than you can casually skim them, and it only takes one unread rm to ruin an afternoon.

Try It: Audit the Agent

An agent left three proposed commands in agent-plan.txt — two are safe, one would wipe files you care about. Read the plan with cat, audit each command (rehearse with echo and ls!), run the safe ones, and don't run the trap.
Audit the Agent

Loading playground...

Vibe it

"Explain this command flag by flag before I run it, and tell me exactly which files it will touch"

"Rewrite this command with a dry-run or preview step first, so I can see what it would do"

Challenge: Approve the Release Plan

Loading challenge...

Part 12

Your Cockpit: Making the Terminal a Place You Like

"If you're going to live in this window, you might as well like it: readable fonts, a prompt that helps, history that finds last Tuesday's command."

You have the skills. This short part is about the room — turning the default terminal into a cockpit: colors and a prompt you actually like, history tricks that recall any command in two keystrokes, the integrated terminal in VS Code where vibe coders live, and running several terminals at once without losing your mind.

12.1 Make It Yours

Same shell underneath — but a window you chose is a window you'll open more often

The default white-on-black is fine, but "fine" is not what you want from a tool you'll live in. Every terminal app ships with themes, font settings, and transparency — five minutes of setup pays off every day after:

macOS: Terminal.app got good again

macOS 26 Tahoe gave the stock Terminal.app its first real overhaul in roughly 24 years — 24-bit true color (~16.7 million shades, where older terminals were stuck with 16 or 256), Powerline font support, and a batch of fresh themes. Settings → Profiles (a profile is one saved bundle of font, colors and behavior): pick one, hit "Default" to keep it. As a beginner you genuinely don't need to install anything. iTerm2 remains the solid power-user choice — hundreds of importable color schemes, better splits, better search — and its AI features are an optional, separate plugin, off by default.

Windows: Windows Terminal

Already the default on Windows 11, and the host for your WSL and Git Bash sessions. Settings → Color schemes ships with One Half, Solarized, and Tango out of the box, and each profile (Ubuntu, Git Bash, PowerShell) can have its own theme — a handy visual cue for which shell you're in.

Linux: GNOME Terminal & friends

GNOME Terminal → Preferences → Profiles: colors, fonts, transparency. Konsole (KDE) and others offer the same. Profiles are cheap — make a garish red one for any terminal that's SSH'd into something important.

Everywhere: the font matters

Monospaced means every character takes the same width — the only reason columns of output line up, so the choice is less taste than it looks. Pick one with a legible 0-vs-O and a size you don't squint at. JetBrains Mono, Fira Code, and Cascadia Code are free favorites.

Everywhere: the clipboard bridge

Terminal to clipboard without a mouse: on macOS ps aux | pbcopy copies the whole output and pbpaste brings the clipboard back into a pipe. Linux says xclip (or wl-copy), WSL says clip.exe. The everyday move for handing a wall of output to an AI chat without screenshotting your terminal.

The upgrade pick: Ghostty

Ghostty (v1.3, 2026; macOS & Linux) is the current favorite when you outgrow the stock app: extremely fast, native-feeling, free and open source, and deliberately AI-free. Your shell, prompt, and everything in this course carry over unchanged.

The AI-first one: Warp

Warp builds an AI agent directly into the terminal. Know what you're choosing: it went open source in 2026, but it wants an account, and its AI runs on a paid, metered credit system — an AI-first terminal with a subscription posture, not a free tool. Nothing it offers replaces being able to read the commands yourself.

A taste of prompt customization

The prompt itself — that user@host:~$ from the intro — is just a variable named PS1, short for prompt string 1, and you set it the way you set any variable (Environment Variables):

PS1 exists (try it, it only lasts until you close the window)
PS1="\w > "
# ~/projects >              <- your prompt is now the current directory

PS1="🌲 \W $ "
# 🌲 projects $             <- yes, emoji work

The name implies a PS2, and there is one: the continuation prompt — the bare > you get if you press Enter with a quote still hanging open. That's the shell asking you to finish the line, not an error.

Hand-rolling a fancy PS1 with colors and git status is a classic rabbit hole. The modern shortcut is Starship — a fast, cross-shell prompt that works in bash, zsh, and every OS in this course. One install, one line in your shell config (Your Shell Config, .bashrc / .zshrc), and your prompt shows the current directory, git branch, language versions, and whether the last command failed — the useful stuff, with zero maintenance.

Pair it with a couple of small zsh plugins — extra files your shell config loads at startup, the same mechanism as the aliases you added there yourself, only someone else wrote the file. Two earn their place: zsh-autosuggestions greys in the rest of a command as you start typing it, and zsh-syntax-highlighting turns a command red before you press Enter if the shell can't find it. A framework like oh-my-zsh bundles hundreds instead, and charges you startup time for the ones you'll never use.

Tip
Practice the audit from Read Before You Run here. Starship's install command is a curl ... | sh one-liner — the red-flag pattern, with one detail worth catching: it ends in sh, not bash. sh is an older, more minimal shell that on many systems isn't bash at all — a quiet swap of what runs the script, and the kind of detail an audit is for. Perfect low-stakes rehearsal: download the script first, skim it (or ask your AI to), then run it. Trusted source, verified anyway — that's the habit.
Vibe it

"Help me install starship and add it to my shell config — explain each step before we run it"

"Set up a terminal color scheme and font that are easy on the eyes for long sessions on my OS"

Try It: Design Your Prompt

Enough talk — build one. Pick a design, make it yours, and take the real starship.toml home. This is a genuine config for Starship, the cross-shell prompt — nothing here touches your machine until you choose to install it.

Preset
Palette
Segment style
Icons
Layout
Blank line above
Path depth
Prompt character
Modules to show
zsh — starship
Take it to your terminal
One-command install

Pastes into zsh, writes your config, enables Starship, and reloads — assuming it's already installed. Everything's inline (not a curl … | sh), so you can .

starship.toml
"$schema" = 'https://starship.rs/config-schema.json'

add_newline = false

format = """
[░▒▓](fg:#7aa2f7)\
$directory\
[](fg:#7aa2f7 bg:#bb9af7)\
$git_branch$git_status\
[](fg:#bb9af7 bg:#7dcfff)\
$nodejs\
[](fg:#7dcfff bg:#9ece6a)\
$python\
[](fg:#9ece6a bg:#e0af68)\
$cmd_duration\
[](fg:#e0af68)\
$character"""

[character]
success_symbol = '[❯](bold fg:#9ece6a)'
error_symbol = '[❯](bold fg:#f7768e)'

[directory]
truncation_length = 3
truncation_symbol = '…/'
truncate_to_repo = false
style = "fg:#1a1b26 bg:#7aa2f7"
format = '[ $path ]($style)'

[git_branch]
symbol = ' '
style = "fg:#1a1b26 bg:#bb9af7"
format = '[ $symbol$branch ]($style)'

[git_status]
style = "fg:#1a1b26 bg:#bb9af7"
format = '[ $all_status$ahead_behind]($style)'

[nodejs]
symbol = ' '
style = "fg:#1a1b26 bg:#7dcfff"
format = '[ $symbol($version) ]($style)'

[python]
symbol = ' '
style = "fg:#1a1b26 bg:#9ece6a"
format = '[ $symbol($version) ]($style)'

[cmd_duration]
min_time = 500
style = "fg:#1a1b26 bg:#e0af68"
format = '[  $duration ]($style)'

Prefer to do it by hand? Three steps:

  1. Install Starship. Reach for a package manager first — it's the auditable route: brew install starship (macOS), winget install starship (Windows), or pacman -S starship / dnf install starship on Linux. Starship's own documented installer is a curl … | sh one-liner — the pattern you learned to stop on in Read Before You Run. If you'd rather take that route, take it in two steps and actually read the script in the middle:
    curl -fsSLo starship-install.sh https://starship.rs/install.sh
    less starship-install.sh   # read it (or hand it to your AI to explain)
    sh starship-install.sh
  2. Save the config. Put the downloaded file at ~/.config/starship.toml (run mkdir -p ~/.config first if that folder doesn't exist).
  3. Turn it on in your zsh. Add one line to your config and reload (switch shell in the bar above):
    echo 'eval "$(starship init zsh)"' >> ~/.zshrc
    source ~/.zshrc

Using a powerline or Nerd Font design? Install a Nerd Font and select it in your terminal's settings, or the arrows and icons show as boxes. The preview above draws them with CSS so you can design without one. Every design here is just a text file — open it, read it, tweak it. It's your prompt now.

Copy link saves your whole design — palette, colors and all — into a shareable URL. Bookmark it to come back to this exact prompt, or send it to a friend and they'll open the designer pre-loaded with it.

12.2 History Superpowers

You almost never type a command twice — you recall it

Watch someone fluent in the terminal and you'll notice they barely type. The shell writes down every command you run in a file it keeps between sessions — that record is your history, and it is not the same thing as scrollback, the output still sitting in the window above your prompt. Scrollback is what a screenshot captures, and it dies with the window; history outlives it. The recall tools built on that record are the biggest speed unlock in this course:

Up-arrow — the one you know

Each press steps one command back through history; Enter re-runs, or edit first. Perfect for the last two or three commands — clumsy for anything older. That's what the rest of this section is for.

history — the full ledger

Prints your numbered command history — and because it's just text output, all of Text & Pipes applies: history | grep ssh finds every ssh command you've ever run. Your history is a searchable log of how you did everything.

!! — the last command, verbatim

!! expands to your previous command. Its one legendary use: you run something, it fails with "permission denied," and sudo !! re-runs it elevated — no retyping. (Every bit of the sudo caution in sudo still applies; the shell prints the expanded command, so you see what's about to run.)

Ctrl+R — reverse search, the crown jewel

Press Ctrl+R and start typing any fragment — the shell live-searches backward through history for the most recent match. Press Ctrl+R again for older matches, Enter to run, arrow keys to edit first, Ctrl+C to bail. That 40-character command from last Tuesday? Three letters and it's back.

Edit the line — without riding the arrow keys

A recalled command usually needs one fix, and holding an arrow key across 60 characters is not the fix. Ctrl+A jumps to the start of the line, Ctrl+E to the end, Ctrl+W deletes the word behind the cursor, and Ctrl+U wipes the line entirely — the panic-adjacent one, for when you've typed something you'd rather not have sitting at a prompt. Four chords, and recall-then-edit becomes one motion.

A history session
history | grep backup
#  212  ./backup.sh notes
#  340  ./backup.sh recipes

npm run deploy
# Error: permission denied
sudo !!
# sudo npm run deploy       <- the shell shows what !! became

# (reverse-i-search)`dep': npm run deploy     <- Ctrl+R, then "dep"
Tip
Make Ctrl+R the habit. The rule of thumb: up-arrow for the last couple of commands, Ctrl+R for everything else. If you catch yourself pressing up-arrow more than three times, stop — reverse search would have had it already. And for commands you recall constantly, promote them to an alias (Your Shell Config) and stop searching altogether.

Try It: Retrace Your Steps

Ctrl+R lives at a real prompt, but you can practice the other half here: run a couple of commands, then pipe history into grep to dig one back out — and save the line you found.
Retrace Your Steps

Loading playground...

Vibe it

"Search my shell history for every command I ran involving npm and summarize what they did"

"Teach me the Ctrl+R workflow with three practice rounds using commands from this session"

12.3 Terminal in VS Code

Editor above, terminal below — one window, and it's the same bash you've been learning

Here's where all of this lands in daily life. VS Code has a full terminal built into the editor — toggle it with ⌃` (Control + backtick, same keys on every OS), and it slides up as a panel beneath your code. It's not a lookalike or a simulation — it runs your real shell, with your .bashrc, your aliases, your PATH, your history. Everything from this course works in it unchanged.

Two conveniences make it better than a separate window: it opens already cd'd into your project folder (no navigating to where your code lives — you're there), and the + button in the panel spawns extra terminals in the same place, with a dropdown to pick which shell (bash, zsh, or on Windows: WSL, Git Bash, PowerShell).

Important
This is where a lot of AI coding actually lands. When a coding agent in VS Code — Copilot, Claude Code, Cursor's agent — runs a command, it usually runs in this integrated terminal: the same panel you can read, scroll, and type into. The agent proposes npm test, you watch it execute, you scroll back through the failures, you run your own grep on the log — human and AI, sharing one shell. Every skill in this course is what lets you be a participant in that terminal instead of a spectator.

That shared visibility is the practical payoff of Terminal for the AI Era: when the agent asks permission to run a command, you audit it first; when it writes a setup.sh, you read it line by line; when it says "tests failed," you can see the exit code it saw. The integrated terminal is the one place code, agent, and shell meet — all in a single window.

VS Code even builds the red-flag mental model from Read Before You Run into settings. Copilot's agent mode runs terminal commands with per-command approval, and you can maintain an allowlist and denylist of which commands auto-approve — let git status through without asking, always stop on rm -rf and sudo. Claude Code ships a VS Code extension that drives the same CLI from a panel in the editor — same terminal, same approval moments, same skills.

Note
One habit worth stealing: keep one terminal for the agent and one for you. Click + to add yours. The agent's commands and output stay in its terminal where you can audit the transcript; your exploring (ls, cat, grep) doesn't tangle with its work. Which is the perfect segue to the next section.
Vibe it

"Run the test suite in the integrated terminal and walk me through the output you see"

"Before you run any command in this terminal, tell me what it does and wait for my okay"

12.4 Many Terminals at Once

One terminal per job: server here, logs there, you in the middle

Sooner or later one terminal isn't enough: a dev server occupies one (it runs until you stop it, holding the prompt hostage), tail -f follows a log in another (Moving Around), and you still need a free prompt to actually work. The answer is never "quit the server" — it's more terminals, and every modern terminal app makes that cheap:

Tabs

Like browser tabs: ⌘T on macOS, Ctrl+Shift+T in Windows Terminal and GNOME Terminal. Each tab is a fresh, independent shell — new history position, own working directory. Best for separate contexts: one tab per project.

Split panes

Two or more shells visible side by side in one window — best for things you watch while you work (server output, a followed log). iTerm2, Windows Terminal, and VS Code's terminal panel all split with a keystroke or the pane's context menu.

A classic three-pane cockpit for a coding session: the dev server in one pane, tail -f server.log in a second, and a free prompt in the third — with your agent's terminal from Terminal in VS Code alongside. Every shell is independent: its own working directory, its own cd, its own foreground command. Nothing you do in one pane disturbs another.

And here's the 2026 twist that turned splits from a power-user nicety into standard practice: people now run multiple AI agents in parallel — one agent per pane or tab, each pointed at its own copy of the project (git calls that a worktree) so they never collide. The split layout stops being "server here, logs there" and becomes several agents, each in its own pane: three panes, three agents on three tasks, and you sweeping your eyes across all of them, approving and course-correcting. Every pane is just a shell — which is why everything in this course scales from one terminal to ten.

One name to file away for later: tmux, a terminal multiplexer that does tabs and splits inside the terminal itself — and whose sessions survive the window closing, your laptop sleeping, even an SSH connection dropping. You log back in, reattach, and every pane is exactly where you left it (agents included — which is why people running several at once tend to live in it).

Careful with that word, though — everywhere else in this course a session is one shell's lifetime, and it dies with its window. A tmux session is a named collection of panes living in a process of its own, which is how it survives yours closing. Its keybindings feel arcane for one reason: every one of them starts with the same two keys, Ctrl+B, pressed and released before the key that does the work — so splitting a pane is Ctrl+B, then %.

Zellij is the friendlier modern multiplexer, with its shortcuts printed on screen. Overkill for today; indispensable the day you work on remote servers. It'll be waiting.

Vibe it

"Set up my layout: dev server in one terminal, log tail in a second, and a free shell for me"

"The dev server is hogging my only terminal — what are my options, and which do you recommend?"

Challenge: Hand Over the Cockpit

Loading challenge...

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...

Part 14

Conclusion: The Terminal Is Yours Now

"The terminal isn't a relic the AI era left behind — it's the interface the AI era runs on."

You started this course unable to read a shell command. Now you navigate, build, search, pipe, permission, script, and — most importantly — audit. This last part distills the mindset, hands you a reference card, sets one final challenge, and points you at the places to keep growing.

14.1 The Command-Line Mindset

Not a list of commands — a way of thinking: small tools, composed, read before run

Commands fade if you don't use them; the mindset sticks. Three ideas carry everything you've learned — and you can rehearse any of them anytime in the — a real shell sandbox, right in your browser.

1

Compose small tools

The Unix philosophy from Text & Pipes: each command does one thing well, and | snaps them together. grep doesn't sort and sort doesn't count — yet grep ERROR log | sort | uniq -c answers a question none of them could alone. When a problem looks big, don't hunt for a big tool — chain small ones.

2

Read before you run

The safety habit that threads the whole course: ls before rm, echo the glob before trusting it, count the arrows in a redirect, audit every AI-proposed command with the four-step routine from Read Before You Run. The terminal does exactly what you say, immediately, with no undo — reading first is what makes that power safe to hold.

3

The terminal is the AI-native interface

The claim from the introduction, now closing the loop: text in, text out is the language AI speaks natively, which is why every coding agent works by running shell commands. It's measurable now — Terminal-Bench (Stanford × Laude Institute) is an entire benchmark that scores AI agents on real terminal work — and it matters because trust hasn't kept up with use: Stack Overflow's 2025 survey found 84% of developers using AI tools while only about 29% trust their output. Someone still has to close that gap. You can watch what an agent runs, audit what it proposes, and step in when it's wrong — which is the whole point of learning to read the shell.

Vibe it

"Quiz me on the command-line mindset: give me five real-world tasks and check my one-liners"

"From now on, before running any command, show it to me and wait — I read before we run"

14.2 Quick Reference Card

Keep this cheat sheet handy — every command from the course, one glance away

Everything from the course, grouped the way you'll reach for it — with NAME, URL and PID standing in for your own, never typed literally (Getting Help). When a command's flags slip your mind, man is one keystroke closer than this table.

TaskCommandRemember
Orient & navigate
Where am I?pwdPrint working directory
What's here?ls -la-a shows dotfiles
Go somewherecd pathcd .. up, cd ~ home, cd - back
Create & inspect
Make folders / filesmkdir -p a/b · touch f-p builds the whole path
Read a filecat · less · head · tailq quits less; tail -f follows
Copy, move, delete
Copy / movecp -r src dst · mv src dstmv also renames
Deleterm file · rm -r dirNo trash can — ls first, always
Select many files*.log · report?.txtecho the glob before trusting it
Text & pipes
Redirect output> · >> · 2>> truncates; >> appends
Search textgrep -rin "text" .recursive, ignore case, line numbers; -v inverts
Shape a streamsort | uniq -c | sort -nuniq needs sorted input first
Find files by namefind . -name "*.md"find = filenames, grep = contents
Permissions & environment
Make runnablechmod +x script.shThen run with ./script.sh
"command not found"echo $PATH · which cmdThe shell only searches PATH
Shortcutsalias gs='git status'Persist in .bashrc / .zshrc, then source it
Text surgery
Find & replacesed 's/old/new/g' f.txtPrints the result; the file is untouched until -i
Edit the file itselfsed -i.bak 's/a/b/g' f.txtNever bare -i — the suffix is your undo
Drop / show linessed '/DEBUG/d' · sed -n '40,55p'Address picks lines, command decides their fate
Pull a columnawk '{print $2}'-F, for CSV; cut for clean delimiters
Processes, ports & the network
What's running?ps aux · pgrep nodePID is the handle; %CPU finds the runaway
Stop itkill PID · kill -9 PIDAsk politely first; -9 skips all cleanup
"Address already in use"lsof -i :3000Find the PID, kill it, start yours
Background a jobcmd & · jobs · fg %1Ctrl+Z pauses, bg resumes
Is the server alive?curl localhost:3000/health-s -o f.json saves it quietly; -I = headers
Copy files to a serverscp f host:~/ · rsync -avz d/ host:d/Like cp, across machines; rsync resumes and skips what's done
Read JSONcurl -s URL | jq -r .field-r drops the quotes for the next command
Keep a secretecho 'K=v' > .env · chmod 600 .envNever type a key into a command — history keeps it
The toolshed
Install a toolbrew install NAMELinux: sudo apt install. Confirm with which
Open an archivetar -tzf f.tar.gz · tar -xzf f.tar.gzt lists, x extracts — peek before you unpack
What's eating the disk?du -sh * · df -hMeasure before you delete
Chaining, history & help
Chain on successa && b · a || b$? holds the last exit code; 0 = success
Recall a commandCtrl+R · !! · historyCtrl+R is the biggest speed unlock
Get helpman cmd · cmd --helpq quits the pager; built-in --help is GNU-only, so man on a Mac
Practice all commandsTry-it activities in nearly every part

14.3 The Final Challenge

Reading is not knowing. Here's the exam — one gloriously messy home folder, no step-by-step instructions. Everything you need is in First Contact through Scripts & Automation, and the playground will tell you the moment you've won.

One messy home folder — navigation, organizing, searching, and scripting, all at once
Important
Your mission: a home folder where downloads, notes, and stray scripts have been piling up for months. Explore it, sort the loose files into sensible folders, hunt down the one file that contains the phrase you're told to find, and finish by writing a small executable script — every part of the course, combined, in any order that works.

Try It: One Messy Home Folder

Start with ls -la and a look around before you touch anything — read before you run, even now. A ✔ appears in the terminal when every goal is met — no partial credit.
The Final Challenge

Loading playground...

And One More: The Midnight Deploy

The first challenge covers First Contact through Scripts & Automation. This one is the power tools: a release is due, a stale server is squatting on your port, the config still says http:, and you refuse to ship on faith. Free the port, fix the config with a backup, start your server, and save the proof it's alive — Text Surgery, Processes & Ports, Talking to the Network and The Toolshed in a single sitting.

Try It: The Midnight Deploy

Four moves: find and stop whoever holds port 3000, rewrite config.yml with sed -i.bak, start the server, then curl the health endpoint into status.json. Verification is part of the job, not an afterthought.
The Midnight Deploy

Loading playground...

The Skill Checklist

Beyond the challenge, here's the honest self-test. Check each item only when you could do it right now, without looking anything up. (Saved locally in your browser — nobody's grading you but you.)

14.4 Keep Learning — The References That Matter

This course ends here — the prompt keeps going

You've practiced everything here in a real shell — but the command line is deep, and the best references are worth knowing by name. These six will cover you from quick lookups to true mastery:

The Linux Command Line— the book, free forever

William Shotts' 500-page classic, now in its 3rd edition (No Starch Press, February 2026) and still free as a PDF from linuxcommand.org. It starts exactly where this course ends and goes all the way to serious shell scripting. When you want to know why the shell works the way it does, this is the answer.

OverTheWire: Bandit— the terminal as a game

A free wargame played entirely over SSH: each level hides the password to the next somewhere in the filesystem, and your only tools are the ones from this course — ls, cat, grep, find. The most fun way to make everything here reflexive.

explainshell.com— paste a command, get the anatomy

The audit tool from Read Before You Run, permanently bookmarked. It maps every flag and argument of a pasted command to the matching lines of the real man pages — the perfect second opinion on anything an AI proposes.

tldr pages— man pages, but the good parts

Community-written cheat sheets: tldr tar gives you the five examples you actually wanted instead of forty flags. Still actively maintained in 2026, installable as a command or usable in the browser — the fastest "how do I use this again?" answer that isn't an AI.

man7.org— the manual, in a browser

The canonical Linux man pages, online and linkable — the same authoritative text man shows you locally, handy when you want to read documentation without leaving the browser (or share a link to a specific flag's definition).

Claude Code's terminal guide— even the AI vendors teach this now

The clearest sign the terminal is the AI-native interface: Anthropic publishes its own beginner terminal guide for people picking up Claude Code. Cross-check what you learned here, and note who's telling you these skills matter — the company whose agent you'll be supervising.

Your Next Course: Git

There's one command this course kept respectfully walking past, and said so at the time (Your Shell Config): git. It's the other half of the vibe coder's toolkit — the save-game system that makes AI-generated changes reviewable, undoable, and safe to experiment with. And it lives exactly where you now feel at home: the terminal.

GitVibes— the sister course, learn Git next

Same format, same free-forever deal, same in-browser playground — but for Git: commits, branches, undo, merge conflicts, and the guardrails that keep AI agents from wrecking your history. Everything you just learned about reading commands is the head start; TerminalVibes graduates are exactly who it was written for.

Important
Final Thoughts: The terminal has outlived every interface fashion of the last fifty years, and the AI era just made it more central, not less — because text is what both you and your agents speak. You can now read what the machine is told to do, verify it, and run it with intent. The promise of this course was never memorizing commands. It was having a way to read what you're about to run — especially when you didn't write it.
Challenge: Clear the Desk for the Demo

Loading challenge...