Skip to content
Home » “zsh: permission denied” on macOS: The Three Causes and Their Fixes

“zsh: permission denied” on macOS: The Three Causes and Their Fixes

zsh: permission denied looks like one error, and it is really three. The shell says it when a file has no execute bit, when the thing you pointed at cannot be executed at all, and when the filesystem will not let you near it. Each has a different fix, and one of the three is not a permissions problem in the first place.

The examples below were run on macOS 26.5.2. The first step is reading the whole line rather than the last two words, because macOS puts the answer in the part everyone skips.

Who printed the error

Everything before the first colon is the program that refused, and it narrows the problem immediately:

zsh: permission denied: ./hello.sh      ← the shell could not execute it
cd: permission denied: somedir         ← a builtin, so it is about the directory
cat: /etc/sudoers: Permission denied    ← a command, so it is about reading the file

Note the capitalisation too. Lowercase permission denied comes from zsh itself; capitalised Permission denied is the system error text passed through by a command. That single letter tells you whether the shell gave up before starting anything, or a program started and then hit a wall.

Cause 1: no execute bit

This is the common one. A script you wrote, downloaded, or copied is a perfectly good file that has never been marked executable:

$ ls -l hello.sh
-rw-r--r--@ 1 osxhub  wheel  22 20 Jul 14:00 hello.sh

$ ./hello.sh
zsh: permission denied: ./hello.sh

Read the first field. -rw-r--r-- has no x anywhere in it. After chmod +x hello.sh it reads -rwxr-xr-x and the script runs. The owner column is redacted in these listings; nothing else is changed.

Which x bits you get is worth a look, because a bare +x is more generous than most people expect and quietly obeys your umask:

$ chmod +x a.sh   ; ls -l a.sh
-rwxr-xr-x@ 1 osxhub  wheel  19 20 Jul 14:10 a.sh

$ chmod u+x b.sh  ; ls -l b.sh
-rwxr--r--@ 1 osxhub  wheel  19 20 Jul 14:10 b.sh

$ (umask 077; chmod +x c.sh; ls -l c.sh)
-rwx------@ 1 osxhub  wheel   2 20 Jul 14:10 c.sh

So +x marks it executable for everyone under the usual umask of 022, u+x keeps it to you, and under a stricter umask the same +x hands out less. None of that matters on a personal Mac; it matters a great deal on anything shared. It is also the reason chmod 777 — still the first suggestion on plenty of forums — is the wrong reflex: it grants write access to every account on the machine to fix a missing execute bit.

There is a second way out that does not touch permissions at all. Handing the file to an interpreter runs it as data rather than as a program, so no execute bit is involved:

$ chmod -x hello.sh
$ zsh hello.sh
hello

That is the right move for a script you are running once and do not own — a colleague’s file on a shared volume, something inside a checkout you would rather not modify.

The exit status distinguishes this case from a missing command without looking at anything else:

$ ./hello.sh ; echo $?
zsh:1: permission denied: ./hello.sh
126

$ nosuchcmd ; echo $?
zsh:1: command not found: nosuchcmd
127

126 means the file was found and could not be run. 127 means nothing was found at all, which is a different problem with a different fix — see “zsh: command not found” on macOS. In a script, testing for 126 rather than “did it fail” saves a lot of guessing.

Cause 2: it is not something you can run

Pointing the shell at a directory produces exactly the same wording as a script without an execute bit:

$ ./somedir
zsh: permission denied: ./somedir

Tab completion makes this easy to hit, and the message gives no hint that the target is a directory. ls -ld ./somedir settles it in one step: a leading d means you have been trying to execute a folder.

Directories use the execute bit for something else entirely — it controls whether you can enter them:

$ chmod -x somedir
$ cd somedir
cd: permission denied: somedir

A directory you can read but not enter will list its contents and refuse everything else, which produces some genuinely confusing behaviour until you know that x on a directory means “traverse”.

Cause 3: the file is not yours to read

$ cat /etc/sudoers
cat: /etc/sudoers: Permission denied

Here the file is fine and your account simply is not allowed. ls -l on the path shows who owns it and what the other-user bits are; if the owner is root and the last three characters are ---, no amount of retrying will change the outcome. sudo is the answer when you genuinely need the file, and a signal to stop when you do not.

Prefixing sudo onto anything that fails is the habit worth avoiding. Cause 1 and cause 2 above are not fixed by sudo — it will happily run a script as root that you could have run as yourself, and it will not make a directory executable.

The one chmod cannot fix

$ touch /usr/testfile
touch: /usr/testfile: Operation not permitted

Different wording, different mechanism. “Operation not permitted” is macOS telling you a protection layer above the permission bits said no, and on a modern Mac there are two of those. System Integrity Protection makes the system volume read-only regardless of ownership, which is why this one survives sudo. Privacy protection (TCC) is the other: a Terminal without Full Disk Access gets the same wording for files in Documents, Desktop and similar folders, and the fix there is granting the app permission in System Settings rather than touching the file.

The practical rule: if ls -l shows permissions that should allow what you are doing, and it still fails, you are looking at SIP or TCC and chmod is the wrong tool.

Where the execute bit actually goes missing

“Unzipping strips the execute bit” gets repeated a lot. On macOS 26.5.2 it does not — both archivers preserve mode on a file that went in as -rwxr-xr-x:

$ unzip -q -o ../pack.zip && ls -l hello.sh
-rwxr-xr-x@ 1 osxhub  wheel  22 20 Jul 14:00 hello.sh

$ tar -xzf pack.tgz -C out2 && ls -l out2/hello.sh
-rwxr-xr-x@ 1 osxhub  wheel  22 20 Jul 14:00 out2/hello.sh

What does lose it is anything that creates a new file rather than restoring one. A download lands as -rw-r--r-- because that is what the default umask of 022 produces, and the same goes for shell redirects and editor saves. FAT and exFAT volumes are a case of their own: they cannot record Unix modes at all, so a file copied onto one shows a mode macOS invents for it, and running chmod against it changes nothing in the listing.

So the honest summary of cause 1 is not that something stripped the bit — it is that the bit was never set, because the file was created rather than copied. chmod +x once, and it stays.

Leave a Reply

Your email address will not be published. Required fields are marked *