Add a folder to PATH on a Mac so the command is found
Adding a folder to PATH on a Mac is three seconds of typing and then, often, an afternoon of confusion. The line goes into a startup file, the command works, a new Terminal window opens and the command is missing again. Or the command works in Terminal and not in an editor launched from the Dock. Or the line is clearly in the file, echo $PATH clearly contains the folder, and the wrong version of the binary still runs.
All three of those have the same root cause: on macOS, PATH is assembled by more than one thing, in a fixed order, and the last one to touch it wins. Knowing that order turns the problem from trial and error into a single decision about which file to edit.
Which file the shell reads, and when
The default login shell on recent macOS releases is zsh, and man zsh states the startup sequence exactly. Commands are read from /etc/zshenv first, and that cannot be overridden. Then $ZDOTDIR/.zshenv. If the shell is a login shell, /etc/zprofile and then $ZDOTDIR/.zprofile. Then, if the shell is interactive, /etc/zshrc and then $ZDOTDIR/.zshrc. Finally, for a login shell, /etc/zlogin and $ZDOTDIR/.zlogin. When ZDOTDIR is unset, the home directory is used, which is the normal case.
Two consequences of that list explain most of the confusion.
A Terminal window on macOS starts a shell that is both a login shell and interactive, so it reads nearly all of those files. A shell started by a script, by a Makefile, or by an editor's integrated terminal may be neither, in which case only the zshenv files are read. That is why a ~/.zshrc edit fixes the prompt and does nothing for a build step.
More importantly, /etc/zprofile is not empty. On a stock macOS install it contains a handful of lines, and the operative ones are these:
if [ -x /usr/libexec/path_helper ]; then
eval `/usr/libexec/path_helper -s`
fi
That runs after ~/.zshenv and before ~/.zprofile and ~/.zshrc. Anything that touches PATH in ~/.zshenv has already been rewritten by the time the prompt appears. Bash users on older setups met the same thing through /etc/profile, which calls the same helper.
Why the addition ends up at the end
man path_helper gives the summary: the utility reads the contents of the files in /etc/paths.d and appends their contents to PATH, with default values obtained from /etc/paths first. Files should contain one path element per line. The manual also notes that the utility should not be invoked directly, being intended only for use by the shell profile.
The source, which Apple publishes, fills in the part that matters. The construction runs in this order: the entries from /etc/paths, then the entries from every regular file one level deep in /etc/paths.d, then, at the end, whatever PATH already contained, merged in element by element and skipped if already present.
The last step is the one that catches people. An entry exported before path_helper runs is not removed, it is moved to the back. A folder that was written first in the file ends up after /usr/bin, and the system copy of a tool keeps winning. Nothing in the shell configuration looks wrong, because nothing in the shell configuration is wrong.
The /etc/paths.d ordering is deliberate too. The comparison function in the source reads a leading number from each filename with strtol and sorts on it, falling back to a locale collation of the whole name. That is why files there are conventionally named with a numeric prefix, and why a file named 10-something is placed ahead of one named 50-something. On a stock system /etc/paths itself contains /usr/local/bin, /System/Cryptexes/App/usr/bin, /usr/bin, /bin, /usr/sbin and /sbin, in that order.
The four places that work, compared
Each option below genuinely works. They differ in who they affect and in whether they survive path_helper.
| Where | Applies to | Runs before path_helper | Needs admin |
|---|---|---|---|
~/.zshrc |
Interactive shells for one user | No, so the order written is kept | No |
~/.zprofile |
Login shells for one user | No, so the order written is kept | No |
~/.zshenv |
Every zsh, including scripts | Yes, so the entry is moved to the end | No |
/etc/paths.d/<file> |
Every user, every login shell | It is the input, so order is by filename | Yes |
launchctl config user path |
Apps and services, not just shells | Separate mechanism | Yes, plus a reboot |
For one person adding one folder, ~/.zshrc is the sane default. It runs after path_helper, so a prepend stays a prepend, and it is the file most documentation assumes. The line is conventional:
export PATH="$HOME/bin:$PATH"
~/.zprofile suits a folder that should be present in login shells but is not worth re-evaluating for every new interactive shell. ~/.zshenv is the right file only for things every non-interactive shell also needs, and PATH additions are usually not in that category precisely because of the reordering.
/etc/paths.d is the option to reach for when the folder belongs to the machine rather than to a person. A file there containing one line takes effect for every account without editing anyone's dotfiles, which is also why package installers use it. Writing to /etc requires administrator rights, and adding a folder that a non-admin user can write to is worth thinking about twice, because it becomes an early entry in every user's PATH.
Keeping the order deliberate
PATH is searched left to right, so position decides which copy of a command runs when two exist. This is not a detail: a Mac frequently has two or three copies of python3, git or node, and the one that answers is simply the one found first.
zsh exposes PATH twice, as the scalar PATH and as the array path, and man zshparam describes the array as the list of directories to search for commands, noting that when the parameter is set each directory is scanned and all files found are put in a hash table. Working with the array avoids the quoting mistakes that a long colon-separated string invites:
path=("$HOME/bin" $path) # prepend
path+=("$HOME/.local/bin") # append
Duplicates accumulate quickly, because a file gets sourced twice or a tool inserts its own line on every update. man zshbuiltins documents the fix under typeset: the -U flag keeps only the first occurrence of each duplicated value, applies to colon-separated special parameters such as PATH, takes effect on assignment, and should be set for every interface to a shared value. The recommended form is therefore typeset -U PATH path, placed before the lines that modify it.
The choice between prepend and append deserves a moment rather than a habit. Prepending puts a personal folder ahead of /usr/bin, which is what makes a newer tool win, and also means a file dropped into that folder can shadow a system command for every later command in the session. Appending is safer and frequently useless, since the system copy keeps answering. The practical middle is to prepend a folder that only one person can write to, and to append anything shared.
Apps launched from the Dock ignore all of it
A command that works in Terminal and not inside a graphical application is not a PATH ordering problem. Applications started by the Dock, by Finder or by Spotlight are launched by launchd, which never reads a shell startup file. There is no ~/.zshrc in their history at all.
man launchctl documents the one supported way to change this. The config subcommand sets persistent configuration for a launchd domain, and path is one of exactly two supported parameters, setting the PATH environment variable for all services within the target domain. The manual is explicit about two constraints. A reboot is required for the change to take effect. And if a service specifies its own PATH, the service-specific value takes precedence. The note underneath adds that the facility is intentionally scoped to PATH and nothing else, for security reasons.
Because it is domain-wide and needs a restart, this is a heavy instrument. In most cases the lighter answer is to give the application the absolute path to the binary in its own settings, which is what editors and launchers generally provide a field for. Reserve the launchctl route for a folder that genuinely has to be visible to every service the account runs.
Checking the result without guessing
Three checks separate the possible failures, and each takes one line.
Print PATH one entry per line rather than as a wall of colons, with echo $PATH | tr ':' '\n'. This makes the order obvious, and duplicates and empty entries visible. An empty element, which shows as a leading, trailing or doubled colon, means the current directory, so a stray colon quietly puts the working directory in the search path.
Ask which copy will run, and which copies exist. In zsh, whence -a <command> lists every match along PATH rather than only the first, and type -a behaves the same way. Comparing that list against the printed PATH shows immediately whether the problem is a missing entry or a wrong order.
If a binary was just installed into a folder already in PATH and the shell still reports it as not found, the hash table is stale. man zshparam explains why the table exists, and man zshbuiltins gives hash -r, with rehash as a synonym, to rebuild it. It also notes that a command name starting with / is never hashed, which is why an absolute path works while the bare name does not. Keeping the folder listing and the shell in one window makes this comparison quick, and the Features page covers what that arrangement looks like in practice.
The order to test in is worth stating plainly: check the current shell first, then open a new window, then log out and back in. A change that survives all three is permanent. A change that survives only the first is in the wrong file, and the table above says which one to move it to. The FAQ page answers the related questions about where per-project settings belong.
One further test is worth running when a build step is the thing that fails. The shell a script gets is usually neither a login shell nor interactive, so it reads only the zshenv files. Running the failing command through a non-interactive shell, rather than typing it at the prompt, reproduces what the build actually sees. If the command works at the prompt and not that way, the fix is either to give the script an absolute path or to accept that the entry belongs in a file every shell reads, with the reordering that implies.
Why the same edit behaves differently on an older Mac
Setups that predate zsh as the default shell put the same line in ~/.bash_profile or ~/.bashrc, and both are still read when bash is started deliberately. The helper is not specific to zsh, though: the system profile for bash calls path_helper as well, so the reordering described above applies identically. A Mac that has been carried forward through several migrations often carries both sets of files, with one line in each, which is a common source of duplicated entries. Printing PATH one line at a time is the fastest way to see which files are contributing, since a folder appearing twice means two files are adding it.
What to change first
Put the line in ~/.zshrc, prepend rather than append, and add typeset -U PATH path above it so repeated edits cannot pile up duplicates. Then verify with echo $PATH | tr ':' '\n' in a brand new window, not in the one that was already open. If the folder needs to exist for every user on the machine, move it to a numerically prefixed file in /etc/paths.d instead, and if a graphical application is the thing that cannot find the command, the shell files were never going to help. A window that holds the folder, the file being edited and the shell at once shortens that loop, which is part of what Atriens is for.
Frequently asked questions
Should the line go in .zshrc or .zprofile?
~/.zshrc for anything wanted at an interactive prompt, ~/.zprofile for login shells only. Both run after /etc/zprofile has called path_helper, so in both cases a prepend stays at the front. The file to avoid for PATH is ~/.zshenv, which runs before path_helper and has its additions moved to the end.
Why is my folder at the end of PATH even though I wrote it first?
Because path_helper rebuilt the variable. It starts from /etc/paths, adds the files in /etc/paths.d, and only then merges the pre-existing PATH entries onto the end. Anything exported before /etc/zprofile runs therefore survives but loses its position, and moving the line to ~/.zshrc fixes it.
Do changes to PATH affect apps opened from the Dock?
No. Those are launched by launchd, which does not read shell startup files. The documented route is launchctl config user path, which man launchctl notes requires a reboot to take effect and is overridden by any service that sets its own PATH. Giving the application an absolute path in its own settings is usually simpler.
Does adding a folder to /etc/paths.d require a restart?
No, but it does require a new login shell, because path_helper reads the directory when /etc/zprofile runs. Opening a new Terminal window is enough. Editing files under /etc needs administrator rights, and the numeric prefix in the filename decides where the entry lands relative to other files there.
The binary exists and the folder is in PATH, so why is the command still not found?
The shell's command hash table is probably stale. man zshparam notes that each directory in the path is scanned and its files put in a hash table, so a binary added after that scan is invisible until the table is rebuilt with hash -r, also spelled rehash. An absolute path works in the meantime, since names beginning with / are never hashed.