Do you use shell-based version managers like nvm, pyenv, or rbenv, in combination with tmux on macOS? If so, you may be in for a surprise. Outside of tmux and screen, version managers will work fine, but inside the session, system or Homebrew installed tools will be used instead. What’s going on here?
Path Helper
MacOS has a utility called path_helper
that initializes the PATH
environment variable by reading paths from /etc/paths and then any file under /etc/paths.d. The path helper utility doesn’t technically set the PATH
, but will output the shell commands to set it accordingly. It is eval
‘d by the first file sourced on a login shell for sh, bash, and zsh:
# Code snippet found in /etc/profile and /etc/zprofile
if [ -x /usr/libexec/path_helper ]; then
eval `/usr/libexec/path_helper -s`
fi
Path Helper and Existing PATH
If you already have a PATH
variable set when invoking path_helper
, such as when you start a new tmux session, path_helper
will ‘intelligently’ create a new PATH
by merging the loaded paths with existing PATH
and removing duplicates. The problem is, while path_helper
deduplicates paths, it doesn’t preserve order. If you have a custom path or paths prepended to the default list in your ~/.bashrc or ~/.zshrc and invoke the path helper, your custom path(s) will now be at the end of the PATH
variable. This effectively disables all your version managers by prioritizing system binaries over them.
Solution
Luckily there is a setting in tmux to tell it to avoid creating a login shell when starting a new session. This avoids sourcing /etc/[z]profile
, and path_helper
does not get invoked. Simply add this setting to your tmux.conf
and you should be good to go:
# Don't create login shells
set -g default-command "${SHELL}"
If having tmux create login shells is somehow a requirement for you, your only other choice is to create or edit the first user controlled file that gets sourced in a login shell, either ~/.bash_profile
or ~/.zprofile
, and add some code to fix your PATH
variable by comparing the existing PATH
against the paths generated when running path_helper
in a clean environment and then rebuilding the PATH
variable with user-defined paths prepended to the system-defined paths:
reorganize_login_subshell_path() {
# save path as old_path
local old_path="$PATH"
# run path_helper against an empty PATH
PATH=''
eval `/usr/libexec/path_helper -s`
# At this point the PATH contains only system-wide paths.
# If paths are the same this is not a subshell or no user-defined paths are
# set. In other words, the PATH is correct and there is no work to be done.
if [ "$old_path" = "$PATH" ]; then
return
fi
# Use parameter substitution to subtract system-wide paths from old_path,
# leaving only user-defined paths. "${var#Pattern}" means "Remove from $var
# the shortest part of $Pattern that matches the front end of $var."
local user_defined_paths="${old_path#$PATH:}"
# Rebuild PATH with user-defined paths prepended to system-wide paths.
PATH="$user_defined_paths:$PATH"
}
if [ -x /usr/libexec/path_helper ]; then
reorganize_login_subshell_path
fi
# remove path reorganization function to avoid cluttering environment
unset -f reorganize_login_subshell_path
The latter approach here is not recommended as it attempts to solve the problem of PATH
munging with more PATH
munging, making it all the more difficult to reason about the state of your shell environment.