Why a script works in your shell but fails from cron
Cron runs jobs with a much shorter PATH than your login shell, so commands in /usr/sbin disappear. How to see it, fix it in two places, and test the way cron really runs your job.
Checked against the official documentation on
On this page9 sections
This guide is for anyone on AlmaLinux 9 whose script runs fine when they type it, but fails, or quietly does the wrong thing, when cron runs it. At the end you will know exactly which environment cron gives a job, how to make a script independent of it, and how to test a job under the same conditions before you trust it.
The usual cause is PATH. Your login shell searches several directories for commands, including /usr/sbin, where many system tools live. Cron gives a job a much shorter list. A script that calls nginx, ss, ip or any other tool from /usr/sbin works in your terminal and cannot find it from cron.
The failure is not always loud. If the script tests a command's result, "command not found" can look like a real problem with the thing being tested, and the script can still exit with success.
Checked on
AlmaLinux 9.8, cronie 1.5, bash 5.1, systemd 252. Every environment value below was read from a real cron job, not taken from memory.
Before you start
You need:
- AlmaLinux 9 with a user that can run
sudo. - cron installed and running. If it is not:
sudo dnf install cronieandsudo systemctl enable --now crond.
The example script tests an nginx configuration, because nginx lives in /usr/sbin. The same problem applies to any command outside /usr/bin and /bin.
See the difference for yourself
First, look at PATH in your login shell:
echo "$PATH"For root on a fresh AlmaLinux 9 system it is:
Output
/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/binA normal user's login shell also includes /usr/sbin.
Now ask cron. Add a job that writes its whole environment to a file. The output below comes from root's crontab (run sudo -i first to match it); a normal user sees their own HOME and LOGNAME.
Open your crontab
Shell · your usercrontab -eAdd this line, then save and quit
crontab* * * * * env > /var/tmp/cron-env.txtWait a minute, then read the result
Shell · your usergrep -E '^(PATH|SHELL|HOME|LOGNAME)=' /var/tmp/cron-env.txtOutput
SHELL=/bin/sh LOGNAME=root HOME=/root PATH=/usr/bin:/binRemove the test line
Shell · your usercrontab -e
Cron gave the job PATH=/usr/bin:/bin. That leaves out /usr/sbin, /sbin and even /usr/local/bin. It also runs the command with /bin/sh, not bash.
The crontab(5) manual lists SHELL, LOGNAME and HOME as the variables cron sets automatically. It does not state a value for PATH, and that value depends on how cron was built. That is why this guide reads it from a real job instead of quoting a number: on a different system, run the same test.
Where a job lives changes its PATH
AlmaLinux has three common places for cron jobs, and they do not behave the same:
- Your crontab (
crontab -e):PATH=/usr/bin:/bin. - A file in
/etc/cron.d/: alsoPATH=/usr/bin:/bin. /etc/crontab: that file starts with its ownPATHline,PATH=/sbin:/bin:/usr/sbin:/usr/bin, so jobs written in it do find/usr/sbin.
The variables at the top of /etc/crontab apply only to the jobs in that file. They do not reach your crontab or the files in /etc/cron.d/. The same script can therefore work from /etc/crontab and fail from /etc/cron.d/, on the same machine, at the same minute. The results in this guide were observed with all three running side by side.
A failure that looks like something else
Here is a script that looks reasonable:
#!/bin/bash
# Test the nginx configuration and record the result.
if nginx -t; then
echo "$(date '+%F %T') config OK"
else
echo "$(date '+%F %T') config BROKEN"
fiRun by hand, it reports config OK. Run from a crontab or /etc/cron.d/ with its output sent to a log, it writes this:
Output
/usr/local/bin/check-nginx.sh: line 3: nginx: command not found
2026-09-16 10:08:01 config BROKENThree things went wrong at once:
nginxwas not found, because/usr/sbinis not in cron'sPATH.- The script reported the configuration as broken. It is not. The test never ran.
- The script still exited with status 0, so anything that only watches the exit status sees success.
If you only read the last line of that log, you would go looking for a configuration problem that does not exist.
Fix it in two places
Fix the environment where the job is defined, and make the script safe on its own. Either fix alone works for that job. Both together mean the script keeps working when someone later runs it from a different place.
Set PATH at the top of your crontab
Open it with
crontab -eand add aPATHline above the jobs:crontabPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin */15 * * * * /usr/local/bin/check-nginx.sh >> /var/tmp/check-nginx.log 2>&1Files in
/etc/cron.d/accept the samePATH=line at the top.Set PATH inside the script, and check the command exists
/usr/local/bin/check-nginx.sh#!/bin/bash # Test the nginx configuration and record the result. # cron runs jobs with PATH=/usr/bin:/bin, which does not include /usr/sbin. export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin if ! command -v nginx >/dev/null; then echo "$(date '+%F %T') ERROR: nginx not found in PATH=$PATH" >&2 exit 1 fi if nginx -t; then echo "$(date '+%F %T') config OK" else echo "$(date '+%F %T') config BROKEN" exit 1 fiThe
command -vcheck turns a missing command into a clear error and a non-zero exit status, instead of a wrong answer. Theexit 1afterconfig BROKENmakes a real failure visible to anything that checks the exit status.
Note
Using full paths such as /usr/sbin/nginx also works, but every command in the script then needs one, and a single missed command brings the problem back. Setting PATH once at the top is easier to get right.
Test the way cron runs it
The only test that counts is one that uses cron's environment. Running the script in your terminal proves nothing, because your terminal already has the longer PATH.
A common tip is to test with env -i, which starts a command with an empty environment. That is not the same as cron. With no PATH at all, bash falls back to its own built-in default:
env -i /bin/bash -c 'echo "PATH=$PATH"; command -v nginx || echo "nginx not found"'Output
PATH=/usr/local/bin:/usr/bin
nginx not foundThat value is bash's, not cron's. The bash manual says "the default path is system-dependent, and is set by the administrator who installs bash." It differs from what cron gave the job (/usr/bin:/bin). A script that calls something in /usr/local/bin would pass this test and still fail from cron.
Instead, give the command the exact variables you read from your cron job earlier:
sudo env -i PATH=/usr/bin:/bin SHELL=/bin/sh HOME=/root LOGNAME=root /bin/sh -c '/usr/local/bin/check-nginx.sh; echo "exit=$?"'With the original script, this reproduces the failure:
Output
/usr/local/bin/check-nginx.sh: line 3: nginx: command not found
2026-09-16 10:08:22 config BROKEN
exit=0With the fixed script, it passes:
Output
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
2026-09-16 10:08:22 config OK
exit=0For a job that runs as another user, use that user's HOME and LOGNAME, and run the command as that user.
Where the output goes
The cron manual says that any output from a job "is mailed to the owner of the crontab", or to the address in a MAILTO line. It also says that "the syslog output will be used instead of mail, when sendmail is not installed". In that case the output goes to the system log, which on AlmaLinux 9 you read with journalctl, mixed in with everything else.
Either way, the error is easy to miss. For a job you care about, send its output to its own log file, as the examples above do with >> /var/tmp/check-nginx.log 2>&1. The 2>&1 matters: "command not found" is written to standard error, and without it that message does not reach the file.
Or use a systemd timer
Services started by systemd do not use cron's environment. The systemd.exec manual says systemd uses a fixed PATH of /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin in the system manager, which includes /usr/sbin. A systemd timer and service pair is a reasonable alternative to a cron job, and its output goes to the journal. The script fixes above are still worth keeping: a script that sets its own PATH works no matter what starts it.