“Command Not Found” in Linux: A Real Troubleshooting Checklist
You run a command and bash throws back the same unhelpful line every time:
$ terraform init
bash: terraform: command not found
Most guides tell you to “check your PATH” and stop there. That’s step one, not the whole answer — and it’s rarely even the actual problem once you’ve been doing this a while. Below is the checklist I actually run through, in the order that catches the most common causes first.
1. Rule out a typo
Obvious, but worth a second look, especially over SSH where autocomplete sometimes doesn’t kick in. Run it again, slowly, and check for anything odd — invisible unicode characters pasted from a doc, a trailing space, wrong case (Linux is case-sensitive: Terraform and terraform are different commands).
2. Confirm it’s actually installed
Don’t assume — verify. command -v is the POSIX-portable way to check if something resolves to an executable, alias, or function:
$ command -v terraform
$ echo $?
1
Empty output and a non-zero exit code means bash genuinely can’t find it anywhere on your PATH. Next question: is it installed at all, just not linked into PATH?
$ find / -name "terraform" -type f 2>/dev/null
/opt/terraform/terraform
There it is. Installed, but sitting in a directory bash never looks at.
3. Check what’s actually on your PATH
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/opt/terraform isn’t in that list, so bash has no way of knowing it exists. Fix it for the session:
export PATH="$PATH:/opt/terraform"
And to make it permanent, add that line to the right shell config file — which brings up the next, more subtle problem.
4. The PATH trap: interactive vs. non-interactive, login vs. non-login shells
This is where most people waste an hour. You add the export line to ~/.bashrc, it works fine when you SSH in and test it manually, then it silently fails in a cron job or a systemd service. Why? Because non-interactive, non-login shells don’t source .bashrc the same way an interactive login shell does.
The short version:
- Login shell (first shell after SSH login, or a TTY login) reads
/etc/profile, then the first of~/.bash_profile,~/.bash_login, or~/.profileit finds. - Interactive non-login shell (opening a new terminal tab on an already-logged-in session) reads
~/.bashrc. - Non-interactive shell (a script, a cron job, most CI runners) reads neither by default — it inherits whatever PATH the parent process handed it.
So a PATH export sitting only in ~/.bashrc will never be seen by a cron job. If a script needs a specific PATH, set it explicitly at the top of the script rather than relying on shell startup files at all — it’s more predictable and doesn’t break the next time someone changes their login shell.
5. sudo has its own PATH
This one catches people constantly: a command works fine as your user, then “command not found” the moment you add sudo in front of it.
$ sudo -V
Sudo version 1.9.13p3
By default, sudo doesn’t inherit your shell’s PATH. It uses secure_path, defined in /etc/sudoers, which is a fixed, restricted list — usually just the standard system directories. If your tool lives in /opt/terraform or anywhere non-standard, sudo terraform will fail to find it even though plain terraform works, because secure_path doesn’t include it. You either add the directory to secure_path with visudo, or call the binary by its full path when using sudo.
6. Was it there before, and did something remove it?
If a command used to work and suddenly doesn’t, someone or something uninstalled it — a package cleanup, a colleague running apt autoremove, an image rebuild. Check the package manager’s log instead of guessing:
# Debian/Ubuntu
$ grep -i "remove" /var/log/dpkg.log
# RHEL/CentOS/Fedora
$ grep -i "erase" /var/log/dnf.log
This turns “it just stopped working” into an actual answer with a timestamp attached.
7. Check for shadowing — alias, function, or another binary with the same name
Sometimes the command *is* found, just not the one you expected. type tells you more than which does here, because it also reports builtins, aliases, and functions — not just files on disk:
$ type cd
cd is a shell builtin
type reveals builtins, aliases and every binary competing for the same name.$ type -a python3
python3 is /usr/local/bin/python3
python3 is /usr/bin/python3
which would only ever show you the first match on PATH. type -a shows every place a name resolves, in the order the shell would check them — which is exactly what you need when two versions of the same tool are fighting for the same name.
The fast version
- Retype the command — rule out typos and case.
command -v <name>— is it installed and on PATH at all?find / -name "<name>" -type f 2>/dev/null— is it installed but not on PATH?echo $PATH— what’s actually being searched?- Setting PATH in a script? Don’t rely on
.bashrc— non-interactive shells won’t read it. - Using
sudo? Remember it has its ownsecure_path, separate from yours. - Command used to work? Check
dpkg.log/dnf.logfor removal history. type -a <name>— check for aliases, functions, or a shadowing binary.
“Command not found” is one error message covering at least half a dozen distinct causes. Working through them in this order — cheapest checks first — gets you to the real answer faster than guessing.
Keep reading
- Linux Package Management with apt and dnf — Like an Admin — Find and install the package that provides the missing command.
- Linux Special Characters Explained (Shell Cheat Sheet) — The shell characters that cause many confusing errors.
- Bash Scripting for Sysadmins (Not Hackers) — Quoting and PATH habits for safer scripts.
