Module 4: Permissions, Processes, and Environment

3. Reading and changing permissions: rwx, octal, and chmod

Description

You write a script, mentally double-click it with ./deploy.sh, and the terminal hands you Permission denied with no further explanation. The file exists. You can open it in your editor, read every line, even copy it. But it does not run. Or the other way around: you clone a repository, generate an SSH key to connect to a server, and ssh flatly refuses with a message about "permissions are too open" that looks nothing like the Permission denied you already know.

By the end of this lesson you will be able to read a string like -rwxr-xr-x letter by letter and say precisely what each type of user can do with that file. You will be able to change those permissions with chmod, both in symbolic notation (u+x, go-w) and in octal (755, 644, 600), with nothing guessed and no number copied from a forum without understanding it. And you will understand why chmod 777 — which shows up in a huge fraction of Stack Overflow answers — is almost never the right answer, and what it really does when you run it.

This is not an academic exercise. A continuous integration pipeline fails because a deployment script lost its execute bit while being copied between systems. A rejected SSH key blocks a production deployment at eleven at night. An .env file with database credentials ends up readable by any other account on a shared server because nobody adjusted its permissions when creating it. All three get solved in thirty seconds if you can read and write permissions; they get solved by superstition (chmod -R 777 and praying) if you cannot.

Connection to the module: in the previous lesson you met the three-class model — owner, group, and others — and understood why Permission denied is a system feature, not a failure of yours. You deliberately stayed at the conceptual level: you knew three classes existed, but you were not yet translating letter by letter what ls -l showed you about them, and you did not have the command to change those bits. That is exactly what this lesson solves: it gives you the full vocabulary to read any permission you see on your terminal and the tool to change it with intent. The next lesson takes it a step further: it asks who is actually the owner and the group of a file, and what to do when you need privileges you do not have today.


Ten characters you already half-knew

Think of the row of ten characters ls -l puts at the start of every line as an access credential repeated three times in a row, one for each different audience: first the credential you have as owner, then the one you gave your work group, and finally the generic credential left for anyone else with an account on that machine. All three credentials answer exactly the same three questions, in the same order: can it read? can it write? can it execute or enter? Ten characters, three identically structured blocks, one after another.

That is literally what you already saw in ls -l's permissions column in the previous lesson, without having broken it down character by character yet. Let us do that now on a real example: drwxr-xr-x.

d rwx r-x r-x
│ │   │   │
│ │   │   └── others      (o) → r-x : can read and traverse/execute, not write
│ │   └────── group       (g) → r-x : can read and traverse/execute, not write
│ └────────── owner       (u) → rwx : can read, write, and traverse/execute
└──────────── entry type: d = directory ( - = regular file · l = symbolic link )

The first character is never a permission: it is the entry's type. - for a regular file, d for a directory, l for a symbolic link (there are a few rarer types, but these three cover almost everything you are going to see). The nine characters that follow group into blocks of three, and every block is always the same trio of questions in the same order: r (does it read?), w (does it write?), x (does it execute or traverse?). A dash (-) in any position means "this question is answered no" for that class.

On a regular file, the three answers mean exactly what you imagine:

PermissionOn a regular file
rYou can read its content: cat, open it in an editor, copy it
wYou can modify its content or delete it (overwrite it, truncate it, delete lines)
xYou can run it as a program — if the system also knows how to run it (a compiled binary, or a script with its #!/bin/bash line at the top)

Worked example

Run this in your terminal — you are going to see three lines with different structures:

ls -l

What to expect (the names, size, and date are going to vary on your machine; what matters is the permissions column):

-rw-r--r--  1 alex  staff   350 Jul 20 09:14 report.csv
-rwxr--r--  1 alex  staff   220 Jul 20 09:14 deploy.sh
drwxr-xr-x  3 alex  staff    96 Jul 20 09:14 scripts

Break each one down with the table above, without running any chmod yet:

  • report.csv → owner rw- (reads and writes, does not execute), group r-- (reads only), others r-- (reads only). Exactly what you would expect from a data file: nobody needs to "execute" a CSV.
  • deploy.sh → owner rwx (reads, writes, and executes), group r--, others r--. Someone already gave the owner execute permission for this script.
  • scripts → it is a directory (leading d), owner rwx, group r-x, others r-x. Notice neither group nor others has w: nobody outside the owner can create or delete files inside this folder, though they can enter and see what is there.

You are going to use exactly this same line-by-line reading every time you need to decide whether a Permission denied comes from a missing read, write, or execute permission — before touching anything with chmod.


What changes when the third bit lives on a directory

Up to here, x was easy to accept: on a file, executing means exactly what you imagine, running the program. But the same letter, in the same position, on a directory, does not mean "execute the folder" — folders are not programs, there is nothing to run. It means something else, and it is the confusion that costs terminal beginners the most time.

Imagine a file-storage hallway with a turnstile at the entrance and, further in, a row of labeled drawers. If you have the turnstile's key but no flashlight to read the labels from the entrance, you can walk to the back of the hallway and open a specific drawer — but only if someone already told you exactly which one, because you cannot "see" the whole row to pick one at random. If instead you have the flashlight but not the turnstile's key, you can stand at the entrance and read every label in the row — you know exactly which drawers exist and what they are called — but you cannot walk to any of them to open it.

That, with no metaphor, is the difference between r and x on a directory:

PermissionOn a directory
rYou can list the names it contains (a plain ls)
xYou can traverse it — enter with cd, or reach a specific file inside it if you already know its exact name (whether with cat, cd, or any path mentioning it)
w (together with x)You can create or delete entries inside (files or subdirectories), not edit the content of what is already there

They are two independent questions. You can have one without the other, and the result is different in each case. Let us verify it, not just read about it.

Worked example

Create a practice directory with a file inside (you do not need any special privileges, it is yours):

mkdir -p ~/lab-permissions/secrets
echo "abc123" > ~/lab-permissions/secrets/token.txt

Strip all permissions from the directory (000 = nothing, for every class):

chmod 000 ~/lab-permissions/secrets
ls ~/lab-permissions/secrets

What to expect:

ls: secrets: Permission denied

No surprises: with neither r nor x, you can neither list nor enter. Now give the owner only x (chmod 100, the 1 is exactly the execute/traverse bit, per the octal table below):

chmod 100 ~/lab-permissions/secrets
ls ~/lab-permissions/secrets

What to expect (the exact message varies a bit between macOS and Linux, but the result is the same: it fails):

ls: secrets: Permission denied

ls still fails — it needs r to read the list of names, and you did not give it that. But look at what happens if you access a file whose name you already know, instead of asking the folder to show you what it contains:

cat ~/lab-permissions/secrets/token.txt

What to expect:

abc123

It worked. x let you traverse the directory to reach token.txt, even though ls on that same directory still fails. Now flip the situation — take x away from the owner and give only r (chmod 400):

chmod 400 ~/lab-permissions/secrets
ls ~/lab-permissions/secrets

What to expect:

token.txt

Now ls does work: you can see the name. But try reading that same file you just saw listed:

cat ~/lab-permissions/secrets/token.txt

What to expect:

cat: /Users/alex/lab-permissions/secrets/token.txt: Permission denied

There is the full proof: you can see the name token.txt because you have r, but you cannot reach it because you are missing x — even though the file itself has plenty of read permission. Seeing something's name and being able to reach it are two different permissions, and this is exactly why a typical working directory carries rwxr-xr-x (755): everyone can list and traverse, only the owner can modify what is inside.

Leave everything in a usable state and clean up your lab:

chmod 755 ~/lab-permissions/secrets
rm -rf ~/lab-permissions

chmod: changing those bits, without guessing

chmod ("change mode") has two different grammars for saying the same thing, and it is worth mastering both because you are going to run into both constantly: one reads like a sentence (symbolic) and the other gets written as a number (octal).

Symbolic notation. You combine a class, an operator, and a permission:

ClassMeans
uowner (user)
ggroup
oothers
aall (all = u+g+o)
OperatorMeans
+adds the indicated permission, leaves the rest as-is
-removes the indicated permission, leaves the rest as-is
=sets the permission exactly like this, erasing whatever you did not mention

So, chmod u+x deploy.sh adds execute for the owner without touching anything else. chmod go-w file.txt removes write from group and others. chmod u=rw,g=r,o= file.txt sets exactly read-write for the owner, read-only for the group, and nothing for others — no matter what it had before.

Octal notation. Each class gets summed up into a single digit, adding the value of every permission you want turned on:

ValuePermission
4read (r)
2write (w)
1execute/traverse (x)
0nothing

You add up the values you want for each class — for example rwx is 4+2+1=7, r-x is 4+0+1=5, rw- is 4+2+0=6 — and write the three digits in a row, one per class, in the same order as always (owner, group, others):

OctalrwxTypical combinations where it shows up
755rwxr-xr-xscripts and everyday directories
644rw-r--r--normal data files (text, CSV, public configuration)
700rwx------private directories, only the owner enters
600rw-------files with secrets: private keys, .env

Worked example

Go back to your script from the start of the lesson. Create it like this, with input-only read permissions:

printf '#!/bin/bash\necho "Deploying..."\n' > deploy.sh
chmod 644 deploy.sh
./deploy.sh

What to expect:

-bash: ./deploy.sh: Permission denied

The file exists, you can read and modify it (644 = rw-r--r--), but no digit there has the execute 1. Fix it in symbolic notation, giving execute to the owner only:

chmod u+x deploy.sh
./deploy.sh

What to expect:

Deploying...

The same result, expressed in octal, would be chmod 744 deploy.sh (owner rwx = 7, group r-- = 4, others r-- = 4) — it is exactly the same operation described with the other grammar. If instead you want anyone on your team to be able to run it but not edit it, the right combination is chmod 755 deploy.sh (owner rwx, group and others r-x): they can read and run the script, nobody outside you can change a line.

Clean up the test file when you are done: rm deploy.sh.


When the shortcut gets expensive: -R, SSH keys, .env, and the 777 myth

-R changes everything it finds, with no distinction. chmod -R 755 project/ applies the same number to every file and every directory inside project/, recursively. The problem is that a typical directory needs x to be traversable, but a normal data file (a .csv, a .json, a .md) almost never should have x — it is not a program. A chmod -R 755 leaves data files marked as executable for no reason, and a chmod -R 777 on a whole project makes it writable by any account on the system, file by file. GNU Coreutils's official documentation also flags a more concrete risk: combining -R with the options that follow symbolic links can let someone introduce a symlink pointing at an arbitrary destination during the walk, and you end up changing permissions on something you never meant to touch. The right alternative is treating directories and files separately:

find project -type d -exec chmod 755 {} \;
find project -type f -exec chmod 644 {} \;

This gives every directory the x it needs to be traversable and every file the data permission it deserves, with no execute handed out to anything that does not need it. If you also have real scripts that do need to run, you add x to them specifically afterward, with chmod +x on those particular files.

SSH keys and .env files: the system requires, convention recommends. With SSH keys, this is not a suggestion: the SSH client and server themselves flatly reject the connection if the permissions are more open than expected, precisely because a private key readable by anyone stops being private.

PathRequired permissionWhy
~/.ssh/700Only you can enter your own SSH configuration directory
~/.ssh/id_ed25519 (private key)600Only you can read it; if another account could read it, it could impersonate you
~/.ssh/id_ed25519.pub (public key)644It is public by design: sharing it is the whole point
~/.ssh/authorized_keys600Defines who can log in as you; nobody else should be able to edit it

An .env file with database credentials or API keys has no operating system program watching over its permissions — nothing is going to reject your connection for having it misconfigured. But the logic is identical to a private key's: if another account on the same server can read it (644 or worse), your secrets are no longer secret. The sensible convention, applying the same principle of least privilege from the previous lesson, is chmod 600 .env: only you read, only you write, nobody else can even try.

Why chmod 777 shows up everywhere — and why it is almost never the answer. 777 means rwxrwxrwx: read, write, and execute for owner, group, and others — that is, for absolutely any account on the system. It shows up all over forums because, on the surface, it "fixes" any Permission denied: if the problem was missing read, missing write, or missing execute, 777 covers all three at once, for everyone, with no need for whoever writes it to think about which of the three was missing. It is the answer of someone who does not want to diagnose. The cost is that it completely wipes out the three-class model you learned in the previous lesson: it no longer matters whether someone should or should not have access, because everyone does. On a shared server, any other account can read, modify, or delete that file. On a container or a machine running more than one service, a compromised process in a different service can modify your files with 777, including scripts you yourself are going to run later. Almost always the real Permission denied gets fixed with a much narrower permission — chmod +x when execute is missing, or an ownership problem (who actually owns the file), which is exactly the next lesson's topic.


Common mistakes

"I gave x to this folder, it should already work" (and Permission denied still shows up). What happens: you changed the final directory's permissions in the path, but one of the parent directories along the way — maybe several levels up — does not have x for the account trying to access it. Why it happens: as you saw in the worked example, the system needs x on every directory that is part of the path in order to fully resolve it, not just the directory where the file lives. How to spot it: check each directory in the path, one by one, from the root down to the file, with ls -ld (on Linux, namei -l /full/path does exactly this in one shot, showing every segment's permissions; on macOS it does not come preinstalled, so a manual check with ls -ld works on any system). How to fix it: add x to the specific directory breaking the chain, not to all of them — usually one is enough.

"I ran chmod -R 777 . to get it to build/deploy, and now git status shows hundreds of modified files I never touched." What happens: Git stores every file's execute bit as part of its history (you see it as old mode 100644 / new mode 100755 in git diff). A chmod -R over the whole repository changes that bit on files that did not have it before, and Git interprets it as a real change, even though not a single line of content changed. How to spot it: git status shows a suspiciously large number of "modified" files right after a chmod -R, and git diff --summary confirms they are pure mode changes, with no lines added or removed. How to fix it: do not repeat -R with a single number; use the same find-by-type technique from above (directories to 755, files to 644, and +x only on the real scripts) to return every file to its correct permission before committing.

"I copied chmod 4777 from a forum thinking it was a 'more permissive' version of 777." What happens: a four-digit octal is not "777 with an extra boost" — the first digit does not represent rwx at all. It is a separate special bit: 4 turns on setuid (the file runs with its owner's privileges, no matter who runs it), 2 turns on setgid, and 1 turns on the sticky bit. chmod 4777 leaves a file -rwsrwxrwx — with an s where you expected the owner's x — that is, a binary anyone can run with whoever owns it's privileges. How to spot it: in ls -l, an s (or an uppercase S) in the owner's execute position, instead of x or -, is the unmistakable sign setuid is active. How to fix it: if you do not know for certain you need setuid (something rare outside a handful of system utilities like sudo itself), use chmod 0755 to clear that first digit and go back to normal permissions.


Exercises

1. Decode and convert

You have this ls -l line:

-rwxr-x---  1 alex  staff   512 Jul 20 09:14 run.sh

Describe in one sentence what each class (owner, group, others) can do with this file, and convert the permission to its octal equivalent.

See solution

Owner: rwx → can read, modify, and run run.sh. Group: r-x → can read and run, but not modify. Others: --- → cannot do anything at all with this file, not even read it.

In octal: owner rwx = 4+2+1 = 7; group r-x = 4+0+1 = 5; others --- = 0. The full permission is 750.

Why it works: each three-character block translates independently by adding the values 4 (r), 2 (w), and 1 (x) that are present; the final result is the concatenation of the three digits in owner-group-others order.

2. Share it with your team, not with the world

You wrote run.sh and want anyone in your work group to be able to read and run it, but nobody outside that group should even be able to open it. Write the chmod command that achieves this, in octal and in symbolic notation.

See solution

Octal: chmod 750 run.sh (owner rwx, group r-x, others ---).

Symbolic: chmod u=rwx,g=rx,o= run.sh — sets exactly those three values, with no dependency on what the file had before.

Why it works: "read and execute, not write" for the group is r-x (4+1=5); "nothing" for others is 0 or, in symbolic form, o= with no permission after the equals sign, which erases any previous permission for that class.

3. Predict before running

A directory has permissions d--x--x--x (octal 111: execute only, for all three classes, no read for anyone). If you know the exact name of a readable file inside it, can you read it with cat? Can you ls that directory and see what it contains? Justify each answer before trying it.

See solution

cat on the file with a known name does work: x on the directory lets you traverse it to reach that specific name, and from there on what decides whether you can read the file is the file's own permissions, not the directory's.

ls on the directory fails: listing the names it contains requires r on the directory itself, and here no class has it. You can enter what you already know about, but you cannot discover what else is there.

Why it works: this is exactly the pattern you saw in the worked example with secrets/111 is the real combination some systems use for directories where access to files by exact path is allowed (for example, a web server serving files by URL) without exposing a full listing to whoever should not see it.

4. The -R disaster in the repository

A coworker ran chmod -R 777 . inside a Git repository to "fix" a permissions error before committing. What do you recommend they check before doing a push, and how do they fix it without using a single number with -R again?

See solution

That they check git status and git diff --summary: they are probably going to see dozens or hundreds of files marked as modified, with mode changes (old mode 100644new mode 100755) and no real content change — the classic signature of a chmod -R that ran over the whole tree.

To fix it without repeating the same mistake: separate directories from files with find, giving 755 to directories (they need x to be traversable) and 644 to data files (they do not need execute), and adding +x specifically only to the scripts that genuinely need to run. Only then should git status go back to showing just the intentional content changes.

Why it works: Git versions the execute bit as part of a file's mode; the only way for git status to come out clean is for every file to end up with the permission it actually deserves, not a flat number applied to the whole tree.


Summary and next step

Before moving on you should be able to:

  • read any ten-character string like -rwxr-xr-x and say, without hesitating, what each class (owner, group, others) can do;
  • explain the difference between x on a file (execute) and x on a directory (traverse), and why you need x on every directory in a path, not just the last one;
  • translate between symbolic notation (u+x, g=rx, o-w) and octal (755, 644, 600) in both directions;
  • apply the right permissions for SSH keys (700/600) and for files with secrets (600), and explain why chmod 777 is almost never the correct answer to a Permission denied.

Today you solved the what — what the bits mean and how to change them — but the whole way through you assumed the file was already yours, that you could apply chmod to it without asking anyone else's permission. That is not always the case. The next lesson solves the who: what it actually means to be "owner" or belong to a "group" at the system level, how to change that identity with chown and chgrp, and what to do — with the right tool, not by reflex — when you need privileges you do not have today.


Resources