PATH issues from using tmux on macOS
If you’re a macOS user with bash, sh, or ksh as your login shell, then the value of the environment variable PATH may not be what you expect after you invoke tmux from your login shell in Terminal.
Problem
Let’s assume your login shell is bash. Upon launch, Terminal invokes bash as an interactive login shell and hence executes the system-wide configuration sh script /etc/profile 1 2, which contains an instruction to execute path_helper.
/etc/profile
# System-wide .profile for sh(1)
if [ -x /usr/libexec/path_helper ]; then
eval `/usr/libexec/path_helper -s`
fi
if [ "${BASH-no}" != "no" ]; then
[ -r /etc/bashrc ] && . /etc/bashrc
fipath_helper appends the path elements found in the file /etc/paths 3 and files in directory /etc/paths.d to PATH.
/etc/paths
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbinThese path elements are of directories containing system and common utilities. /etc/paths.d is typically empty and may have contents added to it when you install a package that will affect all users on the system.
Immediately after /etc/profile has executed, PATH should be something like the following:
PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbinLet’s say the only user startup file you have is ~/.profile and it contains the following:
~/.profile
eval "$(/opt/homebrew/bin/brew shellenv)"
PATH="$HOME/.local/bin:$PATH"When bash launches as an interactive login shell, ~/.profile will be executed after /etc/profile and thus after path_helper.
The first statement adds the installation of Homebrew to PATH. The second statement adds your local bin directory to the front of PATH in order to have priority over the system utilities during command search. 4 Assume $HOME expands to /Users/alfonso.
After ~/.profile’s execution, the path elements
/Users/alfonso/.local/bin/opt/homebrew/bin/opt/homebrew/sbin
are appended to the front of PATH to yield PATH as the following:
PATH
/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbinThe PATH is all fine and dandy until invoking tmux in the typical manner:
bash
tmuxPATH becomes the following:
PATH
/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbinAs you may have noticed, the path elements
Users/alfonso/.local/bin/opt/homebrew/bin/opt/homebrew/sbin
are present in the front and back of PATH. This is not good.
Cause
tmux launches login shells by default. As a result, the startup files are executed again despite the parent shell having already executed them.
/etc/profile executes first and hence path_helper runs. path_helper notices the path elements it intends to append are already present in PATH. PATH already contains the path elements because PATH was inherited from the login shell that invoked tmux. tmux’s login shell is a child process of the login shell that invoked tmux, and therefore environment variables such as PATH are inherited. path_helper strips those path elements from the back of PATH and appends them to the front of PATH to yield the following PATH:
PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin~/.profile then executes and appends the path elements
Users/alfonso/.local/bin/opt/homebrew/bin/opt/homebrew/sbin
to the front of PATH even though they’re already present in PATH, resulting in the following PATH with duplicate path elements:
PATH
/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbinSolution
The solution to this problem is to add the following highlighted lines to the beginning of ~/.profile:
~/.profile
if [ -n "$TMUX" ]; then
PATH=""
. /etc/profile
fi
eval "$(/opt/homebrew/bin/brew shellenv)"
PATH="$HOME/.local/bin:$PATH"TMUX is an environment variable initialized by tmux with internal information. If TMUX is a non-zero length string, then we can assume we’re in a tmux environment.
Assuming we’re in a tmux environment, before the execution of the consequent commands (the commands following the then statement and before the associated fi), PATH is the following:
PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin/etc/profile has already executed in the current tmux environment given that ~/.profile is the startup file that is now executing. That’s why the above PATH is what it is.
By setting PATH to an empty string and then executing /etc/profile in the current shell environment, PATH is stripped of any path elements appended from user startup files and only consists of path elements appended by /etc/profile. After execution of the consequent commands, the remaining commands append the path elements
Users/alfonso/.local/bin/opt/homebrew/bin/opt/homebrew/sbin
to PATH to yield the following:
PATH
/Users/alfonso/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbinThe PATH is now what we wanted!
Footnotes
-
For more info about
bashstartup files, check out this page from thebashmanual. ↩︎ -
bashis capable of executingshscripts because it is “largely compatible withsh”. Although the use of the word largely may not inspire confidence, I and many others have not run into compatiblity issues that were not bugs. ↩︎ -
macOS is “BSD-inspired”, so this documentation page about the FreeBSD directory structure is helpful to understand the purpose of certain directories. ↩︎
-
For more about command search and
PATH, check out the POSIX standard sections “Command Search and Execution” and “Other Environment Variables” section, respectively. ↩︎