127 is the exit code, and the exit code is the clue
The Bash Exit Code Lookup explains 127 next to 126, its close cousin. If the problem is PATH itself (missing directories, duplicates, a typo in .bashrc), paste it into the Bash $PATH Debugger.
command not found is one message for six different problems. The program might not exist, might exist somewhere PATH does not cover, might be a script you ran without ./, might be an alias your script cannot see, or might be a file bash does find but cannot start. The message never says which, so the usual fix (install it again) works for one cause and wastes ten minutes on the other five.
What Does "command not found" Actually Mean?
Reproduced here on 2026-10-03 (Kali, bash 5.3.15, ShellCheck 0.11.0) in a scratch directory. Paths under the home directory are shown as ~.
A typo and a program installed outside PATH print exactly the same thing:
mytool exists. bash only looks in the directories listed in $PATH, in order, and stops at the first executable match. A file anywhere else is invisible to it, including the current directory:
Aliases live only in interactive shells. A script, bash -c, cron or ssh host cmd never sees them:
Why Does It Say "No such file or directory" for a Command That Exists?
bash caches the path of every command it has run. Move the program and the cache still points at the old location. Run as a script here, so bash prefixes the error with the script name instead of bash::
Same exit code, different message, and newbin was on PATH the whole time. This is what happens after a package moves a binary from /usr/local/bin to /usr/bin during an upgrade, or after you uninstall a pip copy and install the distro one. hash -r (or a new terminal) clears it.
A file can also be found and still fail with 127, when its shebang names an interpreter that does not exist. Windows CRLF line endings do that, because the interpreter's name ends in a carriage return:
The second form, $'ls\r': command not found, names a command that ends in \r. The full story is on the CRLF bad interpreter page.
Why Does It Work in the Terminal but Fail in Cron?
cron runs jobs with PATH=/usr/bin:/bin and never reads ~/.bashrc. Anything installed per-user or under /usr/local disappears. env -i reproduces cron's environment without waiting for the schedule:
yt-dlp lives in ~/.local/bin on this box, which is on the interactive PATH and not on cron's. The cron survival guide covers the rest of what cron strips out, and Bash Environment Variables explains why exported variables do not reach it either.
sudo has the same problem from the other direction: it replaces your PATH with secure_path from /etc/sudoers. Compare echo "$PATH" with sudo sh -c 'echo "$PATH"' on your machine. A tool found by one and not the other is the answer.
The 60-Second Diagnosis
The Script
Save as why-command-not-found.sh. Give it the command name and it names the cause: typo of an installed command, file in the current directory, installed outside PATH, CRLF shebang, or missing from cron's PATH.
Prerequisites
bash 4 or later and coreutils. No root, no packages. It reads files and PATH; it changes nothing.
How Does Each Check Work?
type -PsearchesPATHfor a file only, ignoring aliases and functions. That is deliberate: the script sees what any other script would see, so an alias that only works in your terminal shows up as "not on PATH" here.- The CRLF check reads the first line of the resolved file and looks for
\r. A found-but-broken shebang is the one case where "the file is right there" and the command still fails. PATH="$CRON_PATH" type -Prepeats the lookup with cron'sPATH. A hit in your shell and a miss here is the whole "works in terminal, fails in cron" bug../$NAMEcatches the script you wrote in the current directory. bash does not search.unlessPATHcontains it, and it should not.EXTRA_DIRSand/opt/*/binare where installers put binaries without touchingPATH:pip install --user,cargo install,go install, snap, vendor bundles, and/usr/sbinfor a user whosePATHomits it.- The swap loop tries every adjacent-letter swap of the name (
gti→tgi,git). Transposed letters are the most common typo, and onetype -Pper swap is cheap.
What Does the Script Print?
The same scratch directory, with PATH set to ~/demo/bin:~/.local/bin:/usr/local/bin:/usr/bin:/bin:
It exits 1 for every cause it finds and 0 when the command resolves, so it can gate a deploy script: ./why-command-not-found.sh rsync || exit 1. In a crontab, write the expanded path (/home/you/.local/bin/yt-dlp) rather than ~.
When Is It Not "command not found" at All?
Exit 126 means bash found the file and could not run it: a missing execute bit, a directory without search permission, or a noexec mount. That is a different problem with different fixes, covered on Fix "Permission denied" in Bash. An empty variable used as a command ($EDITOR file with EDITOR unset) runs file as the command instead; ShellCheck's SC2086 deep dive and the ShellCheck Error Decoder catch that class before it runs.
Starting every script with set -euo pipefail makes a 127 stop the script on the line that caused it instead of three lines later. The safe bash script template explains each flag, and The Production Bash Toolkit ships that template with ShellCheck-clean helpers.
Frequently Asked Questions
What does exit code 127 mean in bash?
127 means the command could not be found: bash searched PATH and no executable matched, or a script's shebang named an interpreter that does not exist (env: 'bash\r' from a CRLF line ending is the common case). It is different from 126, which means the file was found but could not be executed, usually a missing execute bit or a noexec mount. In cron and CI logs, a bare 127 almost always means the job's PATH is shorter than your terminal's.
Why does a command work in my terminal but not in cron?
cron starts jobs with PATH=/usr/bin:/bin and does not read ~/.bashrc, so anything installed in /usr/local/bin, ~/.local/bin, /snap/bin or an /opt directory is invisible to it and fails with command not found, exit 127. Reproduce it with env -i PATH=/usr/bin:/bin bash -c 'yourcommand'. Fix it by using the full path in the crontab line, or by adding a PATH= line at the top of the crontab that lists the directories you need.
Why does sudo say command not found when the command works without sudo?
sudo replaces your PATH with the secure_path setting from /etc/sudoers, which usually lists only the system directories. A tool in ~/.local/bin, /usr/local/bin or an /opt directory is found by your shell and not by sudo. Run sudo with the full path, sudo "$(command -v tool)", or add the directory to secure_path with visudo. Avoid sudo env PATH="$PATH" as a habit: it lets any directory on your personal PATH supply binaries that run as root.
How do I add a directory to PATH permanently?
Add export PATH="$HOME/.local/bin:$PATH" (with your directory) to ~/.bashrc and open a new terminal or run source ~/.bashrc. Put the new directory first to make it win over system copies, or last ($PATH:/dir) to make it a fallback. This only affects your interactive shells: cron, systemd services and sudo each have their own PATH and need their own fix.
Why does bash say No such file or directory for a command that worked a minute ago?
bash remembers where it found each command in a hash table. If the program is then moved or reinstalled to a different directory, bash keeps running the remembered path, which no longer exists, and prints No such file or directory with exit code 127. hash -t name shows the remembered path and hash -r clears the table so the next run searches PATH again. A new terminal also starts with an empty table.
Part of the bash snippets collection
Related Scripts
- Fix /bin/bash^M: bad interpreter — the CRLF cause in full, with a fixer for a whole tree
- Bash Environment Variables — why cron and child processes see a different environment from your terminal
- Fix "Permission denied" in Bash — exit 126: found the file, could not run it
- Fix "unary operator expected" — the other error an empty variable causes
- Bash Error Messages, Decoded — this error and eight others, each with the one check that names the cause