Do you know where your PATH is?

/media/images/2025-03-21_15-19_system_prompt.png

Yesterday I had a program fail because an update had modified my PATH. This led to an hours long investigation into just how my system PATH is getting generated.

The system PATH is a list of directories that will be searched when you run a binary from the command line. I live in a tmux session running on top of the fish shell, in alacritty, on xfce, in manjaro, an arch based, linux distribution. There are many agents that can monkey with my PATH.

You can view your current PATH with:

echo $PATH

When I ran this during the failure I saw this:

na@hydra ~> echo $PATH
/home/na/source/gocode/bin
/home/na/bin
.
.
.
/home/na/.pyenv/libexec/pyenv
/home/na/.pyenv/libexec
/home/na/.pyenv/shims
/home/na/.pyenv/bin
/opt/rocm/bin
.
/home/na/.pyenv/libexec/pyenv
/home/na/.pyenv/libexec
/home/na/.pyenv/shims
/home/na/.pyenv/bin
.
/home/na/.cargo/bin
/home/na/.local/bin
/usr/local/bin /usr/bin
/bin
/usr/local/sbin
/var/lib/flatpak/exports/bin
/usr/lib/jvm/default/bin
/usr/bin/site_perl
/usr/bin/vendor_perl
/usr/bin/core_perl

There were duplicates and missing paths and I wanted to know who was responsible for this mess. Unfortunately I couldn't find any good way to determine who is modifying your PATH. I wanted something like this:

~/.config/fish/config.fish  added   '~/bin' to your $PATH
~/.profile called '~/.cargo.env' which added '~/.cargo/bin' to your $PATH
/etc/login.defs added '/usr/bin' to your $PATH

Fish has some pretty cool debug flags, but nothing like this.

I like fish. It does a good job of shipping with batteries included, but I don't like how they handle environment variables. The fact that the first entry in their FAQ is how to view, set, and clear your environment variables illustrates the complexity.

The set command has flags to set the scope of variables to local (-l), global (-g), and function (-f). It can also use -U to set a 'universal variable' which will persist across restarts, and an -x option to export variables to child processes. They also have a helper function that will check if a PATH entry already exists before adding it again.

My $PATH had a bunch of duplicates and questionable entries. I tracked them down by grepping through my system config and dotfiles. Several hours later, here's my current PATH and where they are populated.

path                               where it comes from
----                               -------------------
/home/na/.pyenv/bin                # fish env
/home/na/.pyenv/shims              # fish env
/home/na/.pyenv/libexec            # fish env
/home/na/source/gocode/bin         # fish env
/home/na/bin                       # fish env
.                                  # ???
.                                  # ???
.                                  # ???
.                                  # ???
/home/na/.cargo/bin                # ~/.profile -> ~/.cargo/env
/home/na/.nix-profile/bin          # /etc/profile.d/nix.fish
/home/na/.nix-profile/bin          # /etc/profile.d/nix-daemon.fish
/nix/var/nix/profiles/default/bin  # /etc/profile.d/nix-daemon.fish
/home/na/.local/bin                # /etc/profile.d/home-local-bin.sh
/usr/local/bin                     # /etc/login.defs
/usr/bin                           # /etc/login.defs
/bin                               # ???
/usr/local/sbin                    # /etc/login.defs
/var/lib/flatpak/exports/bin       # /etc/profile.d/flatpak-bindir.sh
/usr/lib/jvm/default/bin           # /etc/profile.d/jre.sh
/usr/bin/site_perl                 # /etc/profile.d/perlbin.sh
/usr/bin/vendor_perl               # /etc/profile.d/perlbin.sh
/usr/bin/core_perl                 # /etc/profile.d/perlbin.sh
/opt/rocm/bin                      # /etc/profile.d/rocm.sh

It took several hours to compile this list because some of the entries were added by child processes, and many of the settings persisted in my login shell and fish cache. Also, the PATH will be different depending on if you're running in an interactive shell or a login shell.

One result of this investigation is I removed my pyenv fish plugin. It was inexplicably adding two lists to my PATH including a path to a binary file. This raised enough red flags I just removed the plugin and manually configured the three paths in my fish config.

I also learned that .profile is automatically loaded on every login. To test modifications to this file you need to log out after making changes.

Many of the paths are set up by the system. Scripts in /etc/profile.d/ are run according to your shell and /etc/login.defs sets up some system-wide paths. I could not find who was setting up /bin. Since this is the old school binary path, it might be built into the kernel. The fish scripts found in profile.d were responsible for the nix duplicates. Duplicates aren't bad, but it's sloppy, and I like my system maintainers to be careful.

Finally, there's a bunch of '.' duplicates. I couldn't find where these were being added, and I obviously didn't bother grepping for '.'. I remember working with a windows engineer who was forced by extremity to write code for linux. He was surprised to learn that '.' wasn't in the path. I always appreciated this as a security measure. Writing:

./blow_up_the_world.bin

Is so much more deliberate than writing:

blow_up_the_world.bin

and having your system go looking for a binary that will destroy everything.

I don't know how long distros have been adding '.' to the PATH, but I would remove it if I could.

That windows engineer made his project a subdirectory in a folder called 'linsux'

You can see my updated fish config in the dotfiles project in my gitforge.