TerminalVibes / Permissions & Config
Part 5

Permissions & Environment: How the Machine Decides

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

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

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

5.1 Reading ls -l — The 10-Character Rulebook

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

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

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

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

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

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

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

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

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

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

5.2 chmod — Changing the Rules

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

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

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

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

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

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

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

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

755 — scripts & directories

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

644 — regular files

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

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

Try It: The Script Won't Run

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

Loading playground...

Vibe it

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

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

5.3 sudo — Borrowed Superpowers

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

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

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

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

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

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

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

5.4 Environment Variables — The Invisible Settings

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

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

echo $USER
vibe

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

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

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

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

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

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

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

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

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

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

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

Try It: command not found

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

Loading playground...

Vibe it

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

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

5.5 Your Shell Config — Make It Stick

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

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

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

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

alias                     # List every alias currently defined

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

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

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

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

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

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

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

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

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

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

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

Try It: Make Your Shortcuts

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

Loading playground...

Vibe it

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

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

Challenge: Nothing in the Kit Will Run

Loading challenge...