env.dev

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
Gotcha: Prepending vs appending changes behavior: `export PATH=/new/bin:$PATH` lets /new/bin override system binaries, while `export PATH=$PATH:/new/bin` only fills gaps. A classic footgun is `export PATH=/new/bin` (no `:$PATH`) — it wipes the entire existing PATH, and suddenly even `ls` and `git` stop resolving until you open a new shell.

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.

bash
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 binary

Why 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 split C: off every entry).
  • cmd.exe searches the current directory first, before PATH — a legacy DOS behavior PowerShell deliberately dropped (it requires .\program, same as Unix). The Win32 CreateProcess search order also includes the application directory and the current directory ahead of PATH; Microsoft ships the NoDefaultCurrentDirectoryInExePath environment variable to switch the cwd step off because of decades of binary-planting attacks (CWE-426, "untrusted search path").
  • PATHEXT decides which extensions count as executable (.COM;.EXE;.BAT;.CMD;...), so npm resolves to npm.cmd without 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 you cd into 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_path option in /etc/sudoers replaces it with a vetted list, so sudo mytool can fail with "command not found" even though mytool works 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.

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.

Was this helpful?

Stay up to date

Get notified about new guides, tools, and cheatsheets.

Browse all 244 environment variables →