Linux (AlmaLinux)

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
  1. 01Before you start
  2. 02See the difference for yourself
  3. 03Where a job lives changes its PATH
  4. 04A failure that looks like something else
  5. 05Fix it in two places
  6. 06Test the way cron runs it
  7. 07Where the output goes
  8. 08Or use a systemd timer
  9. 09Sources

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 cronie and sudo 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:

Shell · your user
echo "$PATH"

For root on a fresh AlmaLinux 9 system it is:

Output

/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin

A 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.

  1. Open your crontab

    Shell · your user
    crontab -e
  2. Add this line, then save and quit

    crontab
    * * * * * env > /var/tmp/cron-env.txt
  3. Wait a minute, then read the result

    Shell · your user
    grep -E '^(PATH|SHELL|HOME|LOGNAME)=' /var/tmp/cron-env.txt

    Output

    SHELL=/bin/sh
    LOGNAME=root
    HOME=/root
    PATH=/usr/bin:/bin
  4. Remove the test line

    Shell · your user
    crontab -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/: also PATH=/usr/bin:/bin.
  • /etc/crontab: that file starts with its own PATH line, 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:

/usr/local/bin/check-nginx.sh
#!/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"
fi

Run 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 BROKEN

Three things went wrong at once:

  1. nginx was not found, because /usr/sbin is not in cron's PATH.
  2. The script reported the configuration as broken. It is not. The test never ran.
  3. 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.

  1. Set PATH at the top of your crontab

    Open it with crontab -e and add a PATH line above the jobs:

    crontab
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
    */15 * * * * /usr/local/bin/check-nginx.sh >> /var/tmp/check-nginx.log 2>&1

    Files in /etc/cron.d/ accept the same PATH= line at the top.

  2. 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
    fi

    The command -v check turns a missing command into a clear error and a non-zero exit status, instead of a wrong answer. The exit 1 after config BROKEN makes 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:

Shell · your user
env -i /bin/bash -c 'echo "PATH=$PATH"; command -v nginx || echo "nginx not found"'

Output

PATH=/usr/local/bin:/usr/bin
nginx not found

That 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:

Shell · your user
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=0

With 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=0

For 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.

Sources