The most surprising thing about Zsh startup files is that the order in which they’re read isn’t always what you’d expect, and the first one to set a variable often wins, regardless of later files.

Let’s see this in action. Imagine you have a simple zshenv and zshrc:

~/.zshenv:

export MY_VAR="set in zshenv"

~/.zshrc:

export MY_VAR="set in zshrc"

If you start an interactive shell (like opening a new terminal window), you’ll see:

% echo $MY_VAR
set in zshrc

But if you run a non-interactive script that sources your Zsh configuration, like this:

test_script.zsh:

#!/bin/zsh
source ~/.zshrc
echo $MY_VAR

And you run it:

% zsh test_script.zsh
set in zshrc

Now, let’s swap them:

~/.zshenv:

export MY_VAR="set in zshenv"

~/.zshrc:

export MY_VAR="set in zshrc"

And run the same test script:

% zsh test_script.zsh
set in zshrc

This is because zshrc is explicitly sourced by the shell for interactive sessions after zshenv. The core mental model here is that Zsh has a hierarchy, but also a "first-writer-wins" rule for environment variables that can override this hierarchy.

Here’s the full picture of Zsh startup files and their typical order:

  1. zshenv: This is the first file sourced for all Zsh invocations (interactive, non-interactive, login, non-login). It’s primarily for setting environment variables that should be available everywhere. If you set MY_VAR="global" here, it will be set.

  2. zprofile: Sourced for login shells. This is where you’d typically put things like PATH modifications or other settings that should only apply when you first log in.

  3. zshrc: Sourced for interactive shells. This is the workhorse for your daily terminal use – aliases, functions, prompt customization, etc. Crucially, it’s sourced after zprofile for login interactive shells.

  4. zlogin: Sourced for login shells, after zshrc. Less commonly used than zprofile.

  5. zlogout: Sourced when a login shell exits.

The key distinction is between login and non-login shells, and interactive versus non-interactive shells.

  • Login shell: You get this when you log in directly (e.g., via SSH or login command). It sources zshenv, then zprofile, then zshrc (if interactive), then zlogin (if interactive).
  • Non-login interactive shell: This is typically a new terminal window you open after you’ve already logged in. It sources zshenv, then zshrc.
  • Non-interactive shell: This is a script run with zsh script.zsh or sh script.zsh (if script.zsh has #!/bin/zsh). It sources zshenv. If the script explicitly sources zshrc, then zshrc will be read.

The "first writer wins" behavior is most prominent with environment variables (export VAR=value). If zshenv exports MY_VAR="global" and zshrc exports MY_VAR="interactive", the zshrc value will overwrite the zshenv value in an interactive shell because zshrc is read later. However, if a script explicitly sources zshenv after zshrc, the zshenv value will win. This is why you need to be careful about explicit source commands in scripts.

The typical use case for zshenv is setting fundamental environment variables like PATH, LANG, or EDITOR that need to be consistent across all shell types. zprofile is for login-specific setup, and zshrc is for interactive session customization.

One thing most people don’t know is how Zsh handles PATH specifically. While export PATH=/new/path:$PATH in zshenv will set the PATH, if you then do export PATH=/another/path:$PATH in zshrc, the zshrc value will prevail for interactive shells. However, if you have a script that only sources zshenv, that PATH will be the only one set. This means you often see PATH modifications duplicated or carefully ordered across zshenv and zshrc to ensure consistency for interactive shells while respecting the minimal environment for non-interactive ones.

The next concept you’ll likely grapple with is how to manage these files effectively, especially when dealing with different environments or projects, leading you to tools like direnv.

Want structured learning?

Take the full Zsh course →