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:
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?
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
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.
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
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):
echo $SHELL
# /bin/zshcmd.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
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.
who@where:directory$. In Make It Yours you'll learn to customize it yourself.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.
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).
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.
"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.
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).
whoami
# vibeThe 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):
echo "Hello, terminal"
# Hello, terminalAsk for the time, and then wipe the slate clean:
date
# Sat Jul 12 10:15:42 PDT 2026
clear
# The screen empties - the text is still there, just scrolled out of sightThe 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:
ecoh "hello"
# bash: ecoh: command not foundRead 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.
Try It: Say Hello to the Machine
help in the terminal for the full command list.Loading playground...
"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.
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 -laisls -a -lwritten faster; add at—ls -lat— and you've asked for them sorted newest first as well. Only single letters cluster this way;--all --longdoesn't squash into anything.
-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.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:
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:
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.
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 pages — too 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:
--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
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).Loading playground...
"Explain this command flag by flag: ls -laht"
"What is crontab, and why does man 5 crontab show something different from man 1 crontab?"
Loading challenge...
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.
pwd → ls → cd — 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.
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:
pwd
# /home/vibeThat 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"):
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:
ls -a
# . .. .bashrc .config documents notes.txt projects. 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.
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 projectsThree 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.
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.)"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.
~/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:
# 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.mdThe Four Shortcuts Every Path Uses
| Symbol | Means | Example |
|---|---|---|
/ | 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.
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.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.
"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
cd "My Projects", then leave a shipped.txt behind.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.
~/projects/app but your terminal opens in ~. Every command the AI gives you assumes you've made the trip.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 wereThat 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.
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.Try It: Find the Lost API Key
pwd, ls -a, and cd to hunt it down. Type help for the full command list.Loading playground...
"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.
src folder with a components subfolder, plus a README." You could
click through a file manager… or type one line.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 verifyPlain mkdir has one rule: the parent must already exist. Ask it for a nested path and it refuses:
-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)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."
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 tooNothing 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.
"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.
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.dbThat 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 promptq 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:
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.
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 20tail -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
cd, build a project skeleton with mkdir -p and touch, then verify your work with ls and cat.Loading playground...
"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"
Loading challenge...
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.
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.
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:
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/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 insidecp 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."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.
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.mdThe 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.
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."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 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.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 existEach 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.
-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:
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, Enterrm -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.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
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.Loading playground...
"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.
.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.| Pattern | Matches | Example |
|---|---|---|
* | Any run of characters (including none) | *.log → app.log, errors.log |
? | Exactly one character | page?.html → page1.html, not page12.html |
[abc] | One character from the set (ranges like [0-9] work too) | report-[12].txt → report-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:
echo the glob first echo *.tmp
# cache1.tmp cache2.tmp scratch.tmp <- exactly what rm would receive
rm *.tmp # Now you KNOW what this deletesecho 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:
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 deepThat 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.
* 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.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
echo every glob before you commit to it; that's the skill
being tested.Loading playground...
"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"
Loading challenge...
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.
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: >.
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> 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:
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 directoryYou'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 liveWith 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.)
"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
found.txt, the scary error into errors.txt — with > and 2>.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.
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?
12Two 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.
| 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."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.
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 hackBoth 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.
| Character | In a glob (filenames) | In a regular expression (grep) |
|---|---|---|
* | Any run of characters | Zero or more of the character before it |
? | Exactly one character | An ordinary question mark |
. | An ordinary dot | Any 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 onesTry It: Find the Crash
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.Loading playground...
"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?"
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 delimiterThat 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.
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 | uniq — sort herds the duplicates together first, then uniq collapses
them.Try It: Count Before You Fix
grep ERROR server.log into wc -l to count the error lines, and save the total to a file.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.
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 /homeTwo 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.
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 1sort: 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
...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.)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.9Try 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.Loading playground...
"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 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'*.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
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.Loading playground...
"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.
Loading challenge...
Permissions & Environment: How the Machine Decides
"Permission deniedandcommand not foundaren'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.
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.
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 projectsBefore 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 -gn — g 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:
- file, d dir, l linkthe owner (you)
your group
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).
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."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.
backup.sh, but running it fails with permission denied../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 numbers — chmod 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.
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
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.Loading playground...
"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 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.
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.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.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."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.
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
nanoThe $ 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.
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.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
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.Loading playground...
"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.)
The customization you'll use most is the alias — a personal shorthand for a longer command:
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 definedThat 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):
# --- 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 matchedOne 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:
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 backThe $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:
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.
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
alias, use it, list
your aliases, and append your favorite to ~/.bashrc with echo >> so it would survive a restart.Loading playground...
"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"
Loading challenge...
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.
&& chain.6.1 Your First Script
.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.
#!/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.
chmod +x backup.sh
./backup.sh
# Backed up notes to backups/notes-backup-2026-07-12The ./ 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:
#!/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"./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$(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:
cat <<'EOF' > backup.sh
#!/usr/bin/env bash
mkdir -p backups
cp -r notes "backups/notes-$(date +%F)"
EOFRead 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.)
.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.cp copies, rm deletes, and curl fetches something off the network to run on your machine.Try It: Automate the Backup
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.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
Loading playground...
"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, 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:
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 soOn 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.
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:
# 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.; 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
$?, &&, 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.Loading playground...
"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"
Loading challenge...
Text Surgery: Find, Change, Extract
"grepasks: which lines?sedanswers: 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.
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:
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, limeRead 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:
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 wrappedThat | 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.
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."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
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.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:
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 lineThose 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/ matchAddresses 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.
"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.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 -i — edit 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.
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.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 wrongThe 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.
"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
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.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:
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 onlyThe 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.
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."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.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.
Loading challenge...
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.
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:
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 --foreverThe 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.
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.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?""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.
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.
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.
"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
ps aux,
try kill first and read what happens — then escalate. Leave the innocent server
running.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:3000 — localhost 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:
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:3000That'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.
lsof -i :3000 tells you its name."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
serve to meet the error, then work the ritual: lsof -i :3000 for the PID, kill it, and start your own.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.
./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 spotlightTwo 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.
%1 and %2."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
&, confirm it's there with jobs, then bring it forward with fg %1 and check what it left
behind.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.
Loading challenge...
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.
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:
http://localhost:3000/api/health
└─┬─┘ └───┬───┘ └─┬┘ └────┬────┘
│ │ │ └── which room inside — the path
│ │ └── which door — the port
│ └── which machine — here, this one
└── the language — httpThe 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.
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."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:
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 headersFour 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.
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."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?
curl, then save the
reply to status.json with -s -o so you have the receipt.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.
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.0A 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.
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."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
api.vibecloud.dev/releases for the latest version, pull it out with jq -r, and leave just the number in version.txt.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.
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:
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 itThat 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.
.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."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.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.
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:
scp — cp, 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 cpThat 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 firstOne 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.
ssh-keygen and installing it; it's a five-minute setup you do once per machine."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.
Loading challenge...
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.
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.
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.
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."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
cowsay fail, install it, and watch the same command start working — then
use which to see exactly where the file landed.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:
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.
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."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
tar -tzf before extracting it — then unpack it for real.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:
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.1Those 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 manager — nvm 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.
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."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.
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.
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."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
du -sh * first, then remove the
one that's actually large — measurement before deletion.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.
Loading challenge...
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.
11.1 Read Before You Run
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.
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.
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.
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.
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:
# 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/*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 Network — curl 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' winget — Package 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.
rm to ruin an afternoon.Try It: Audit the Agent
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.Loading playground...
"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"
Loading challenge...
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
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="\w > "
# ~/projects > <- your prompt is now the current directory
PS1="🌲 \W $ "
# 🌲 projects $ <- yes, emoji workThe 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.
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."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.
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 .
"$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:
- Install Starship. Reach for a package manager first — it's the auditable
route:
brew install starship(macOS),winget install starship(Windows), orpacman -S starship/dnf install starshipon Linux. Starship's own documented installer is acurl … | shone-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
- Save the config. Put the downloaded file at
~/.config/starship.toml(runmkdir -p ~/.configfirst if that folder doesn't exist). - 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
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.
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"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.Loading playground...
"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
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).
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.
ls, cat, grep) doesn't tangle with its work. Which is the perfect segue to the next
section."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
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.
"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?"
Loading challenge...
Under the Hood: The Machine You've Been Driving
"Every key you press is a byte on a wire. This is where you meet the wire."
For twelve parts you've driven this machine without once opening the hood. Now you've earned
the wrench. This part is the optional deep dive: first how the terminal actually works — ttys, PTYs, escape sequences, and what Ctrl+C really does — and then what that
machinery is becoming in the AI era, and the genuinely advanced things you can build now that you
understand it. Nothing here is required to use the terminal. All of it makes the everyday mysteries
— tty, ^[[A, Ctrl+C — feel ordinary.
13.1 How the Terminal Works
You've used every piece of this machine for twelve parts — typed at prompts, piped bytes,
interrupted stuck commands, read exit codes. Time to see inside. If you've ever wondered why
it's called a tty, why arrow keys sometimes print ^[[A instead of moving, or what Ctrl+C actually does, this is where those
mysteries get solved. It's deliberately the most technical section of the course — and it repays
the effort with a working mental model of the machine you've been driving all along.
In the beginning, the terminal was furniture. A teletypewriter — a keyboard fused to a printer — sat at the end of a serial cable, and the computer at the other end typed its replies onto rolling paper. Unix, born in that world, abbreviated the device to tty, and the abbreviation outlived the hardware by half a century. The paper is gone, the cables are gone, but every terminal window on your machine still checks in with the kernel (Stopping Things) as a tty device — ask it yourself:
tty
# /dev/ttys004 (macOS)
# /dev/pts/0 (Linux)No real teletype has been wired to a computer in decades — so today, software impersonates one. Three players stand in for that vanished machine:
Terminal emulator
The app you actually open — Terminal.app, iTerm2, Windows Terminal. It draws a grid of character cells, turns your keystrokes into bytes, and paints the bytes that come back. It emulates the old hardware — hence the name.
The PTY pair
A pseudo-terminal: two connected endpoints the kernel provides on request. The emulator holds the master end; whatever runs inside the window is attached to the slave end (newer docs say primary/secondary). It's a pipe wearing a teletype costume.
The shell
Just a process. bash reads bytes from the slave end and writes bytes back — it can't tell whether a human, a 1970s teletype, or an AI agent sits on the other side. That indifference is why the same shell works everywhere.
Keystrokes travel down the chain, output travels back up — and every hop is plain bytes.
One hop in that diagram does more than ferry bytes. Between the two ends of the PTY lives a slice of kernel code called the line discipline — a tiny line editor that decides how much of your typing to hold back and when to hand it over. It has two personalities:
Cooked mode (canonical)
The default. The kernel buffers your keystrokes, handles Backspace itself, and delivers nothing until you press Enter — then the program receives one finished line. This is why the shell never sees your typos: you edit the line before it exists.
Raw mode
Every keystroke is delivered immediately, unedited. Programs that react key by key — vim, less, the arrow-key line editor inside bash itself — switch the terminal
into raw mode while they run, and back on exit. (A crash that skips the "back" is how
terminals end up garbled — reset fixes it.)
That split personality explains a small mystery. Arrow keys don't send a character — there
is no "up" letter — they send a short byte sequence beginning with the Escape character: up
arrow is ESC [ A. A raw-mode program recognizes the sequence and moves the cursor.
Hand it to a program reading cooked input — press up while cat is waiting — and it echoes as the literal ^[[A. That gibberish is readable once you know the convention: a caret is
how a terminal writes a control character on paper, so ^[ is Ctrl+[, which is Escape, followed by the
plain characters [ and A. The terminal spoke arrow, and nobody translated.
You can inspect your own line discipline's settings any time:
stty -a
# speed 38400 baud; rows 24; columns 80;
# intr = ^C; erase = ^?; kill = ^U; eof = ^D;
# icanon iexten echo echoe echok ...The -a earns its place: a bare stty reports a couple of lines, -a reports everything — that's the Linux layout above, and macOS groups the same
settings under different headings. icanon is cooked mode's official name. erase = ^? is the kernel handling your Backspace, and ^? is the one token that breaks the
caret rule: it's Delete, not Ctrl+?. kill = ^U wipes the line you're part-way through typing — the Ctrl+U from History Superpowers, meeting its kernel name — eof = ^D on an empty line ends it — and neither has anything to do with the kill command from Stopping Things, which talks to other
processes entirely. The 38400 baud is a speed for a serial wire this PTY doesn't
have, vestigial like the name tty. And intr = ^C is a promise we'll cash in a moment.
Escape sequences run in the other direction too — they're how programs draw. A
program attached to a tty can only send bytes, so color, bold, and cursor movement travel in-band, mixed right into the text.
Characters have numeric codes underneath (Looking Inside Files), and byte 27 — ESC, written \e — announces "the next few bytes are instructions, not text", so the emulator
obeys them instead of printing them. The vocabulary was standardized in 1978 around the VT100,
a physical terminal you could put on a desk, sold by Digital Equipment Corporation (DEC) — your
gleaming modern emulator is still, at heart, impersonating it. Watch it obey:
printf '\e[32mgreen\e[0m\n'
# green (printed in actual green)\e[32m means "switch the pen to green"; \e[0m means "back to normal". That's printf and not the echo you've used all course for one reason: echo's handling
of backslashes differs from shell to shell, so \e can arrive at the terminal as
a backslash and an e. printf behaves the same everywhere, at the price of writing the trailing newline \n yourself. Every colorful ls, every fancy prompt, every full-screen dashboard is built from sequences
like these — and the colored output in this site's playground works the same way in spirit:
the text carries its own formatting, and whatever paints the glyphs interprets it.
One last piece of magic to demystify: Ctrl+C. It feels like input, but it
never reaches the program as text. When Ctrl+C's byte arrives at the line
discipline, the kernel intercepts it — that's the intr = ^C setting you just saw — and instead of passing it along, sends the foreground program a signal (Stopping Things)
called SIGINT ("interrupt"). The program can catch it and tidy up, or die on the spot. Either way, you get your
prompt back.
Ctrl+C will interrupt a stuck command, and
now you know why it's reliable: it isn't input the program must get around to reading — it's the
kernel tapping the program on the shoulder. That's why it works even when a program is too busy
to look at the keyboard. (A few programs catch SIGINT and ask questions first
— that's them trapping the signal, not you failing to send it.)You've already felt this from the other side. In Processes & Ports you sent SIGTERM with kill and watched a stubborn process shrug it off,
then sent SIGKILL with kill -9, which it could not refuse.
Same machinery, three doors: Ctrl+C is SIGINT to whatever is in
the foreground, kill is SIGTERM to any PID you name, and kill -9 is the one signal no program is allowed to catch. What looked like three
unrelated tricks is one mechanism you now understand end to end.
Put the whole chain together, and you can narrate the few milliseconds after you press Enter
on ls:
- The emulator turns Enter into a carriage-return byte — the teletype's "slam the print head back to the left margin", a different byte from the newline that rolls the paper up a line — and writes it to the PTY master, same as any other key.
- The line discipline recognizes the end of a line, converts it to a newline, and releases the whole buffered line to the slave end.
- The shell, which has been blocked reading the slave — parked by the kernel, using no CPU at all while it
waits, which is why a prompt can sit open all day for free — wakes up holding
lsand parses it, expanding any$VARIABLES,~, and globs first. - It forks — makes a copy of itself — and
inside that copy runs exec, which
throws out the shell's program and loads
lsin its place. That's why the two always travel together: fork makes the process, exec decides what it becomes. The copy's input and output are still wired to the same tty. lsdoes its work and writes its results — text plus color escape sequences — to the slave end.- The bytes flow back through the PTY to the master; the emulator reads them, obeys the escapes, and paints glyphs into its character grid.
lsexits; the shell collects its exit code (the$?you met in Scripts & Automation) and prints a fresh prompt. Your turn.
That's the entire machine: an app impersonating 1970s furniture, a kernel pipe in costume, a tiny in-kernel line editor, and a shell that just reads and writes bytes. From here on, nothing the terminal does will look like magic — every key press is a byte with a place to go, and now you can name every stop on the way.
And here's the part the museum plaque leaves out: the byte protocol didn't stop evolving in 1978. Modern terminals are still quietly minting new escape sequences. OSC 8 — an Operating System Command, the numbered family of escapes for things that aren't drawing, and despite the name nothing to do with your operating system — makes text in a terminal a real clickable hyperlink, and a family of shell-integration markers lets the shell whisper structure into the byte stream itself. That second one turns out to be the quiet foundation of the whole AI-terminal era — and it's exactly where we're headed next.
13.2 The Terminal, Evolving
Knowing how the machine works is satisfying. Knowing what people are building on it right now is useful — because the terminal is in the middle of its biggest growth spurt since the VT100, and everything driving it is a payoff of something you just learned. This section is the tour: the invisible protocol that lets editors and agents read the terminal, the new generation of terminals built around agents, and the advanced automation you can write yourself now that pipes, signals, and exit codes are yours.
The shell and the terminal are talking behind your back
Remember the trick from How the Terminal Works — instructions traveling in-band, mixed into the text? Modern shells and terminals use the same trick to
talk about the conversation itself. A standard called OSC 133 (born in the FinalTerm terminal, now adopted almost everywhere) has the shell emit invisible escape
sequences that mark the seams of every command: here the prompt starts, here the command starts, here the output begins, here it ended — with this exit code. VS Code's terminal adds its own richer
dialect, OSC 633, which also carries the command
line itself. You never see any of it — the emulator swallows the markers like it swallows \e[32m — but the byte stream is now structured:
Invisible bookmarks in the byte stream: every command arrives pre-labeled with where it began, what it was, and how it went.
Those bookmarks are why VS Code's integrated terminal (Terminal in VS Code) can
paint a little success or failure dot next to every
command you run, let you jump between commands with a keystroke, and pin the running command
to the top of the panel while output scrolls. And they matter double in the AI era: an agent
watching a terminal through OSC markers doesn't have to guess where your prompt
ends and the output begins, or grep the scrollback for the word "error" — it knows
the exact command, the exact output, and the exact exit code, machine-readably. The read-before-you-run
contract from Terminal for the AI Era works in both directions now: you can read what the
agent runs, and the agent can reliably read what happened.
The terminals being built around agents
In Make It Yours you picked a window to live in. Step back and you can see
the whole landscape splitting in two. On one side, the classic emulators keep competing on speed
and standards: Ghostty 1.3 (March 2026) restructured
itself so its entire terminal core is a reusable library — code packaged for other programs to call rather than to run, which is what the lib in libghostty marks. It's the machinery of How the Terminal Works, ready for any app that wants to embed a real terminal. On
the other side, a new generation is being designed around agents: Warp now calls itself an "agentic
development environment" and open-sourced its core — its pitch is orchestrating whole fleets
of local and cloud agents from one window, the split-pane fleet from Many Terminals at Once promoted to a first-class product. And cmux builds terminal panes that agents
can drive programmatically, through a Unix socket — a file-like endpoint two
programs on one machine use to talk. Panes as an API, not just a view.
Here's the through-line, and it's the whole reason How the Terminal Works was worth your time: every one of these — the speed demons, the agent fleets, the socket-driven panes — still speaks the same protocol. Bytes through a PTY, escape sequences in-band, signals from the line discipline. A fifty-year-old interface turned out to be so simple, so universal, and so automation-friendly that when AI agents needed a body, they moved into the terminal. The machinery didn't get replaced by the AI era; it got adopted by it.
The agent is a shell command now
The adoption runs deeper than windows and panes. Headless agent CLIs make the agent itself a composable Unix tool: run claude -p ("print mode") and the agent reads stdin and writes stdout — the numbered streams from Redirection — and sets an exit code, the exact contract grep and sort have honored since 1973. Which means everything you learned in Text & Pipes and Permissions & Config applies, unchanged, to intelligence itself:
# Pipe a diff in, get a review out — stdin to stdout, like any tool
git diff main | claude -p "review this diff; list issues as filename:line"
# JSON output makes it pipeline-friendly: hand it to jq like anything else
# --oneline = one commit per line; -20 = only the last 20 of them
git log --oneline -20 \
| claude -p "summarize this week's work in one paragraph" --output-format json \
| jq -r '.result'Read that first line again with Text & Pipes eyes: a program's output flowing through | into another program's input. The second program just happens to be a language model. It sorts
into pipelines, redirects into files, chains with &&, and reports success through $? — the Unix philosophy, now with a very well-read tool in the toolbox. (The
second block is one command typed across three lines: a trailing backslash tells the shell the
line isn't finished, so it keeps reading instead of running what it has. Type that backslash and
press Enter yourself and the > that comes back is bash saying "go on, I'm still listening" — not an error.)
Writing scripts that deserve the word "automation"
And that unlocks the last upgrade: scripts that orchestrate agents need to be sturdier than
the five-liners from Scripts & Automation, and you finally know enough to write the
grown-up kind. Every robust bash script starts with the same three lines of armor — each one
cashing in a lesson from this part. set does nothing on its own: it flips switches
that stay flipped for the rest of the script, and this particular combination is common enough
to have a name — strict mode.
#!/usr/bin/env bash
set -euo pipefail
# -e stop at the first failing command (no barreling on after an error)
# -u treat unset variables as errors (catches $TYPO before it deletes ~)
# -o pipefail a pipeline fails if ANY stage fails, not just the last
scratch=$(mktemp -d) # -d = a directory, not a file: a fresh empty one in
# the system temp dir (/tmp on Linux), new name each run
cleanup() { rm -rf "$scratch"; }
trap cleanup EXIT INT # runs on normal exit AND on Ctrl+CThe line above the trap defines a function: cleanup() names a block of commands, the braces hold the body, and the ; before the closing brace is required. Defining it runs nothing — which is the
whole point, because the next line hands the name to something that will run it later.
That something is trap, and it's the SIGINT lesson from How the Terminal Works cashed in: Ctrl+C sends a signal, signals can
be caught, and trap is how a script catches one. Its first argument is the handler;
the rest are signal names with the SIG dropped, so INT is SIGINT. EXIT isn't a signal at all — it's bash's own invention meaning "on the way out,
however that happens", which is what makes the cleanup run on the ordinary path too. Put the armor
on a real job and you get something like this: a script that runs an agent review over every file
you've changed and collects the results —
#!/usr/bin/env bash
set -euo pipefail
scratch=$(mktemp -d)
cleanup() { rm -rf "$scratch"; }
trap cleanup EXIT INT
git diff --name-only main > "$scratch/changed.txt"
while read -r file; do
echo "reviewing $file ..."
claude -p "review this file; list issues as line: problem" \
< "$file" > "$scratch/$(basename "$file").review"
done < "$scratch/changed.txt"
cat "$scratch"/*.review > review-report.txt
echo "done: $(wc -l < "$scratch/changed.txt") files reviewed -> review-report.txt"The loop is the one piece of shell grammar this course hasn't handed you yet. while read -r file; do … done runs its body once per line: read takes the next line and drops it into $file, and -r tells it to leave any backslashes in that line alone
rather than reading them as special. When the lines run out, read reports failure
— and that failure is what stops the loop. The redirect hangs off done rather than off a command, which is what makes it feed the whole loop: read draws from the file instead of your keyboard. Inside, basename strips the folders off src/lib/foo.ts, because $file arrives as a path and a path can't be a flat filename inside $scratch.
The loop's sibling turns up in agent transcripts even more often: for walks a list instead of a file. for f in *.txt; do wc -l "$f"; done reads: expand the glob (Wildcards — the shell does it before the loop starts), park each name in $f in turn,
run the body. Same skeleton — do … done — and the same quoting habit around "$f", for the same spaces-in-filenames reason as ever. You now
read both of the loops the shell knows, which is most of the loop-shaped code an agent will
ever ask you to approve.
cron, the scheduler that has shipped with every Unix since before screens. crontab -e opens your personal schedule in an editor; each line is five time fields, then a command — 0 9 * * 1-5 ~/backup.sh notes means minute 0, hour 9, any day of the month, any
month, Monday through Friday. You met its manual pages back in Getting Help: man 5 crontab is the field-format page, and this
is where that early example finally pays off. A real-machine tool — the sandbox has no clock ticking
overnight — and the reason a server can feel staffed at 3 a.m.Count the course in that script: redirection and pipes (Text & Pipes), a loop
feeding on a file, scripting and variables (Scripts & Automation), signals and cleanup
(this part), and an AI agent doing the reading — supervised by a script you can read line by line. If it fails halfway, set -e stops it; if you Ctrl+C it, the trap tidies up. This is what "advanced automation"
actually looks like: not longer commands — stronger habits.
"Write me a bash script that runs claude -p over every file changed since main and collects the output into one report — with set -euo pipefail and a cleanup trap"
"Take my deploy script and harden it: strict mode, a trap that cleans up temp files on Ctrl+C, and mktemp instead of fixed /tmp paths — explain each change"
grep, and write scripts that clean up after
themselves when signals fly. That's a working model of the machine most day-to-day use never
requires — and the tour is nearly over. One part to go: the send-off.Loading challenge...
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
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.
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.
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.
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.
"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
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.
| Task | Command | Remember |
|---|---|---|
| Orient & navigate | ||
| Where am I? | pwd | Print working directory |
| What's here? | ls -la | -a shows dotfiles |
| Go somewhere | cd path | cd .. up, cd ~ home, cd - back |
| Create & inspect | ||
| Make folders / files | mkdir -p a/b · touch f | -p builds the whole path |
| Read a file | cat · less · head · tail | q quits less; tail -f follows |
| Copy, move, delete | ||
| Copy / move | cp -r src dst · mv src dst | mv also renames |
| Delete | rm file · rm -r dir | No trash can — ls first, always |
| Select many files | *.log · report?.txt | echo the glob before trusting it |
| Text & pipes | ||
| Redirect output | > · >> · 2> | > truncates; >> appends |
| Search text | grep -rin "text" . | recursive, ignore case, line numbers; -v inverts |
| Shape a stream | sort | uniq -c | sort -n | uniq needs sorted input first |
| Find files by name | find . -name "*.md" | find = filenames, grep = contents |
| Permissions & environment | ||
| Make runnable | chmod +x script.sh | Then run with ./script.sh |
| "command not found" | echo $PATH · which cmd | The shell only searches PATH |
| Shortcuts | alias gs='git status' | Persist in .bashrc / .zshrc, then source it |
| Text surgery | ||
| Find & replace | sed 's/old/new/g' f.txt | Prints the result; the file is untouched until -i |
| Edit the file itself | sed -i.bak 's/a/b/g' f.txt | Never bare -i — the suffix is your undo |
| Drop / show lines | sed '/DEBUG/d' · sed -n '40,55p' | Address picks lines, command decides their fate |
| Pull a column | awk '{print $2}' | -F, for CSV; cut for clean delimiters |
| Processes, ports & the network | ||
| What's running? | ps aux · pgrep node | PID is the handle; %CPU finds the runaway |
| Stop it | kill PID · kill -9 PID | Ask politely first; -9 skips all cleanup |
| "Address already in use" | lsof -i :3000 | Find the PID, kill it, start yours |
| Background a job | cmd & · jobs · fg %1 | Ctrl+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 server | scp f host:~/ · rsync -avz d/ host:d/ | Like cp, across machines; rsync resumes and skips what's
done |
| Read JSON | curl -s URL | jq -r .field | -r drops the quotes for the next command |
| Keep a secret | echo 'K=v' > .env · chmod 600 .env | Never type a key into a command — history keeps it |
| The toolshed | ||
| Install a tool | brew install NAME | Linux: sudo apt install. Confirm with which |
| Open an archive | tar -tzf f.tar.gz · tar -xzf f.tar.gz | t lists, x extracts — peek before you unpack |
| What's eating the disk? | du -sh * · df -h | Measure before you delete |
| Chaining, history & help | ||
| Chain on success | a && b · a || b | $? holds the last exit code; 0 = success |
| Recall a command | Ctrl+R · !! · history | Ctrl+R is the biggest speed unlock |
| Get help | man cmd · cmd --help | q quits the pager; built-in --help is GNU-only, so man on a Mac |
| Practice all commands | Try-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.
Try It: One Messy Home Folder
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.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
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.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
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.
Loading challenge...