Two numbers cover most of what you will ever set with chmod. 755 is rwxr-xr-x: the owner can do everything, everyone else can read and execute. 644 is rw-r--r--: the owner can read and write, everyone else can only read. The working rule is 755 for directories and for files you run, 644 for every other file. Here is why, and where that rule breaks.
To check any mode as you read, open the chmod calculator for octal and symbolic permissions. Tick the boxes or type an octal mode such as 2775, and it shows the rwxr-xr-x form, a plain-English explanation, an ls -la preview and the chmod command to copy. It also handles the special bits and umask.
How the three digits work
Each digit is three bits: read is 4, write is 2, execute is 1, and the digit is the sum of the bits that are on. The first digit applies to the file's owner, the second to its group, the third to everyone else.
| Digit | Sum | Symbolic | Grants |
|---|---|---|---|
| 7 | 4 + 2 + 1 | rwx | read, write, execute |
| 6 | 4 + 2 | rw- | read, write |
| 5 | 4 + 1 | r-x | read, execute |
| 4 | 4 | r-- | read only |
| 0 | 0 | --- | nothing |
Because each octal digit is exactly three binary bits, 755 is 111 101 101; the number base converter shows that for any mode. To read the mode of an existing file:
$ stat -c '%a %A %n' deploy.sh # Linux (GNU coreutils)
755 -rwxr-xr-x deploy.sh
$ stat -f '%Lp %Sp %N' deploy.sh # macOS
755 -rwxr-xr-x deploy.sh
One detail catches people out: the kernel applies exactly one class. If you own the file, only the owner digit counts, even when the group or others digit is more generous. A file at 077 is readable by everyone except its owner, whose cat fails with "Permission denied". Root skips most of these checks.
What r, w and x mean on a directory
A directory is a list of names, and the bits mean something different there:
- r lets you list the names (
ls). - x lets you traverse it:
cdinto it and reach anything inside by name. - w lets you create, delete and rename entries, but only together with x.
With r but no x, a regular user on Linux gets the names and nothing else:
$ ls onlyr
known.txt
$ ls -l onlyr
ls: cannot access 'onlyr/known.txt': Permission denied
total 0
-????????? ? ? ? ? ? known.txt
$ cat onlyr/known.txt
cat: onlyr/known.txt: Permission denied
The reverse, x without r, makes ls onlyx fail with "cannot open directory", yet cat onlyx/known.txt works because you knew the name. That is why 644 is wrong for a directory: even its owner can't enter it. It is also why nginx returns 403 for /home/alice/site/index.html when /home/alice is 700: every directory on the path needs x for nginx's user.
Deleting a file changes the directory, not the file: with w and x on a directory, you can delete a root-owned 600 file in it that you can't even read. The sticky bit, below, fixes that.
Which mode for which file
| Mode | Symbolic | Use it for |
|---|---|---|
| 755 | rwxr-xr-x | Directories, scripts, compiled binaries |
| 644 | rw-r--r-- | Regular files: source, config, HTML, images |
| 750 / 640 | rwxr-x--- / rw-r----- | Shared with one service group, hidden from other users |
| 700 | rwx------ | ~/.ssh, ~/.gnupg, private working directories |
| 600 | rw------- | SSH private keys, .env files with secrets, ~/.pgpass |
| 400 | r-------- | Keys you never want to edit by accident |
| 777 | rwxrwxrwx | Nothing you care about |
OpenSSH enforces the private-key row. Leave a key at 644 and ssh refuses to use it:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.
Fix it with chmod 600 ~/.ssh/id_ed25519 and chmod 700 ~/.ssh. On the server, sshd's StrictModes (on by default) rejects authorized_keys when it or the directories above it are writable by group or others.
As for 777: any user or process on the machine can rewrite the file, and a world-writable script run by root or cron is a privilege-escalation path. When 777 "fixes" a permission error, the real problem is ownership: chown the files to the service's user, or share a group and use 775 and 664.
Symbolic mode: change one bit, keep the rest
An octal mode sets all the bits at once. Symbolic mode edits them: who (u owner, g group, o others, a all), an operator (+ add, - remove, = set exactly) and the bits.
chmod u+x deploy.sh # 644 → 744
chmod go-w shared.txt # 777 → 755
chmod a=r notes.txt # 644 → 444
chmod u=rwx,go= private/ # 755 → 700
Note that = resets only the classes you name: chmod u=rwx on a 644 file gives 744, not 700, so spell out go= to clear the rest. Also, chmod +x with no class means "all, minus whatever the umask masks": under umask 077 it turns 644 into 744, while chmod a+x gives 755.
Recursive chmod without making everything executable
The classic mistakes:
chmod -R 755 site/ # every file becomes executable too
chmod -R 644 site/ # worse: directories lose x
The second breaks itself halfway: chmod removes x from site/, then can't get inside it. As a regular user you get:
chmod: cannot access 'site/index.html': Permission denied
chmod: cannot access 'site/css': Permission denied
Handle directories and files separately with find:
find site -type d -exec chmod 755 {} +
find site -type f -exec chmod 644 {} +
That strips x from scripts too. Capital X is smarter: it adds execute only to directories and to files already executable by someone.
chmod -R u=rwX,go=rX site/
On a tree with directories at 700, a.txt at 600 and run.sh at 700, this gives directories 755, a.txt 644 and run.sh 755. The script stays runnable and nothing else gains execute.
umask: where 644 and 755 come from
Programs usually create files with mode 666 and directories with 777, and the kernel then clears every bit set in your umask. With the common umask 022, write is removed for group and others: 666 becomes 644 and 777 becomes 755.
The umask is a bit mask, not a number you subtract. The result is mode AND NOT umask, which differs from subtraction whenever the umask names a bit that was never there. Umask 033 shows it: subtraction predicts 633 for files, but the real result is 644, because new files never had x to remove.
| umask | New files | New directories | Typical use |
|---|---|---|---|
| 022 | 644 | 755 | Common default |
| 002 | 664 | 775 | Teams sharing a group |
| 027 | 640 | 750 | Servers: nothing for others |
| 077 | 600 | 700 | Single-user secrets |
| 033 | 644 | 744 | Proof that it isn't subtraction |
The calculator's umask section does this with checkboxes. Set the umask in your shell profile, or for a systemd service with UMask= in the unit file. It applies only when a file is created, never to existing files, and programs can ask for less: ssh-keygen writes private keys as 600 regardless.
setuid, setgid and the sticky bit
An optional leading digit holds three special bits: 4 is setuid, 2 is setgid, 1 is sticky. A missing digit means 0, so 0644 and 644 are the same mode.
| Mode | ls -l shows | Effect |
|---|---|---|
| 4755 | -rwsr-xr-x | The program runs as the file's owner. /usr/bin/passwd is 4755 and owned by root, which is how it can update password files. Linux ignores setuid on scripts. |
| 2775 | drwxrwsr-x | On a directory, new files inherit its group: ideal for shared team directories. |
| 1777 | drwxrwxrwt | In a directory everyone can write to, only a file's owner, the directory's owner or root can delete or rename it. This is /tmp. |
The s or t replaces the x in the listing. A capital letter means the special bit is set but the x beneath it isn't: chmod 4644 shows -rwSr--r--, which is almost always a mistake. GNU chmod on Linux keeps a directory's setgid bit when you give a numeric mode, so chmod 755 on a 2775 directory leaves 2755; use chmod g-s or chmod 00755 to clear it. macOS chmod clears it.
Git and Docker: where modes get lost
Git records only two modes for a regular file, 100644 and 100755. Commit a 600 secret and scripts at 700 and 750, and a fresh clone (umask 022) gives 644 and 755:
$ git ls-files -s
100755 f2ad6c76f0115a6ba5b00456a849810e7ec0af20 0 group.sh
100755 61780798228d17af2d34fce4cfbdf35556832472 0 run.sh
100644 78981922613b2afb6025042ff6bd878ac1994e85 0 secret.env
Only the execute bit survives version control. On Windows, where files have no execute bit, mark a script executable with git update-index --chmod=+x deploy.sh and commit. With core.fileMode set to false, Git ignores executable-bit changes in your working tree.
Docker's COPY keeps the mode bits from the build context but makes the files owned by root unless you say otherwise. A 600 file lands as 600 root:root, and a container running as a non-root USER gets "Permission denied" reading it. Fix it in the Dockerfile:
COPY --chown=app:app config/ /app/config/
COPY --chmod=644 app.conf /etc/app/app.conf
Combined with Git, this is why an image can differ between your laptop (file at 600) and CI's fresh checkout (644).
Next steps
Paste the mode from a failing deploy into the chmod calculator to see who can do what. If a script runs from your shell but not from cron, check its x bit and read cron syntax explained. For the network side, see CIDR notation and subnet masks explained.