chmod on a Mac: changing who can read and write a file
Most searches for chmod on a Mac start from a specific refusal. A script will not run. A folder copied from a server cannot be opened. A build step writes into a directory and gets Permission denied. The command itself is small enough to memorise in a minute, and that is the problem: it gets memorised as a spell rather than understood, and the version that gets memorised is usually chmod 777.
What follows is the mode system as macOS actually implements it, with the behaviours that are easy to state wrongly checked on macOS 27. The parts worth attention are what the execute bit means on a directory, why capital X exists, and the four separate reasons chmod can appear to succeed and change nothing.
Reading the mode before changing it
Every file carries a mode: three bits each for the owner, the group, and everyone else. Run ls -l and the mode is the first column.
-rwxr--r--@ 1 admin wheel 0 Sep 26 11:23 f.txt
The leading character is the file type, a dash for a regular file and d for a directory. Then three groups of three: read, write, execute for the owner, then the group, then others. The trailing @ means the file also carries extended attributes, which ls -l@ will list and xattr -l will print. A + in that position means an access control list is attached, which matters later.
The octal form of the same thing comes from stat:
stat -f "%A %Sp %N" f.txt
744 -rwxr--r-- f.txt
Read is 4, write is 2, execute is 1, and the three digits are the sums for owner, group, and others. So 744 is read, write and execute for the owner and read only for everyone else. 644 is the same without the owner's execute bit. 755 adds execute back for group and others, and is the normal mode for a directory or an executable.
Checking the mode before changing it sounds like advice nobody needs, and then a recursive chmod goes out across a project directory and the only record of what the modes used to be was the output nobody captured. stat -f "%A %N" across the tree, saved to a file, costs nothing and turns an irreversible change into a reversible one.
Octal and symbolic, and when each is the right choice
Two notations reach the same bits. Octal replaces the whole mode. Symbolic adjusts part of it and leaves the rest alone.
chmod 644 report.txt
chmod u+x deploy.sh
chmod go-w shared.conf
chmod a+r public.html
Symbolic mode takes a target, an operator, and a set of bits. The targets are u for the owner, g for the group, o for others, and a for all three. The operators are + to add, - to remove, and = to set exactly, discarding whatever was there for that target.
The distinction that matters in practice is that octal is absolute. chmod 644 on a file that was 755 removes the execute bit whether or not that was intended. chmod u+x on a file that was 640 leaves the group's read bit untouched. When the goal is "make this one thing true", symbolic is the safer instrument. When the goal is "put this file into a known state", octal is clearer to read six months later in a script.
The = operator deserves a mention because it is the only symbolic form that removes bits without naming them. chmod go= private.key leaves the owner's bits alone and clears everything for group and others, which is more legible than working out that the octal is 600.
One habit that prevents a class of accident: on a directory, run the octal form on the directory itself and the symbolic form recursively on its contents, rather than one recursive octal pass over both. Directories and files want different modes, and a single number cannot be right for both.
What r and x mean on a directory
This is where the model stops being an analogy and starts producing surprising behaviour, and it is worth demonstrating rather than describing.
A directory holding one file, note.txt, was set to three different modes and the same two commands were run against it each time.
At 500, read and execute for the owner, listing the directory returned note.txt and reading the file returned its contents. Normal.
At 400, read without execute, ls printed the name note.txt and also printed ls: fts_read: Permission denied. Reading the file returned Permission denied. The names were visible. Nothing behind them was reachable.
At 100, execute without read, ls returned Permission denied for the directory itself, but reading note.txt by its full path returned the contents.
So on a directory, read grants the ability to list names, and execute grants the ability to use those names. They are separate powers and either can be held without the other. A directory with read but no execute is a catalogue of things that cannot be opened. A directory with execute but no read is a room with no light, where anything can be reached if the path is already known.
That is the explanation for the most common recursive chmod accident. Running chmod -R 644 over a tree is a reasonable instruction for files and a disaster for directories, because it strips the execute bit from every directory and nothing below them can be reached any more. The tree looks intact in a listing and is unusable.
Why capital X exists
The fix for that state is capital X, and it is the single most useful flag in the whole command.
Lowercase x sets the execute bit unconditionally. Capital X sets it only on directories, and on files that already have an execute bit set for somebody. It skips ordinary files entirely.
The behaviour was checked directly. A directory containing one plain file had execute removed from everything, then chmod -R a+X was run over it. Afterwards the directory read 755 and the plain file read 644. The directories came back. The data file was left alone, and did not become an executable.
That is the repair for a tree that had chmod -R 644 run across it, and it is also the correct way to set modes on a tree in the first place: chmod -R a+X for traversal, then the file modes separately.
For finer control, find separates the two cases explicitly, which is worth using when the tree contains real executables whose modes should not be touched:
find . -type d -exec chmod 755 {} +
find . -type f -exec chmod 644 {} +
The four reasons chmod appears to do nothing
A chmod that exits without an error and leaves the mode unchanged is the most confusing state to be in. There are four common causes, and they are distinguishable.
| Symptom | Cause | How to confirm |
|---|---|---|
| Mode unchanged, no error | The target is a symbolic link | ls -l shows l as the first character |
| Mode unchanged, no error | The volume has no POSIX modes | mount shows exFAT, FAT32, or a mounted share |
| Mode looks right, access still refused | An access control list overrides it | ls -le lists entries below the file |
| Operation not permitted | System Integrity Protection, or not the owner | The path is inside a protected system directory |
The symlink case is the quietest. chmod follows symbolic links by default, so chmod 600 on a link changes the file the link points at and leaves the link's own mode alone. Checked on macOS 27: a link at 755 pointing to a file at 755 was given chmod 600, and afterwards the target read 600 while the link still read 755. Adding -h changes the link itself instead.
The filesystem case comes up with USB drives and mounted shares. exFAT and FAT32 have no concept of a POSIX mode, so the mode shown in ls -l is synthesised from the mount options and chmod has nothing to write to. Changing the mount options is the only lever, and for a network share the answer is on the server rather than the Mac.
The ACL case is the one most likely to waste an afternoon, because the mode in ls -l looks permissive and the operation still fails. A file was set to 777, then given a single ACL entry denying delete to its own owner. Deleting it returned rm: acl.txt: Permission denied, with the mode reading rwxrwxrwx. ls -le printed the reason on the line below:
-rwxrwxrwx 1 admin wheel 0 Sep 26 11:23 acl.txt
0: user:admin deny delete
ACL entries are evaluated before the mode, so a deny entry cannot be overridden by any chmod of the numeric kind. They are added with chmod +a, removed with chmod -a, and cleared entirely with chmod -N. The habit worth forming is to reach for ls -le rather than ls -l the moment a permission result contradicts the mode.
The last case, Operation not permitted on a system path, is System Integrity Protection refusing the change. It is not a permissions problem to be solved with a bigger hammer, and reaching for sudo will not help. The right response is to place the file somewhere writable instead.
What 777 actually does, and why the default is 022
chmod 777 grants read, write and execute to every account on the machine. On a single-user laptop that sounds harmless, and it is the reason the command spreads: it makes the immediate error go away.
What it also does is remove the distinction between the owner and everything else, permanently, with no record of what the mode was before. On a directory, it means any process running as any user can write into it. On a shared machine, a build server, or anything reachable over file sharing, that is a different proposition entirely. And it rarely fixes the actual problem, because a refusal that survives 755 is usually an ACL or an ownership issue, not a mode issue.
The system's own default says what a reasonable mode looks like. The shell's umask on a fresh macOS account is 022, which masks the write bit for group and others. A new file therefore lands at 644 and a new directory at 755. Those two numbers cover the great majority of real cases, and a mode that needs to be more permissive than 755 is worth a second look before it is applied.
Where the answer genuinely is "these two accounts need to write to the same folder", the tool is group ownership plus an ACL, not a blanket 777. chmod +a "staff allow write,delete,add_file,add_subdirectory" grants exactly that and leaves everyone outside the group where they were.
Seeing the modes while working with the files
The friction in all of this is that the mode lives in one window and the files live in another. Checking a mode means leaving the folder view, running ls -le, reading the output, and going back. When a recursive change is about to run, the thing most worth having on screen is the tree it will touch.
A file manager with a built-in terminal removes that switch, because the shell and the listing share a working directory. Run find . -type d -exec chmod 755 {} + and the pane above shows the tree it just changed. The Features page describes what that arrangement covers, and the Compared with other file managers page sets out how it differs from the alternatives.
What to change first
Stop reaching for 777 and reach for ls -le instead, because a refusal that contradicts the mode is an ACL and no number will fix it. Use chmod -R a+X rather than a recursive octal mode, so directories keep their traversal bit and data files do not become executables. If moving between the listing and the command is the slow part, Atriens keeps both in one window.
Frequently asked questions
Why does chmod say nothing changed when the mode is clearly wrong?
Three likely causes. The target is a symbolic link, so chmod changed the file it points at rather than the link; add -h to change the link. The volume is exFAT, FAT32, or a mounted share with no POSIX modes, in which case the displayed mode comes from the mount options. Or an access control list is overriding the mode, which ls -le will show.
What is the difference between `chmod -R a+x` and `chmod -R a+X`?
Lowercase x sets the execute bit on everything it touches, including data files, which makes text files and images look like programs. Capital X sets it only on directories and on files that already had an execute bit for somebody. For a tree containing a mix of both, capital X is almost always the one intended.
Is `chmod 777` ever the right answer?
Rarely, and almost never as a first attempt. It removes the distinction between the owner and every other account on the machine, and it does not fix refusals caused by access control lists or by ownership. When two accounts genuinely need to write to the same directory, group ownership with an ACL grants that without opening the directory to everything else.
How do you undo a recursive chmod that broke a project folder?
If the modes were not recorded beforehand, run chmod -R a+X to restore traversal on the directories, then find . -type f -exec chmod 644 {} + for the files and set the real executables back individually. Next time, save stat -f "%A %N" output for the tree first, which makes the change reversible.
Why does Get Info show different permissions than `ls -l`?
Get Info shows a simplified view built from both the mode and any access control list, collapsed into Read & Write, Read only, or No Access per entry. ls -l shows only the mode, and ls -le shows the ACL entries separately. When the two disagree, the ACL is usually the part Get Info is folding in.