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