Troubleshooting

Exit code 127: 'command not found' in scripts, cron, sudo and Docker

127 means the shell could not find the command. Why it happens in cron, sudo, systemd, npm scripts and Docker, how 126 differs, and a PATH checklist.

Published ~10 min read

Exit code 127 means the shell could not find the command it was asked to run. Either the program is not installed, the name is misspelled, or it is not on the PATH of the environment that runs it. That last case is why something can work in your terminal and fail with 127 in cron, under sudo, in a systemd service, an npm script or a Docker container. Exit code 126 is the sibling: the command was found but could not be executed.

The fix is to install the program, or to call it by its absolute path, or to set PATH in the environment where it fails. Start by asking that environment what it sees:

quick check
command -v mytool          # path the shell would run, or nothing at all
echo "$PATH"               # where this environment looks
ls -l /usr/local/bin/mytool

# fix: call it by its full path, or extend PATH where the job runs
/usr/local/bin/mytool --version
export PATH="/usr/local/bin:$PATH"

What do exit codes 127, 126 and 137 mean?

Every command returns a number between 0 and 255 when it ends, and the shell keeps it in $?. The values that matter here:

  • 0 — success.
  • 1 — a general error. The program ran and reported a failure.
  • 2 — misuse: a syntax error in the shell, or bad arguments for many tools (ls and grep use 2 for errors).
  • 126 — found, but could not be executed: no execute permission, a directory, or a binary built for another CPU architecture.
  • 127 — not found: not installed, not on PATH, or the path you typed does not exist. Also used when a program’s shared library is missing, and by bash 5.2 when a script’s interpreter is (bash 5.1 and 5.3 report that as 126).
  • 128 + n — killed by signal n. 130 is Ctrl+C (SIGINT, 2), 137 is SIGKILL (9), often the out-of-memory killer, and 143 is SIGTERM (15), a normal stop request.
reading the exit code
nosuchcmd
echo $?            # 127

# in a script, save it before the next command overwrites it
mytool --check
rc=$?
echo "mytool exited with $rc"

Exit codes only go up to 255 and wrap around after that: a script that ends with exit 300 reports 44. A program can also return 127 or 126 on purpose, but when the shell prints “not found” or “Permission denied” next to it, the meaning above applies.

Why does every tool word it differently?

The code is the same everywhere; the message depends on who ran the command. These all mean exit code 127 (the bash, dash, make and Python lines were checked on Debian 12 with bash 5.2):

what 127 looks like
bash: nosuchcmd: command not found                      # bash (-bash: at a login prompt)
sh: 1: nosuchcmd: not found                             # dash, /bin/sh on Debian and Ubuntu
zsh: command not found: nosuchcmd                       # zsh, the default shell on macOS
sh: nosuchcmd: not found                                # BusyBox sh, e.g. Alpine
Command 'tree' not found, but can be installed with:    # Ubuntu's suggestion helper
exec: "nosuchcmd": executable file not found in $PATH   # Docker, at container start
returned non-zero exit status 127.                      # Python subprocess with shell=True
make: *** [Makefile:2: all] Error 127                   # make (make itself exits 2)
Error: Process completed with exit code 127.            # GitHub Actions

One trap in Python: subprocess.run(["nosuchcmd"]) without a shell never produces 127. Python tries to start the program itself and raises FileNotFoundError instead.

Is the program installed at all?

Check before you debug PATH. If command -v finds nothing and the file is nowhere on disk, install it. On a fresh server or a slim container image, missing tools are the normal state, not a fault. Each of these has its own guide:

If you do not know which package provides a command, apt-file search bin/mytool on Debian and Ubuntu or dnf provides '*/bin/mytool' on Rocky and AlmaLinux will tell you. And if apt answers that it cannot find the package, see Unable to locate package.

Why does it work in my terminal but fail in cron?

Because cron gives your job almost no environment. It runs the line with /bin/sh, sets HOME and LOGNAME, and uses a minimal PATH — /usr/bin:/bin in the cron daemons of Debian, Ubuntu and the RHEL family. It reads none of your startup files. Anything in /usr/local/bin, ~/.local/bin or a version manager such as nvm or pyenv is therefore “not found”. Debian’s /etc/crontab sets a longer PATH, but only for the system crontab, not for the one you edit with crontab -e.

crontab -e
# 1. see the environment cron really gives your jobs (remove the line afterwards)
* * * * * env > /tmp/cron-env.txt 2>&1

# 2. fix: a full PATH at the top of the crontab, written out
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * backup.sh >> /var/log/backup.log 2>&1

# 3. or absolute paths, found with command -v in your normal shell
*/5 * * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Why does it fail under sudo?

sudo does not keep your PATH. It replaces it with secure_path from /etc/sudoers. On Debian that is /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, and Ubuntu adds /snap/bin. Rocky, AlmaLinux and RHEL 9 use /sbin:/bin:/usr/sbin:/usr/bin, without /usr/local/bin. Anything installed in your home directory is invisible to sudo everywhere.

sudo
sudo sh -c 'echo $PATH'                 # the PATH sudo really uses

# resolve the program as yourself, run it as root
sudo "$(command -v mytool)" --args

If the message is sudo: command not found, sudo itself is missing — see sudo: command not found. For Node tools installed with nvm, the npx guide’s sudo section explains why installing system-wide is the better fix.

Why does it fail in a systemd service?

systemd does not read shell profiles either. Services get a fixed default PATH with no home directories in it. Two details trip people up:

  • ExecStart is not looked up in PATH. A bare name is resolved against a fixed list of system directories compiled into systemd, so use an absolute path. If that program is missing, systemd reports status=203/EXEC, not 127.
  • 127 comes from inside. When ExecStart runs a script and a command in that script cannot be found, the service fails with 127. That is the case Environment=PATH= fixes.
/etc/systemd/system/myapp.service
[Service]
# the program by its absolute path, not just its name
ExecStart=/opt/app/venv/bin/python /opt/app/worker.py
# what scripts and child processes of the service will search
Environment=PATH=/opt/app/venv/bin:/usr/local/bin:/usr/bin:/bin
apply and check
sudo systemctl daemon-reload
sudo systemctl restart myapp
journalctl -u myapp -n 20

Runtimes installed per user — nvm, pyenv, a conda in ~/miniconda3 — are a common source of 127 here. Point ExecStart at the interpreter inside them by full path, or install the runtime system-wide.

Why does an npm script fail with “sh: 1: vite: not found”?

npm runs package.json scripts with sh, which is dash on Debian and Ubuntu, and puts the project’s node_modules/.bin on the PATH. So sh: 1: vite: not found rarely means a PATH problem. It means the dependency was never installed — typically because the install skipped devDependencies on a server or in CI. The vite guide’s server and CI section has the fix, and it applies to every build tool installed as a devDependency. When the command that is not found is yarn or pnpm itself, the package manager was never set up — see yarn: command not found.

Why does a Docker container exit with code 127?

Docker documents three codes of its own for docker run: 125 when Docker itself failed, 126 when the container’s command could not be invoked, and 127 when it could not be found. Any other value is the exit code of your program. The usual reasons behind a 127 or a “not found” when a container starts:

  • The program is not in the image. Slim, Alpine and distroless images leave out most tools. Alpine has sh but no bash, so CMD ["bash", "start.sh"] fails there.
  • The exec form is one string. CMD ["npm start"] asks Docker to run a program literally called npm start. Split it: CMD ["npm", "start"].
  • The entrypoint script has Windows line endings. The shebang then names /bin/sh followed by an invisible carriage return, an interpreter that does not exist. The start fails with “no such file or directory” although the script is right there. Convert it with sed -i 's/\r$//' entrypoint.sh, and add *.sh text eol=lf to .gitattributes so it does not come back.
  • The script calls something missing. Then the 127 comes from the script, and the container log shows the shell’s “not found” line.
look inside the image
# what does the container see? (needs a shell in the image)
docker run --rm --entrypoint sh myimage -c 'echo $PATH; command -v myapp'

# why did a stopped container exit?
docker inspect --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}' mycontainer

Set PATH in the image with ENV PATH="/opt/app/bin:$PATH" rather than in a shell startup file, which the container never reads. A 137 with OOMKilled true is a different problem: the container hit its memory limit.

How do I check and fix PATH?

any shell
echo "$PATH" | tr ':' '\n'          # one directory per line
command -v mytool                    # what the shell would run, or nothing
type -a mytool                       # every match, aliases and functions included
hash -r                              # forget cached lookups (zsh: rehash)
export PATH="$HOME/.local/bin:$PATH" # this shell only

Where a permanent PATH change belongs depends on which shell reads which file:

  • ~/.profile — read by bash login shells: an SSH login, su -. If ~/.bash_profile exists, bash reads that instead. Debian’s and Ubuntu’s default ~/.profile already adds ~/bin and ~/.local/bin when they exist — but only at login, so log out and in again after creating them.
  • ~/.bashrc — interactive bash shells that are not login shells, such as a new terminal window on a desktop. The default Debian and Ubuntu .bashrc stops at once when the shell is not interactive.
  • ~/.zshrc — interactive zsh. ~/.zprofile is read by login shells and ~/.zshenv by every zsh, scripts included.
  • Scripts, cron, systemd and Docker read none of these. Set PATH explicitly there, as in the sections above.

Why is it 127 or 126 when the file exists?

  • cannot execute: required file not found (bash 5.2, as on Debian 12 and 13 and Ubuntu 24.04), exit 127, or bad interpreter: No such file or directory (bash 5.1 and 5.3, as on Ubuntu 22.04 and Rocky 9), exit 126: the interpreter named in the script’s first line does not exist, or the line ends in a Windows carriage return.
  • error while loading shared libraries, exit 127: the program was found, but a library it needs was not — how to fix a missing shared library.
  • Permission denied, exit 126: no execute bit, or a filesystem mounted noexec — see bash: Permission denied.
  • cannot execute binary file: Exec format error, exit 126: a binary for another CPU architecture, such as an ARM build on an x86 server.

PATH problems are easiest to pin down on a machine with nothing on it but the stock image — a Standard KVM VPS is ready in about a minute and shows you exactly what a fresh install does and does not have.

The message itself — for any tool, in bash, zsh and fish — and the permanent PATH fix: command not found: the three causes and the fix for every tool.

Frequently asked

What does exit code 127 mean?

The shell could not find the command: it is not installed, the name is misspelled, or it is not on the PATH of the environment that ran it. The same code appears when a program's shared library is missing, and with bash 5.2 when a script's interpreter is missing (bash 5.1 and 5.3 return 126 for that).

What is the difference between exit code 126 and 127?

127 means the command was not found. 126 means it was found but could not be executed — usually a missing execute permission, a directory, a filesystem mounted noexec, or a binary built for another CPU architecture.

Why does my cron job fail with exit code 127?

cron runs jobs with a minimal PATH, usually /usr/bin:/bin, and reads none of your shell startup files. Use absolute paths in the crontab or put a full PATH= line at the top of it. cron does not expand variables there, so write the directories out.

How do I fix exit code 127 in Docker?

docker run returns 127 when the container's command cannot be found. Check that the program exists in the image (slim and Alpine images lack bash and many tools), write exec-form commands with separate arguments such as ["npm", "start"], and convert entrypoint scripts to LF line endings.

What does exit code 137 mean?

128 + 9: the process was killed with SIGKILL. Most often the kernel's out-of-memory killer or a container memory limit ended it, and docker inspect shows OOMKilled true for a container that hit its limit. It is not a missing-command problem.

Related articles

All troubleshooting