Fix: bash: ./script.sh: Permission denied (Linux, macOS, WSL)

The Error

You try to run a script or access a file and get:

bash: ./script.sh: Permission denied

Or one of these variations:

-bash: ./deploy.sh: Permission denied
zsh: permission denied: ./script.sh
EACCES: permission denied, open '/path/to/file'
cp: cannot create regular file '/some/path': Permission denied
mkdir: cannot create directory '/some/path': Permission denied

Why This Happens

Every file and directory on Linux and macOS has three sets of permissions: owner, group, and others. Each set controls read (r), write (w), and execute (x) access.

When you see “Permission denied,” the operating system is telling you that your user account lacks the required permission for the operation you attempted. The most common causes:

  • The file is not executable. You downloaded or created a script, but the execute bit is not set.
  • You don’t own the file. Another user (often root) owns it, and the permissions for “others” don’t include what you need.
  • The directory is not writable. You’re trying to create or modify a file in a directory you don’t have write access to.
  • A security module is blocking access. SELinux, AppArmor, or filesystem ACLs are denying access even though standard Unix permissions allow it.

You can inspect a file’s permissions with:

ls -la script.sh

Output like this:

-rw-r--r-- 1 root root 1024 Mar 15 10:00 script.sh

This tells you: the file is owned by root, the group is root, and the permissions are rw-r--r-- — the owner can read and write, everyone else can only read. Nobody has execute permission.

Fix 1: Make the Script Executable (chmod +x)

This is the most common fix. You have a script but the execute permission is not set:

chmod +x script.sh

Now run it:

./script.sh

If you want to set permissions more precisely, use the numeric form:

# Owner: read+write+execute, Group: read+execute, Others: read+execute
chmod 755 script.sh

# Owner: read+write+execute, Group and Others: no access
chmod 700 script.sh

Quick reference for numeric permissions:

NumberPermission
7read+write+execute
6read+write
5read+execute
4read only
0no access

Why your script lost the execute bit

  • Git does not always preserve execute bits. If someone committed a script without the execute bit, everyone who clones the repo gets a non-executable file. Fix it and tell Git to track the permission: git update-index --chmod=+x script.sh. If you’re having other Git issues, see Fix: git fatal: not a git repository.
  • Downloading from the web (via a browser or curl/wget) never sets the execute bit. You always need to chmod +x after downloading.
  • Copying from a Windows/NTFS/FAT32 filesystem strips execute bits because those filesystems don’t support Unix permissions.
  • Extracting from a zip file may not preserve permissions depending on the archiver. Tar archives (tar.gz, tar.bz2) preserve them; zip files often don’t.

Fix 2: Run the Script Through the Interpreter

If you can’t or don’t want to change permissions, run the script by passing it directly to the interpreter:

bash script.sh
python3 script.py
node script.js

This works because you only need read permission on the file — the interpreter reads it and executes the content. You’re not executing the file itself.

Fix 3: Fix File Ownership (chown)

If the file is owned by another user (typically root), change ownership:

sudo chown $USER:$USER script.sh

For an entire directory and its contents:

sudo chown -R $USER:$USER /path/to/directory

Check ownership before and after:

ls -la script.sh

When to use chown vs chmod: Use chown when the wrong user owns the file. Use chmod when the right user owns the file but the permission bits are wrong.

Fix 4: Fix Directory Permissions

Sometimes the error is not about the file itself but the directory containing it. You need execute permission on a directory to access anything inside it, and write permission to create or delete files in it:

# Make a directory accessible
chmod 755 /path/to/directory

# Make a directory and everything in it writable by you
sudo chown -R $USER:$USER /path/to/directory

A common trap: you have permission on the file but not on a parent directory in the path. Check every directory in the path:

namei -l /full/path/to/file

This prints the ownership and permissions of every component in the path, making it easy to spot which directory is blocking access.

Fix 5: Using sudo (and When Not To)

sudo runs a command as root, bypassing all permission checks:

sudo ./script.sh
sudo mkdir /opt/myapp
sudo cp config.yaml /etc/myapp/

When sudo is appropriate:

  • Writing to system directories (/etc, /usr, /var)
  • Installing system packages (apt, dnf, pacman)
  • Managing system services (systemctl)

When sudo is NOT appropriate:

Warning: Never run package managers like npm, pip, or gem with sudo unless you’re deliberately installing into a system directory. These tools execute arbitrary code from packages, and running them as root is a security risk.

Fix 6: Fix the Shebang Line

If chmod +x is set but you still get “Permission denied” or “bad interpreter,” check the first line of your script (the shebang):

head -1 script.sh

A correct shebang looks like:

#!/bin/bash
#!/usr/bin/env bash
#!/usr/bin/env python3

Common problems:

Wrong line endings (Windows CRLF): If the file was created or edited on Windows, it may have \r\n line endings. The system tries to find an interpreter called bash\r, which doesn’t exist:

# Check for Windows line endings
file script.sh
# If it says "with CRLF line terminators":
dos2unix script.sh
# Or without dos2unix:
sed -i 's/\r$//' script.sh

Missing shebang: Without a shebang, the system tries to execute the file with the default shell, which may not understand the script’s syntax. Always add a shebang as the first line.

Incorrect interpreter path: /bin/bash doesn’t exist on some systems (certain containers, NixOS). Use #!/usr/bin/env bash for portability — it finds bash wherever it’s installed.

Fix 7: EACCES in Node.js File Operations

Node.js throws EACCES when your process lacks permission to read, write, or access a file or directory:

Error: EACCES: permission denied, open '/var/log/app.log'
Error: EACCES: permission denied, mkdir '/opt/myapp/data'
Error: EACCES: permission denied, access '/usr/local/lib/node_modules'

For npm global install errors, see Fix: EACCES permission denied when installing npm packages globally.

For file operation errors in your own code:

  1. Check that the target path exists and your user owns it:
ls -la /var/log/app.log
  1. If writing to a system directory, create a dedicated directory owned by your app user:
sudo mkdir -p /var/log/myapp
sudo chown $USER:$USER /var/log/myapp
  1. If running as a service (systemd), make sure the User= directive in the unit file matches the owner of the directories the app needs:
[Service]
User=myapp
Group=myapp
ReadWritePaths=/var/log/myapp /var/lib/myapp
  1. Port binding below 1024: If your Node.js app gets EACCES when trying to listen on port 80 or 443, unprivileged users can’t bind to ports below 1024. Use a reverse proxy like Nginx on ports 80/443 and run your app on a high port (3000, 8080), or grant the capability:
sudo setcap 'cap_net_bind_service=+ep' $(which node)

Fix 8: Docker Volume Permission Issues

Files created inside Docker containers often end up owned by root on the host, or host files are inaccessible inside the container:

# Container can't write to mounted volume
docker run -v $(pwd)/data:/app/data myimage
# Error: EACCES: permission denied, open '/app/data/output.txt'

Fix: Match the container user to your host user:

docker run -u $(id -u):$(id -g) -v $(pwd)/data:/app/data myimage

Fix in Dockerfile: Create a non-root user in your image:

RUN groupadd -g 1000 appuser && \
    useradd -u 1000 -g appuser -m appuser
USER appuser

Fix with Docker Compose:

services:
  app:
    image: myimage
    user: "${UID}:${GID}"
    volumes:
      - ./data:/app/data

Fix: Reset ownership of files created by containers:

sudo chown -R $USER:$USER ./data

For more Docker permission issues, see Fix: Docker Permission Denied While Trying to Connect to the Docker Daemon Socket.

Still Not Working?

SELinux is blocking access

On Fedora, RHEL, CentOS, and Amazon Linux, SELinux may deny access even when standard Unix permissions allow it. Check for SELinux denials:

# Check if SELinux is enforcing
getenforce

# Search for recent denials
sudo ausearch -m avc -ts recent

# Check the audit log directly
sudo grep denied /var/log/audit/audit.log | tail -5

Temporarily disable SELinux to confirm it’s the cause:

sudo setenforce 0
# Try your command again
sudo setenforce 1  # re-enable immediately

For a permanent fix, use the appropriate SELinux context. For example, to let a web server read files:

sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/myapp(/.*)?"
sudo restorecon -Rv /var/www/myapp

For Docker volumes on SELinux systems, add the :z or :Z flag to the volume mount:

docker run -v $(pwd)/data:/app/data:z myimage

AppArmor is blocking access

On Ubuntu and Debian, AppArmor can block access silently. Check for denials:

sudo dmesg | grep -i apparmor | tail -10
sudo aa-status

Temporarily put a profile in complain mode:

sudo aa-complain /usr/sbin/nginx

The file has immutable attributes (chattr)

A file can be marked immutable, preventing even root from modifying or deleting it:

# Check for special attributes
lsattr script.sh

If you see i in the output (e.g., ----i---------e--- script.sh), the file is immutable:

# Remove the immutable flag
sudo chattr -i script.sh

This is uncommon but occasionally set on critical system files or by security hardening scripts.

Access Control Lists (ACLs) are overriding permissions

Standard ls -l permissions might look fine, but an ACL could be denying access. A + at the end of the permissions string indicates ACLs are set:

-rwxr-xr-x+ 1 user user 1024 Mar 15 10:00 script.sh

View the ACL:

getfacl script.sh

Remove all ACLs to fall back to standard permissions:

sudo setfacl -b script.sh

Or grant your user explicit access:

sudo setfacl -m u:$USER:rwx script.sh

The filesystem is mounted noexec

Some systems mount /tmp, /home, or external drives with the noexec option, which prevents executing any files on that filesystem regardless of file permissions:

# Check mount options
mount | grep noexec
# Or:
findmnt -t ext4,xfs,btrfs,tmpfs

If your script is on a noexec mount, you have two options:

  1. Move the script to a filesystem without noexec:
cp /tmp/script.sh ~/script.sh
chmod +x ~/script.sh
~/script.sh
  1. Run it through the interpreter (bypasses noexec):
bash /tmp/script.sh
  1. Remount without noexec (if you control the system):
sudo mount -o remount,exec /tmp

umask is creating files with restrictive permissions

Your shell’s umask controls the default permissions for newly created files. An overly restrictive umask can cause permission issues:

# Check current umask
umask

Common values:

  • 0022 — default. New files get 644 (rw-r—r—), new directories get 755.
  • 0077 — restrictive. New files get 600 (rw-------), new directories get 700. Other users can’t access anything you create.
  • 0002 — permissive. New files get 664, new directories get 775. Group members can write.

If files you create are not accessible to others (or to services running as a different user), check if your umask is 0077. Change it in ~/.bashrc or ~/.zshrc:

umask 0022

Windows WSL permission issues

WSL has its own permission model that bridges Windows and Linux. Common problems:

Files on Windows drives (/mnt/c) always show 777 or always deny access:

By default, WSL maps all Windows files to a single set of permissions. To enable proper Linux permissions on Windows drives, add to /etc/wsl.conf:

[automount]
enabled = true
options = "metadata,umask=022,fmask=133"

Then restart WSL:

wsl --shutdown

Scripts from Windows have wrong line endings:

dos2unix script.sh

chmod doesn’t seem to work on /mnt/c:

The metadata mount option (shown above) is required for chmod to work on Windows drives. Without it, permission changes are silently ignored.

File permissions work differently inside vs outside the WSL filesystem:

Store your development files inside the WSL filesystem (~/projects/) rather than on the Windows mount (/mnt/c/Users/...). The Linux filesystem supports proper permissions, is faster, and avoids line-ending issues.

SSH key or config file has wrong permissions

SSH is strict about file permissions and silently refuses to use files that are too open:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/authorized_keys

If you’re getting “Permission denied (publickey)” when using Git over SSH, see Fix: Permission denied (publickey) — Git SSH Authentication Failed.

NFS or network-mounted filesystems

Network filesystems (NFS, CIFS/SMB) often use root_squash, which maps root access to an unprivileged user. sudo may not help on NFS mounts:

# Check if the mount is NFS
mount | grep nfs

On NFS with root_squash (the default), even sudo chown will fail. The fix depends on server-side NFS export configuration. Contact the filesystem administrator or use no_root_squash in the NFS exports (security tradeoff).

Snap package confinement

Snap-installed applications run in a sandboxed environment and cannot access files outside their confinement. If a snap-packaged tool gives “Permission denied” on files in unexpected locations:

# Check if the tool is a snap
which tool-name
snap list | grep tool-name

Snaps can typically only access files under ~/ and explicitly connected interfaces. You may need to connect additional interfaces:

sudo snap connect myapp:home
sudo snap connect myapp:removable-media

Related: Fix: Docker Permission Denied While Trying to Connect to the Docker Daemon Socket | Fix: EACCES permission denied when installing npm packages globally | Fix: Permission denied (publickey) — Git SSH Authentication Failed

Related Articles