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:
- File permissions (the script isn't executable)
- User permissions (wrong user running the cron job)
- 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 -
.bashrcand.bash_profileloaded - ✅ Current directory - You're in a specific location
Example: When you run ./backup.sh from /home/user/scripts/, your shell:
- Uses your user permissions
- Knows where to find commands (via
$PATH) - Can access files you own
- 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 -
.bashrcis NOT loaded - ⚠️ Different working directory - Often starts in
/or the user's home
Example: When cron runs ./backup.sh:
- It might be running as a different user
- It can't find commands not in the minimal PATH
- It can't access files owned by other users
- 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? (
whoamitest) - Does that user own the files or have access? (
chownorchmod) - Can that user access the directories? (
chmod 755on directories)
3. PATH and Environment
- Are you using absolute paths? (
which commandto find them) - Is the working directory correct? (
cd /path || exit) - Are all commands available? (Set
PATHin script)
Work through these systematically, and you'll solve 99% of cron permission errors.
Your Next Steps
- Test your script as the cron user:
sudo -u username /path/to/script.sh - Add logging to see what's happening:
>> /tmp/cron.log 2>&1 - Check cron logs:
grep CRON /var/log/syslog - Fix permissions:
chmod +xand verify ownership - Use absolute paths: Replace all commands with full paths
- 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:
- Why Your Cron Job Isn't Running - Complete debugging guide
- Environment Variables in Cron Jobs - PATH and env issues
- Where Did My Cron Job Output Go? - Logging guide
Security & Best Practices:
- Secure Your Cron Jobs from Attacks - Security hardening
- Organize Your Crontab Like a Pro - Maintainability
- Automate Database Backups with Cron - Production examples
Master Cron:
- The Ultimate Guide to Cron Jobs - Beginner to advanced
- Cron Operators Explained: *, /, -, and , - Syntax guide
- 10 Essential Cron Jobs for Web Developers - Practical tasks
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