PATH
A colon-delimited list of directories the shell searches for executable programs. When you run a command, the shell checks each directory in PATH from left to right until it finds a matching executable. This is one of the most fundamental environment variables on Unix-like systems.
Last updated:
PATH is the colon-separated list of directories your shell searches, left to right, to resolve a bare command name like `git` or `node`. Order is everything: the first match wins, so a directory earlier in PATH shadows the same binary later on. This is why `which node` and version managers (nvm, pyenv, rbenv) work by prepending their shim directory to PATH, and why 'command not found' after installing a tool almost always means its directory is not on PATH. On Windows the separator is a semicolon, not a colon.
- Provider
- General / OS
- Category
- system
- Set by
- Set by the shell profile files (~/.bashrc, ~/.zshrc, /etc/environment)
- Example
- /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
How to set PATH
add a directory (prepend, takes priority)
export PATH="$HOME/.local/bin:$PATH"add a directory (append, lowest priority)
export PATH="$PATH:/opt/tools/bin"inspect, one entry per line
echo "$PATH" | tr ':' '\n'How does PATH resolution actually work?
When you type a command without a slash, the shell hands the name to the C library's execvp(3) family, which tries each PATH directory in order and executes the first match. A command containing a slash — ./script.sh, /usr/bin/git — skips PATH entirely, which is why the leading ./ is required to run something from the current directory on Unix. Shells add one wrinkle on top: bash and zsh cache successful lookups in a hash table, so after you move or install a binary the shell can keep executing the old path. hash -r (bash) or rehash (zsh) clears the cache; so does opening a new shell.
type -a node # every resolution, in PATH order — first one wins
which -a node # same idea, external lookup
echo "$PATH" | tr ':' '\n' # one directory per line, in search order
hash -r # bash: forget cached lookups after moving a binaryWhy do version managers fight over PATH order?
nvm, pyenv, rbenv, asdf, and mise all work the same way: prepend a shim or version-specific bin directory so their managed binary shadows the system one. First match wins, so whoever prepends last in your shell profile wins at runtime. This is the root cause of "I switched Node versions but node -v didn't change" — something later in your .zshrc re-prepended a different directory. Diagnose with type -a node: every entry above the one you want is a shadow. The Node.js environment variables guide covers how nvm wires this up.
macOS adds its own twist: /usr/libexec/path_helper runs from /etc/zprofile and reorders PATH from /etc/paths and /etc/paths.d — which is why PATH set in .zshenv can end up mysteriously rearranged by the time a login shell finishes starting.
How is PATH different on Windows?
- The separator is a semicolon (
C:\bin;C:\tools), not a colon — a detail that breaks naive cross-platform scripts that split on:(which would splitC:off every entry). cmd.exesearches the current directory first, before PATH — a legacy DOS behavior PowerShell deliberately dropped (it requires.\program, same as Unix). The Win32CreateProcesssearch order also includes the application directory and the current directory ahead of PATH; Microsoft ships theNoDefaultCurrentDirectoryInExePathenvironment variable to switch the cwd step off because of decades of binary-planting attacks (CWE-426, "untrusted search path").PATHEXTdecides which extensions count as executable (.COM;.EXE;.BAT;.CMD;...), sonpmresolves tonpm.cmdwithout you typing the extension.- PATH is the merge of a machine-wide value and a per-user value from the registry; installers edit those, and already-open terminals keep their stale copy until restarted.
PATH hijacking: the security angle
Every directory on PATH is a place an attacker can plant a malicious binary named ls or git and wait for someone — ideally root — to run it. The classic rules follow directly from "first match wins":
- Never put
.(the current directory) on PATH. A tarball you just extracted becomes attacker-controlled PATH territory the moment youcdinto it. - Never put world- or group-writable directories ahead of system directories. This is exactly the bug class behind a long line of local privilege escalations in installers and services that launch helpers via a relative name.
- This is also why sudo ignores your PATH by default: the
secure_pathoption in /etc/sudoers replaces it with a vetted list, sosudo mytoolcan fail with "command not found" even thoughmytoolworks fine without sudo.
Why is PATH different in cron, CI, and containers?
Your interactive PATH is the product of login scripts that non-interactive environments never run. Vixie cron hands jobs a bare PATH=/usr/bin:/bin; Docker containers get whatever the base image baked in; CI runners and systemd units each have their own minimal defaults. The fix is always the same — set PATH explicitly in the crontab/unit/Dockerfile, or call binaries by absolute path. If a script "works in my terminal but not in cron", diff echo $PATH between the two before debugging anything else. The same dynamic applies to every variable, not just PATH — the Docker environment variables guide and best practices guide walk through how each environment builds its variable set.
References
Frequently Asked Questions
I installed a CLI but get 'command not found'. Why?
Its install directory is not on PATH, or the shell has cached the old PATH. Find the binary (e.g. `find / -name toolname 2>/dev/null`), add its directory with `export PATH="/that/dir:$PATH"` in your ~/.bashrc or ~/.zshrc, then open a new shell or run `hash -r` to clear the lookup cache.
Why does the wrong version of a tool run?
PATH is searched left to right and the first match wins. Another copy earlier in PATH is shadowing the one you want. Run `which -a toolname` to see every match in order, then reorder PATH so the directory you want comes first.
Stay up to date
Get notified about new guides, tools, and cheatsheets.