Setting the PATH variable on macOS with ZSH

Published by Bill Glover on

The behaviour of the PATH variable in ZSH on macOS can be confusing. If you follow ZSH guidance, you should set your PATH variable in ~/.zshenv.

.zshenv is sourced on all invocations of the shell, unless the -f option is set. It should contain commands to set the command search path, plus other important environment variables. .zshenv should not contain commands that produce output or assume the shell is attached to a tty.”

To illustrate the confusing behaviour, I need an example. I’m going to show how prepending a component to PATH does not result in the expected behaviour. Before you jump in and tell me that this isn’t something I should be doing, I hear you. There are reasons why this is a bad idea and, in general, you should be appending PATH components instead. I’m doing it here for illustrative purposes only.

My .zshenv:

export PATH=/Users/bg/bin:$PATH

My PATH variable:

/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/Users/bg/bin

I expected the /Users/bg/bin path component to appear as the first component in the PATH variable. system default. Instead it is being appended to the end of the existing PATH. Why?

The answer lies in the order ZSH reads its configuration files. The full sequence depends on the type of shell you are opening. But we are lucky. We can explain this unexpected behaviour early on in the sequence.

  1. /etc/zshenv - this doesn’t exist on my machine
  2. ~/.zshenv - this is where we set our path
  3. /etc/zprofile - this runs a utility called path_helper
  4. ~/.zprofile - this doesn’t exist on my machine

The path_helper utility sets up the default path components. Because it is called in /etc/zprofile it runs after we have made our PATH modifications. We are prepending our custom path to an empty PATH variable. The default PATH components are set-up after our modifications. This explains why our custom path component appears at the end of the PATH variable.

If we were to ignore the ZSH guidance and set up our path in ~/.zshrc we don’t see this behaviour. This is because ~/.zshrc exists later in the chain. We get the behaviour we expect.

My .zshrc:

export PATH=/Users/bg/bin:$PATH

My PATH variable:

/Users/bg/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin

In most cases it won’t matter where you set your PATH variable. But it is reassuring to know that there is an explanation behind the behaviour I was seeing.