zsh: command not found on macOS: how PATH works

command-not-found

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/local Homebrew too. You can end up with two Homebrews, and which copy of a tool runs depends on the order in PATH. type -a <name> shows both.
  • Terminal running under Rosetta. If arch prints i386, 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, gradlew and scripts in the current folder need ./ in front: ./mvnw. Commands from a project's node_modules/.bin run through npx <name> or a package.json script.
  • Shell functions. nvm, mkvirtualenv and conda activate are functions defined by a script your ~/.zshrc must 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 found comes from bash, which reads ~/.bash_profile instead. 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 set PATH in that tool's settings.

Guides for specific tools

Sources

related.

Ruslan Osipov

Ruslan Osipov

About the author