cron Cheatsheet
Environment and PATH
Use this cron reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Cron's PATH is not your PATH
This is the number one reason a command that works in your shell fails under
cron. Cron does not read .bashrc, .bash_profile, .profile, /etc/profile,
or anything else. It sets a minimal environment and runs the command.
| Variable | Value under cron |
|---|---|
PATH | /usr/bin:/bin, the compiled-in default in Vixie cron and cronie |
SHELL | /bin/sh, documented as /usr/bin/sh on recent Debian |
HOME | from /etc/passwd |
LOGNAME | from /etc/passwd, cannot be overridden |
USER | same as LOGNAME on BSD-derived crons |
MAILTO | unset, so mail goes to the crontab owner |
| everything else | not set |
So none of these exist under cron: $PATH entries from /usr/local/bin,
nvm, pyenv, rbenv, cargo, go, Homebrew's /opt/homebrew/bin,
$AWS_PROFILE, $DISPLAY, $SSH_AUTH_SOCK, $LANG, or any variable you set in
a dotfile.
# Fails: node is in ~/.nvm/versions/..., which cron's PATH does not include 0 3 * * * node /srv/app/task.js # Works: absolute path 0 3 * * * /home/deploy/.nvm/versions/node/v22.11.0/bin/node /srv/app/task.js
Prove it in thirty seconds
Before debugging anything else, capture the actual environment your job sees.
* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1
Wait one minute, then compare.
diff <(sort /tmp/cron-env.txt) <(env | sort)
The diff is the complete list of things your job cannot rely on. Do the same for the working directory and the user:
* * * * * { id; pwd; echo "PATH=$PATH"; } >> /tmp/cron-probe.log 2>&1The working directory is $HOME, not the directory the script lives in.
The three ways to fix PATH
# 1. Set PATH at the top of the crontab. Explicit, and it applies to every job. PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 0 3 * * * backup.sh # 2. Absolute paths everywhere. Verbose, but immune to PATH changes. 0 3 * * * /usr/local/bin/backup.sh # 3. Set PATH inside the script, which keeps the crontab line short. #!/bin/bash export PATH=/usr/local/bin:/usr/bin:/bin
Note that PATH=$PATH:/opt/bin in a crontab does not work. Crontab
environment lines are literal, with no variable substitution and no ~
expansion, so that sets PATH to the string $PATH:/opt/bin. Debian's
crontab(5) documents this explicitly, along with the fact that C=$A $B does
not reference A and B either. Write the full path list.
/etc/crontab and files in /etc/cron.d are not exempt, they just usually ship
a PATH line already. Debian's is:
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
SHELL, and why bash -lc is a trap
Cron runs the command with /bin/sh -c, and /bin/sh is dash on Debian and
Ubuntu, not bash. Anything bash-specific breaks: [[ ]], arrays, source,
local in some forms, brace expansion, $'...', and pipefail.
SHELL=/bin/bash 0 3 * * * [[ -f /tmp/flag ]] && /usr/local/bin/job.sh
The popular shortcut is to source a login shell:
0 3 * * * /bin/bash -lc 'nvm use 22 && node /srv/app/task.js'
That does work, and it is fragile in a specific way. It makes the job's behavior
depend on the contents of a dotfile, so an unrelated edit to .bash_profile
changes what the scheduled job does, and an interactive-only line such as a
prompt or a tput call can hang or fail. It also means the job no longer runs
reproducibly under a different user. Prefer setting the two or three variables
the job actually needs, in the script itself.
For a version manager specifically, resolve the interpreter path once and hard code it, or use the manager's own shim directory:
0 3 * * * /home/deploy/.pyenv/shims/python /srv/app/task.py
Every path in the job must be absolute
The working directory is $HOME, so relative paths resolve somewhere you did not
intend, and redirections create files there.
# Broken: relative input, relative output, relative script 0 3 * * * ./backup.sh data/ >> log.txt 2>&1 # Correct 0 3 * * * cd /srv/app && /srv/app/backup.sh /srv/app/data/ \ >> /var/log/backup.log 2>&1
The cd matters for anything that reads a config file relative to the current
directory, which includes docker compose, npm, make, terraform, and most
language toolchains.
A script template that survives cron
Write the schedule as one crontab line calling one script, and put the environment in the script where it is version controlled and testable.
#!/bin/bash set -euo pipefail export PATH=/usr/local/bin:/usr/bin:/bin export HOME=/home/deploy export TZ=UTC export LANG=C.UTF-8 cd /srv/app exec >> /var/log/app-nightly.log 2>&1 echo "=== $(date -Is) starting ===" ./manage.py cleanup --older-than 30d echo "=== $(date -Is) done ==="
0 3 * * * /srv/app/bin/nightly.sh
Then test it the way cron will run it, with an empty environment:
env -i /bin/sh -c /srv/app/bin/nightly.sh
That single command catches nearly every environment bug before it becomes a
missed backup, because it reproduces exactly what cron does: no profile, no
inherited variables, and /bin/sh as the shell.