Back to Blog
Tutorial

Fixing 'Permission Denied' Errors in Your Cron Jobs

Your script works perfectly in the terminal but fails in cron with 'Permission Denied'? This step-by-step guide solves the most common cron errors: file permissions, user context, and PATH issues.

10 min read
By Cron Generator Team

You run your script from the terminal. It works perfectly. You add it to cron. You wait. Nothing happens.

You check the logs. There it is: "Permission Denied".

This is one of the most frustrating problems in system administration. Your command works manually, but cron refuses to run it. You've checked the script three times. The cron syntax is correct. Yet it still fails.

The good news: This is almost always one of three fixable problems:

  1. File permissions (the script isn't executable)
  2. User permissions (wrong user running the cron job)
  3. PATH issues (cron can't find your commands)

This guide walks through each problem step-by-step and shows you exactly how to fix it.

The Problem: Why Cron Behaves Differently

First, understand why this happens at all.

When You Run a Command in Your Terminal

Your terminal session has:

  • Your user context - All your permissions and access rights
  • Full environment variables - $PATH, $HOME, $USER, etc.
  • Interactive shell - .bashrc and .bash_profile loaded
  • Current directory - You're in a specific location

Example: When you run ./backup.sh from /home/user/scripts/, your shell:

  1. Uses your user permissions
  2. Knows where to find commands (via $PATH)
  3. Can access files you own
  4. Executes from the current directory

When Cron Runs a Command

Cron operates in a minimal, non-interactive environment:

  • ⚠️ Limited user context - Runs as a specific user (often not the one you expect)
  • ⚠️ Minimal PATH - Usually just /usr/bin:/bin
  • ⚠️ No interactive shell - .bashrc is NOT loaded
  • ⚠️ Different working directory - Often starts in / or the user's home

Example: When cron runs ./backup.sh:

  1. It might be running as a different user
  2. It can't find commands not in the minimal PATH
  3. It can't access files owned by other users
  4. The ./ relative path doesn't work (wrong directory)

This is why "it works in the terminal" doesn't mean it works in cron.

Diagnostic Step: Check Your Cron Logs

Before fixing anything, see what's actually failing.

On Most Linux Systems

# Check system log for cron errors
grep CRON /var/log/syslog | tail -20

On Ubuntu/Debian

# View cron logs
tail -f /var/log/cron.log

On Red Hat/CentOS/Fedora

# View cron logs
tail -f /var/log/cron

On macOS

# View system log
log show --predicate 'process == "cron"' --last 1h

Common Error Messages

"Permission denied"

Jun 10 02:00:01 server CRON[12345]: (user) CMD (/path/to/script.sh)
Jun 10 02:00:01 server CRON[12345]: (user) MAIL (mailed 1 byte of output but got status 0x0001: Permission denied)

"No such file or directory"

/bin/sh: 1: /home/user/script.sh: not found

"Command not found"

/bin/sh: 1: my-command: not found

Now let's fix each problem systematically.

Problem 1: File Permissions (The Script Isn't Executable)

This is the most common cause of "Permission Denied" errors.

The Issue

Your script file exists, but it doesn't have execute permissions. When cron tries to run it, the system says "You can't execute this file."

How to Check

ls -l /path/to/your/script.sh

Example output:

-rw-r--r-- 1 user user 1234 Jan 09 10:00 backup.sh

Reading the permissions:

-rw-r--r--
│││││││││└─ Others: read
│││││││└─── Others: no write
│││││└───── Others: no execute ❌
││││└─────── Group: read
│││└──────── Group: no write
││└───────── Group: no execute ❌
│└────────── Owner: read
└─────────── Owner: write
            Owner: no execute ❌

Problem identified: No execute permission (x) anywhere. The file is readable and writable, but not executable.

The Fix: Make the Script Executable

chmod +x /path/to/your/script.sh

Verify the fix:

ls -l /path/to/your/script.sh

Now you should see:

-rwxr-xr-x 1 user user 1234 Jan 09 10:00 backup.sh

Permission breakdown:

-rwxr-xr-x
      ││└─ Others: can execute ✅
      │└── Group: can execute ✅
      └─── Owner: can execute ✅

Understanding chmod

The chmod command changes file permissions.

Common patterns:

| Command | Meaning | Result | |---------|---------|--------| | chmod +x file.sh | Add execute for everyone | -rwxr-xr-x | | chmod u+x file.sh | Add execute for owner only | -rwxr--r-- | | chmod 755 file.sh | Owner: rwx, Group: rx, Others: rx | -rwxr-xr-x | | chmod 700 file.sh | Owner: rwx, Others: none | -rwx------ | | chmod 644 file.sh | Owner: rw, Others: r (not executable) | -rw-r--r-- |

For cron scripts, use:

chmod 755 /path/to/script.sh  # Standard: anyone can execute

Or for more security:

chmod 700 /path/to/script.sh  # Only owner can execute

Test the Script Manually

After adding execute permissions, test it:

/path/to/script.sh

If you see:

bash: /path/to/script.sh: Permission denied

The execute permission still isn't set correctly. Re-run chmod +x.

If the script runs: You've fixed the execute permission problem. If cron still fails, continue to the next section.

Special Case: Shebang Line Required

Make sure your script starts with a shebang (#!) line:

#!/bin/bash

# Your script content here

Without a shebang, the system doesn't know which interpreter to use. Add one of these at the very first line:

| Shebang | Use For | |---------|---------| | #!/bin/bash | Bash scripts | | #!/bin/sh | POSIX shell scripts | | #!/usr/bin/python3 | Python 3 scripts | | #!/usr/bin/env python3 | Python 3 (uses PATH) | | #!/usr/bin/node | Node.js scripts | | #!/usr/bin/env node | Node.js (uses PATH) | | #!/usr/bin/php | PHP scripts |

Problem 2: User Permissions (Wrong User Context)

Even if the script is executable, it might be running as the wrong user.

The Issue

Cron jobs run under specific user contexts. If the cron job runs as www-data but the script is owned by john, you'll hit permission problems.

Identify Which User Runs the Cron Job

Method 1: Check which crontab you're editing

# Your personal crontab (runs as YOU)
crontab -e

# Another user's crontab (runs as THAT user)
sudo crontab -u username -e

# Root's crontab (runs as ROOT)
sudo crontab -e

Method 2: Add a test job to see who's running

# Add this temporary cron job
* * * * * whoami > /tmp/cron-user.txt

Wait one minute, then check:

cat /tmp/cron-user.txt

This shows which user executed the cron job.

Common Scenarios

Scenario 1: Personal crontab

crontab -e

Jobs run as your user (e.g., john).

Scenario 2: System crontab

sudo nano /etc/crontab

Jobs run as the user specified in the 6th field:

# Format: minute hour day month dow USER command
0 2 * * * root /path/to/script.sh

This runs as root.

Scenario 3: /etc/cron.d/ files

sudo nano /etc/cron.d/myapp

Same format as system crontab—specify the user:

0 3 * * * www-data /var/www/myapp/backup.sh

This runs as www-data.

The Fix: Match User Permissions

You have two options:

Option A: Run cron as a user who has access

If your script needs to access files owned by john, run the cron job as john:

# Edit john's crontab
sudo crontab -u john -e

Or in system crontab:

0 2 * * * john /home/john/scripts/backup.sh

Option B: Change file ownership to match the cron user

If cron runs as www-data, give ownership to www-data:

sudo chown www-data:www-data /path/to/script.sh
sudo chmod 755 /path/to/script.sh

Verify ownership:

ls -l /path/to/script.sh

Should show:

-rwxr-xr-x 1 www-data www-data 1234 Jan 09 10:00 script.sh

Directory Permissions Matter Too

The script might be executable, but if it's in a directory the cron user can't access, it still fails.

Check directory permissions:

ls -ld /path/to/
ls -ld /path/to/scripts/

Fix directory permissions:

sudo chmod 755 /path/to/scripts/

Rule of thumb: All parent directories need at least execute permission for the user to traverse them.

Files the Script Accesses

Your script might access other files (config files, data files, etc.). Those need proper permissions too.

Example script:

#!/bin/bash
# This script reads config and writes to log

source /home/john/.config/app.conf  # Needs read permission
echo "Backup started" >> /var/log/backup.log  # Needs write permission

Fix permissions:

# Allow cron user to read config
chmod 644 /home/john/.config/app.conf

# Allow cron user to write to log (or create log with right ownership)
sudo touch /var/log/backup.log
sudo chown www-data:www-data /var/log/backup.log
sudo chmod 644 /var/log/backup.log

Testing with sudo

Test your script as the cron user would run it:

# If cron runs as www-data, test as www-data
sudo -u www-data /path/to/script.sh

If this fails with permission errors, you've found the problem. Fix the file permissions for that user.

If this succeeds, the user permissions are correct. Move to the next section.

Problem 3: The PATH Problem (Cron Can't Find Commands)

This is the sneakiest problem. Your script runs fine manually but fails in cron with "command not found."

The Issue

Your interactive shell has a full $PATH that includes many directories:

echo $PATH

Example output:

/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/user/.local/bin

Cron's PATH is minimal:

/usr/bin:/bin

If your command lives in /usr/local/bin or any other location, cron won't find it.

How to Check

See cron's PATH:

Add this temporary cron job:

* * * * * echo $PATH > /tmp/cron-path.txt

Wait one minute, then check:

cat /tmp/cron-path.txt

Example output:

/usr/bin:/bin

Compare this to your interactive $PATH. Notice what's missing?

The Fix: Use Absolute Paths

Instead of relying on $PATH, use the full path to every command.

❌ Wrong (relative paths):

#!/bin/bash
mysqldump -u root mydb > backup.sql
gzip backup.sql
aws s3 cp backup.sql.gz s3://my-bucket/

✅ Correct (absolute paths):

#!/bin/bash
/usr/bin/mysqldump -u root mydb > /var/backups/backup.sql
/usr/bin/gzip /var/backups/backup.sql
/usr/local/bin/aws s3 cp /var/backups/backup.sql.gz s3://my-bucket/

Finding the Full Path to Commands

Use which or whereis:

which mysqldump
# Output: /usr/bin/mysqldump

which python3
# Output: /usr/bin/python3

which node
# Output: /usr/local/bin/node

which aws
# Output: /usr/local/bin/aws

Update your script to use these full paths.

Alternative: Set PATH in the Script

You can define $PATH at the top of your script:

#!/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin

# Now you can use relative commands
mysqldump -u root mydb > backup.sql
gzip backup.sql

Or set PATH in the crontab itself:

# In crontab -e
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin

0 2 * * * /home/user/scripts/backup.sh

This gives all cron jobs in that crontab the extended PATH.

Special Case: Language Version Managers

If you use nvm (Node.js), rvm (Ruby), pyenv (Python), or similar tools, cron won't load them automatically.

Problem:

# This fails in cron
node /path/to/script.js

Because node is managed by nvm, which isn't loaded in cron's non-interactive environment.

Solution 1: Use absolute path to the version

/home/user/.nvm/versions/node/v18.0.0/bin/node /path/to/script.js

Solution 2: Source the version manager in the script

#!/bin/bash
# Load nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# Now node is available
node /path/to/script.js

Solution 3: Use system-installed versions

Instead of version managers, install Node/Python/Ruby system-wide:

sudo apt install nodejs

Then use the system path:

/usr/bin/node /path/to/script.js

Current Directory Issues

Your script might assume it's running from a specific directory.

❌ Wrong (relative paths in script):

#!/bin/bash
# Assumes we're in /home/user/scripts/
source ./config.sh  # Fails: cron isn't in this directory
python3 process.py  # Fails: can't find process.py

✅ Correct (absolute paths or explicit cd):

#!/bin/bash
# Change to script directory first
cd /home/user/scripts/ || exit

# Now relative paths work
source ./config.sh
python3 process.py

Or use absolute paths everywhere:

#!/bin/bash
source /home/user/scripts/config.sh
/usr/bin/python3 /home/user/scripts/process.py

Complete Troubleshooting Checklist

Work through this checklist systematically:

1. Check File Permissions

# Is the script executable?
ls -l /path/to/script.sh

# Should show: -rwxr-xr-x (or at least -rwx------)
# If not, fix it:
chmod +x /path/to/script.sh

2. Check Script Ownership

# Who owns the script?
ls -l /path/to/script.sh

# Example: -rwxr-xr-x 1 john john
# Script is owned by 'john'

3. Check Which User Runs the Cron

# Add test job to see user
* * * * * whoami > /tmp/cron-user.txt

# Wait a minute, then check
cat /tmp/cron-user.txt

4. Test as the Cron User

# If cron runs as 'www-data', test as www-data
sudo -u www-data /path/to/script.sh

# If this fails, you have a permission problem
# Fix file/directory permissions for that user

5. Check for Shebang Line

# First line of script should be:
head -n 1 /path/to/script.sh

# Should show: #!/bin/bash (or appropriate interpreter)

6. Use Absolute Paths

# In your script, use full paths to commands
which mysqldump  # Find full path
which python3    # Find full path
which node       # Find full path

# Update script to use: /usr/bin/mysqldump, etc.

7. Set Working Directory

# In your script, change to the right directory
cd /path/to/script/directory || exit

8. Check Cron Logs

# View what actually happened
grep CRON /var/log/syslog | tail -20

# Or for detailed output, redirect in crontab:
* * * * * /path/to/script.sh >> /tmp/cron-output.log 2>&1

9. Capture Error Output

# In crontab, redirect both stdout and stderr
0 2 * * * /path/to/script.sh >> /var/log/cron-job.log 2>&1

# Now check the log file
cat /var/log/cron-job.log

10. Test the Cron Expression

# Make sure your cron timing is correct
# Use https://crontab.guru or our generator

# Test with a frequent schedule first
* * * * * /path/to/script.sh >> /tmp/test.log 2>&1

# Once it works, change to your real schedule

Real-World Examples: Before and After

Example 1: Python Script Not Executable

❌ Before (doesn't work in cron):

# Crontab entry
0 2 * * * /home/user/scripts/backup.py

# Script file
-rw-r--r-- 1 user user 456 backup.py

Error: "Permission denied"

✅ After (works):

# Add shebang to script
#!/usr/bin/env python3

# Your script code

# Make executable
chmod +x /home/user/scripts/backup.py

# Verify
-rwxr-xr-x 1 user user 456 backup.py

Example 2: Wrong User Running Cron

❌ Before (doesn't work in cron):

# Crontab: runs as www-data
sudo crontab -u www-data -e
0 2 * * * /home/john/backup.sh

# Script ownership
-rwx------ 1 john john 789 backup.sh

Error: "Permission denied" (www-data can't access john's files)

✅ After (works):

# Option A: Run as john instead
sudo crontab -u john -e
0 2 * * * /home/john/backup.sh

# Or Option B: Make accessible to www-data
chmod 755 /home/john/backup.sh
chmod 755 /home/john  # Directory needs execute permission too

Example 3: PATH Problem

❌ Before (doesn't work in cron):

#!/bin/bash
# Script uses node, but node is in /usr/local/bin
node /var/www/app/process.js

Error: "node: command not found"

✅ After (works):

#!/bin/bash
# Use absolute path
/usr/local/bin/node /var/www/app/process.js

Or:

#!/bin/bash
# Set PATH at top of script
PATH=/usr/local/bin:/usr/bin:/bin
node /var/www/app/process.js

Example 4: Relative Path Issues

❌ Before (doesn't work in cron):

#!/bin/bash
# Assumes running from /home/user/app/
source config.sh  # Can't find it
python3 main.py   # Can't find it

Error: "config.sh: No such file or directory"

✅ After (works):

#!/bin/bash
# Change to script directory first
cd /home/user/app || exit 1

# Now relative paths work
source config.sh
python3 main.py

Or use absolute paths:

#!/bin/bash
source /home/user/app/config.sh
/usr/bin/python3 /home/user/app/main.py

Advanced: SELinux and AppArmor Issues

On systems with security modules, you might have another layer of permission restrictions.

Check if SELinux is Enabled (Red Hat/CentOS/Fedora)

getenforce

If output is "Enforcing", SELinux might be blocking cron.

View SELinux denials:

sudo ausearch -m avc -ts recent

Fix SELinux context:

# Restore default context
sudo restorecon -v /path/to/script.sh

# Or allow cron to execute
sudo chcon -t user_cron_spool_t /path/to/script.sh

Check if AppArmor is Blocking (Ubuntu/Debian)

sudo aa-status

View AppArmor denials:

sudo dmesg | grep -i apparmor

Fix AppArmor profile:

# Put cron in complain mode temporarily
sudo aa-complain /usr/sbin/cron

# Or disable for cron
sudo ln -s /etc/apparmor.d/usr.sbin.cron /etc/apparmor.d/disable/
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.cron

Prevention: Write Cron-Friendly Scripts

Follow these best practices to avoid permission problems:

1. Always Use Absolute Paths

#!/bin/bash
# Good: absolute paths throughout
/usr/bin/mysqldump -u root mydb > /var/backups/backup.sql
/usr/bin/gzip /var/backups/backup.sql

2. Set PATH Explicitly

#!/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
export PATH

3. Change to Working Directory

#!/bin/bash
cd "$(dirname "$0")" || exit 1
# Now relative paths work relative to script location

4. Check for Required Commands

#!/bin/bash
# Fail early if command not found
command -v mysqldump >/dev/null 2>&1 || { echo "mysqldump not found"; exit 1; }

5. Create Log Files with Proper Permissions

#!/bin/bash
LOG_FILE="/var/log/myapp.log"

# Create log if it doesn't exist
touch "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/myapp.log"

# Now write to log
echo "Started at $(date)" >> "$LOG_FILE"

6. Handle Permission Errors Gracefully

#!/bin/bash
if [ ! -r /path/to/config ]; then
    echo "ERROR: Cannot read config file" >&2
    exit 1
fi

if [ ! -w /path/to/output ]; then
    echo "ERROR: Cannot write to output directory" >&2
    exit 1
fi

Quick Reference: Common Permission Fixes

| Problem | Command | Effect | |---------|---------|--------| | Script not executable | chmod +x script.sh | Makes script runnable | | Script only owner can execute | chmod 700 script.sh | Owner: rwx, Others: none | | Script anyone can execute | chmod 755 script.sh | Owner: rwx, Others: rx | | Wrong file owner | chown user:group script.sh | Changes ownership | | Directory not accessible | chmod 755 /path/to/dir/ | Makes directory traversable | | Config file readable | chmod 644 config.conf | Owner: rw, Others: r | | Log file writable | chmod 666 logfile.log | Everyone can write (use carefully) |

Conclusion: The Three Permission Pillars

When your cron job fails with "Permission Denied," check these three things in order:

1. File Permissions

  • Is the script executable? (chmod +x)
  • Does it have a shebang line? (#!/bin/bash)

2. User Permissions

  • Which user runs the cron job? (whoami test)
  • Does that user own the files or have access? (chown or chmod)
  • Can that user access the directories? (chmod 755 on directories)

3. PATH and Environment

  • Are you using absolute paths? (which command to find them)
  • Is the working directory correct? (cd /path || exit)
  • Are all commands available? (Set PATH in script)

Work through these systematically, and you'll solve 99% of cron permission errors.

Your Next Steps

  1. Test your script as the cron user: sudo -u username /path/to/script.sh
  2. Add logging to see what's happening: >> /tmp/cron.log 2>&1
  3. Check cron logs: grep CRON /var/log/syslog
  4. Fix permissions: chmod +x and verify ownership
  5. Use absolute paths: Replace all commands with full paths
  6. Test with frequent schedule: * * * * * to see quick results

Ready to build reliable cron schedules that actually work? Use our Cron Expression Generator to create and validate your automation with confidence.


Related Articles

Essential Troubleshooting:

Security & Best Practices:

Master Cron:


Keywords: cron permission denied, cron job not working, crontab permissions, cron file permissions, cron command not found, cron path issues, cron user permissions, chmod cron script