You’ve written a script. You test it manually: ./my-script.sh — works perfectly. You add it to cron, wait for the scheduled time… nothing happens. No output, no errors, just silence.

This is one of the most frustrating debugging experiences in Linux. Let’s fix it.

The Core Problem: Cron Runs in a Different Environment

When you run a script manually, you’re running it in your interactive shell with:

  • Your PATH variable (with all your custom directories)
  • Your environment variables (loaded from .bashrc, .profile, etc.)
  • Your current working directory
  • Your user’s full shell configuration

Cron runs scripts in a minimal environment with almost none of this. That’s the root cause of nearly every “works manually, fails in cron” issue.

Cause 1: PATH Doesn’t Include Your Commands

The most common culprit. Your script calls aws, docker, python3, or some other command that lives in /usr/local/bin or another directory that’s in your interactive PATH but not in cron’s PATH.

Cron’s default PATH is typically: /usr/bin:/bin

Diagnosis:

1
2
3
4
5
6
7
# Check where your command lives
which aws
# Output: /usr/local/bin/aws

# Check cron's PATH
crontab -l
# If no PATH= line, it's using the minimal default

Fix — Option 1: Use absolute paths in your script

1
2
3
#!/bin/bash
/usr/local/bin/aws s3 sync /data s3://my-bucket/
/usr/bin/python3 /home/user/scripts/process.py

Fix — Option 2: Set PATH in crontab

1
2
3
PATH=/usr/local/bin:/usr/bin:/bin

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

Fix — Option 3: Set PATH in the script itself

1
2
3
4
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

aws s3 sync /data s3://my-bucket/

Cause 2: Missing Environment Variables

Your script relies on AWS_PROFILE, DATABASE_URL, API_KEY, or other environment variables that are set in your .bashrc but don’t exist in cron’s environment.

Diagnosis:

1
2
3
4
5
6
# See what environment cron actually has
# Add this to crontab temporarily:
* * * * * env > /tmp/cron-env.txt

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

You’ll likely see a very sparse environment compared to your interactive shell.

Fix — Source your environment in the script:

1
2
3
4
5
6
#!/bin/bash
source /home/user/.bashrc
# or load specific vars
source /home/user/.env

# Now your script runs

Fix — Set variables in crontab:

1
2
3
4
AWS_PROFILE=production
DATABASE_URL=postgres://localhost/mydb

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

Cause 3: Working Directory Assumptions

Your script assumes it’s running from a specific directory. Maybe it reads ./config.yaml or writes to ./output/.

Cron runs from your home directory by default — not from where the script lives.

Diagnosis:

1
2
# Add this to your script temporarily
pwd > /tmp/cron-pwd.txt

Fix — cd to the script’s directory:

1
2
3
4
5
6
#!/bin/bash
cd "$(dirname "$0")" || exit 1

# Now relative paths work
./helper.sh
cat ./config.yaml

Cause 4: No Display / TTY

Scripts that use GUI tools, open browsers, or expect user input will fail in cron. There’s no terminal attached.

Common offenders:

  • open or xdg-open commands
  • Anything that prompts for input
  • GUI applications
  • notify-send without proper DBUS setup

Fix: Make your script fully non-interactive. Use flags like --yes, --quiet, or --non-interactive where available.

Cause 5: Output Goes Nowhere

Cron emails output to the system mail (if configured). If mail isn’t set up, output vanishes. Including errors.

Fix — Redirect output to a log file:

1
0 * * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1

The 2>&1 captures both stdout and stderr.

Fix — Add MAILTO in crontab:

1
2
3
MAILTO=you@example.com

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

Cause 6: Permission Issues

The cron daemon runs your job, but maybe the script isn’t executable, or it tries to write somewhere the cron user can’t access.

Fix:

1
2
3
4
5
chmod +x /home/user/scripts/backup.sh

# Check the script's shebang line
head -1 /home/user/scripts/backup.sh
# Should be: #!/bin/bash (not #!/usr/bin/env bash if env isn't in PATH)

The Nuclear Option: Debug Wrapper

When you can’t figure it out, wrap your script to capture everything:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash
# debug-wrapper.sh

exec > /tmp/cron-debug.log 2>&1
set -x  # Print every command

echo "=== Debug info ==="
echo "Date: $(date)"
echo "User: $(whoami)"
echo "PWD: $(pwd)"
echo "PATH: $PATH"
echo "=== Environment ==="
env
echo "=== Running script ==="

# Your actual script here
/home/user/scripts/backup.sh

Run this wrapper from cron instead. You’ll get complete visibility into what’s happening.

Quick Checklist

When your cron job fails silently:

  1. ☐ Add >> /tmp/myjob.log 2>&1 to capture output
  2. ☐ Use absolute paths for all commands
  3. ☐ Set PATH at the top of your script or crontab
  4. ☐ Source required environment variables
  5. cd to the script’s directory if using relative paths
  6. ☐ Check the script is executable (chmod +x)
  7. ☐ Check /var/log/syslog or /var/log/cron for cron errors
  8. ☐ Verify the crontab syntax with crontab.guru

The pattern that never fails: make your script self-contained. Set its own PATH, cd to its own directory, source its own environment. Assume nothing from the outside world.