Skip to content
Happy Programming Guide
Start learning
Coding Tools

Bash Script Not Working in crontab

The script runs perfectly by hand and does nothing from cron. It is almost always the environment: a minimal PATH, no shell profile, a different working directory. How to see the error and fix each cause.

A ladybird resting on a green leaf

A script that works when you run it and fails under cron is failing because cron runs it in a different world: a nearly empty PATH, no environment variables from your profile, a home directory as the working folder, and no terminal. The script itself is fine. The fix is to stop assuming any of those things and, first, to capture the error you are currently not seeing.

Step 1: capture the output#

Cron mails output to the local user, which nobody reads. Redirect it somewhere you will:

Output
*/5 * * * * /home/you/bin/backup.sh >> /home/you/backup.log 2>&1

Wait for the next run, then read the log. Nearly every mystery becomes a specific error at this point — and the error is usually one of the following.

Terminal
grep CRON /var/log/syslog | tail        # Debian/Ubuntu: confirms cron ran the job at all
journalctl -u cron --since "10 min ago"  # systemd systems

Cause 1: PATH#

Output
backup.sh: line 3: aws: command not found
backup.sh: line 5: python3: command not found

Cron’s default PATH is typically /usr/bin:/bin. Anything in /usr/local/bin, ~/.local/bin, a version manager’s folder, or a virtual environment is invisible. Two fixes, and use both:

Output
# at the top of the crontab
PATH=/usr/local/bin:/usr/bin:/bin:/home/you/.local/bin
Terminal
#!/bin/bash
# in the script: full paths for anything not in /usr/bin
/home/you/project/.venv/bin/python /home/you/project/run.py

Absolute paths in the script are the robust answer, because they work regardless of who or what runs it.

Cause 2: the profile never ran#

Cron does not read ~/.bashrc, ~/.profile or ~/.zshrc. Every export in them — API keys, JAVA_HOME, nvm’s setup, conda’s activation — is absent. Options:

Output
# crontab: set what the script needs directly
API_KEY=abc123
0 2 * * * /home/you/bin/report.sh >> /home/you/report.log 2>&1
Terminal
# or, in the script, source a file that contains only the variables
set -a
. /home/you/.env
set +a

Sourcing your whole .bashrc from a cron job is possible but fragile; many profiles exit early when not interactive. A small dedicated env file is cleaner.

Cause 3: the working directory#

Cron starts the job in the user’s home directory, not the script’s folder. A script that does cat config.yml or ./helper.sh finds nothing.

Terminal
#!/bin/bash
cd "$(dirname "$0")" || exit 1        # work from the script's own folder

Or use absolute paths for every file. Or cd in the crontab line: cd /home/you/project && ./run.sh.

Cause 4: the shell#

Cron runs jobs with /bin/sh, which on Debian and Ubuntu is dash, not bash. A script with bash-only syntax — arrays, [[ ]], source — that is invoked as sh script.sh or lacks a shebang fails with syntax errors. Give the script a bash shebang and execute it directly, or set the shell in the crontab:

Output
SHELL=/bin/bash

Cause 5: permissions and ownership#

Terminal
chmod +x /home/you/bin/backup.sh
ls -l /home/you/bin/backup.sh

The script must be executable if called directly. And it runs as the user whose crontab it is in: a job in your crontab cannot write to root-owned locations, and a job in root’s crontab has a different home directory and PATH again. Check whose crontab you edited with crontab -l as that user.

Cause 6: percent signs and line endings#

In a crontab line, % is special: it means newline, and everything after the first one becomes standard input. A date format such as date +%Y-%m-%d in the crontab breaks silently. Escape them as \%, or move the command into the script where % is ordinary.

A script edited on Windows has carriage returns, and cron reports /bin/bash^M: bad interpreter. Run sed -i 's/\r$//' script.sh.

Cause 7: it needs a terminal or a display#

Anything that prompts for input hangs forever. Anything that opens a window fails with no display. Commands that check for a TTY may behave differently — some tools disable colour or progress bars, others refuse to run. Give tools non-interactive flags (-y, --batch, --no-input), and for SSH inside cron, use a key with no passphrase or an agent set up for the purpose.

A script that survives cron#

Terminal
#!/bin/bash
set -euo pipefail                          # stop on error, unset variable, or failed pipe
export PATH=/usr/local/bin:/usr/bin:/bin
cd "$(dirname "$0")"
exec >> "$HOME/backup.log" 2>&1          # log everything, from here on
echo "=== $(date) start"

/usr/bin/rsync -a /home/you/data/ /mnt/backup/data/
/home/you/.venv/bin/python notify.py

echo "=== $(date) done"

Absolute paths, an explicit PATH, an explicit working directory, and its own log. A script written like this runs the same under cron, from a terminal, and from systemd.

Questions people ask#

Should I use systemd timers instead?

On systemd systems they offer logging, dependencies and easier environment handling. Cron is simpler and universal. Either works once the script does not assume a login environment.

Why does the job run but only partly?

One command failed silently and the script carried on. set -e stops at the first failure so the log shows where.

Does cron run if I am logged out?

Yes, for user crontabs on a running machine. It does not run jobs that were due while the machine was off; anacron handles that for daily jobs.

My job needs a virtual environment activated.

Do not activate; call the environment’s interpreter by full path, as in the example. Activation only edits PATH, and the full path makes it unnecessary.

Where to go next#

Error handling in bash scriptsRead next

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

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