zsh: command not found on macOS: how PATH works
Published
zsh: command not found: <name> means one thing: zsh looked for <name> and
did not find it. zsh has been the default shell on macOS since Catalina, so
this is the error every Mac user sees when a tool is missing. The fix is
either to install the tool or to tell zsh where it already is. This page
explains how zsh searches, which file to edit, and where Homebrew puts
things. The guides for specific tools are at the end.
How zsh finds a command
When you type mvn, zsh checks, in order: aliases, shell functions,
builtins, and then each folder listed in PATH, left to right. The first
match wins. If no folder has an executable called mvn, you get the error.
zsh keeps PATH in two linked forms: the colon-separated string $PATH and
the array $path. The array is easier to read:
print -l $path # one folder per line, in search order
command -v mvn # where zsh would run mvn from, or nothing
type -a python3 # every match, in order, including aliases
Step 1: is the tool installed at all?
Search the usual places before you change any settings:
ls /opt/homebrew/bin/<name> /usr/local/bin/<name> 2>/dev/null
brew list --versions | grep <name>
If nothing turns up, the tool is not installed, and editing PATH cannot
help. A few common ones were part of macOS once and are not any more: php
(removed in macOS 12), telnet (removed in 10.13) and python (only
python3 exists since 12.3).
Step 2: where Homebrew puts commands
Homebrew links every command it installs into one folder, and that folder depends on the Mac:
| Mac | Homebrew prefix | Commands in |
|---|---|---|
| Apple silicon (M1 and later) | /opt/homebrew |
/opt/homebrew/bin |
| Intel | /usr/local |
/usr/local/bin |
/usr/local/bin is on the default macOS PATH. /opt/homebrew/bin is not,
which is why Homebrew's installer ends by asking you to add this line to
~/.zprofile:
eval "$(/opt/homebrew/bin/brew shellenv)"
It sets PATH and a few HOMEBREW_* variables. If you skipped that step,
every tool installed with brew gives "command not found" on an Apple
silicon Mac, brew included. See
zsh: command not found: brew.
Two traps on Apple silicon Macs:
- Migrated from an Intel Mac. Migration Assistant copies the old
/usr/localHomebrew too. You can end up with two Homebrews, and which copy of a tool runs depends on the order inPATH.type -a <name>shows both. - Terminal running under Rosetta. If
archprintsi386, the terminal runs as Intel code, and the installers you run in it may pick/usr/local. Turn off "Open using Rosetta" in the app's Get Info window.
Step 3: put the folder on PATH in the right file
zsh reads its startup files in a fixed order. Which ones it reads depends on how it was started:
| File | Read by | Use it for |
|---|---|---|
~/.zshenv |
every zsh, including scripts | almost nothing on macOS |
/etc/zprofile, then ~/.zprofile |
login shells | PATH and other environment variables |
/etc/zshrc, then ~/.zshrc |
interactive shells | aliases, prompt, completion, nvm, pyenv init |
~/.zlogin |
login shells, after .zshrc |
rarely needed |
Terminal.app, iTerm2 and the VS Code terminal start zsh as a login shell, so
a new tab reads both ~/.zprofile and ~/.zshrc. That gives a simple rule:
PATH goes in ~/.zprofile, everything interactive goes in ~/.zshrc.
Why not ~/.zshenv? On macOS, /etc/zprofile runs a tool called
path_helper after ~/.zshenv. It rebuilds PATH from /etc/paths and
/etc/paths.d and moves your additions behind the system folders. A
folder added in ~/.zshenv can therefore lose to /usr/bin without any
error.
Add a folder to the front so it wins over older copies further down:
# ~/.zprofile
export PATH="$HOME/.local/bin:$PATH"
Never write export PATH=/some/folder without :$PATH. That replaces the
whole search list, and then even ls gives "command not found". If that
already happened, restore a working list for the current tab, then fix the
file:
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
open -e ~/.zprofile
Step 4: load the change
Editing a file changes nothing in tabs that are already open. Open a new tab, or replace the current shell with a fresh login shell:
exec zsh -l
source ~/.zprofile also works, but running it more than once adds the same
folder to PATH again each time. typeset -U path in ~/.zprofile stops
duplicates. After installing a new tool into a folder already on PATH, run
rehash so zsh forgets any command locations it cached.
When PATH is not the problem
- Project-local commands.
mvnw,gradlewand scripts in the current folder need./in front:./mvnw. Commands from a project'snode_modules/.binrun throughnpx <name>or apackage.jsonscript. - Shell functions.
nvm,mkvirtualenvandconda activateare functions defined by a script your~/.zshrcmust load. Installing the package without that line gives "command not found". - Different version managers. With nvm, pyenv or asdf, a global tool is installed for one version only. Switch versions and it is gone.
- Not zsh at all.
-bash: <name>: command not foundcomes from bash, which reads~/.bash_profileinstead. See bash: command not found. - GUI apps, cron and Git hooks. They do not read your zsh files. Use the
full path from
command -v <name>, or setPATHin that tool's settings.
Guides for specific tools
- zsh: command not found: brew: load Homebrew's shell setup.
- zsh: command not found: mvn: Maven,
./mvnwandJAVA_HOME. - zsh: command not found: composer: Composer and its global
vendor/bin. - zsh: command not found: php: PHP after macOS 12, and versioned
php@formulas. - zsh: command not found: yarn: Corepack, Yarn Classic and nvm.
- zsh: command not found: virtualenv:
python3 -m venv, pipx and~/Library/Python. - zsh: command not found: telnet: Homebrew, or
ncandcurlinstead. - zsh: command not found: jupyter: Jupyter in a virtual environment.
- node: command not found: Node.js and npm.
Sources
related.
Ruslan Osipov
About the author