Managing Zsh Configuration Files
If you've ever wondered why your terminal is slow to start, or why a command works in one context but not another, the answer often lies in how zsh loads its configuration. Zsh has four main config files, each run under different conditions - and putting things in the wrong place leads to confusion, sluggish startup, and broken scripts.
Why It Matters
Performance. Heavy setup (version managers, completion systems, plugins) runs on every shell start. If it lives in .zshrc, it runs every time you open a tab. If it's in .zprofile, it runs once per login. If it's in .zshenv, it runs even when you invoke zsh -c 'some script'. The wrong placement can easily add hundreds of milliseconds - or more - to every shell.
Correctness. Non-interactive shells (e.g. zsh -c 'node app.js') don't load .zshrc or .zprofile. They only load .zshenv. If your PATH is defined in .zshrc, that script won't see it. Build tools, launchers, and IDEs that spawn shells with -c will fail in subtle ways.
Maintainability. A clear split - env in one place, interactive behavior in another - makes it obvious where to put new config and keeps installers from cluttering the wrong files.
The Loading Order
Zsh reads config files in this sequence:
| Order | File | When it runs |
|---|---|---|
| 1 | .zshenv | Every zsh (interactive, non-interactive, scripts) |
| 2 | .zprofile | Login shells only (new terminal, SSH) |
| 3 | .zshrc | Interactive shells only |
| 4 | .zlogin | Login shells only (after .zshrc) |
| 5 | .zlogout | When exiting a login shell |
For most setups, .zlogin and .zlogout are optional. The core split is:
- Env →
.zshenv - Login-only →
.zprofile - Interactive →
.zshrc
What Goes Where
.zshenv - Environment for everything
Use this for anything that must exist in all zsh invocations, including scripts:
PATHand other env vars (EDITOR,LANG,PAGER)eval "$(brew shellenv)"or equivalent- Tool-specific env (e.g.
ANDROID_HOME,GOENV_ROOT) - Sourcing
.profileso zsh picks up POSIX env (see below)
Keep it minimal: no aliases, prompts, or heavy logic. This file runs even for zsh -c 'true', so avoid anything that breaks non-interactive use. The Unix Stack Exchange answer on this topic is a solid reference.
.zprofile - Login shells only
Use for setup that should run once per login session:
PATHmodifications that are expensive- Login-only integrations (e.g. starting ssh-agent)
- Anything that should run once when you open a terminal, not on every subshell
Many setups leave .zprofile almost empty if most env is already in .zshenv.
.zshrc - Interactive shells only
Use for everything that only makes sense when you're typing:
- Prompt configuration (
PROMPT,vcs_info, themes) - Aliases and helper functions
- Completions (
compinit), key bindings, plugins - Version manager init that modifies interactive behavior (e.g.
fnm env,goenv init)
The .profile Question
.profile is a POSIX convention for login shells. Zsh does not read .profile. Bash and sh do. So if rustup, pyenv, or another installer adds source ~/.cargo/env to .profile, zsh will never see it.
Recommended: Source .profile from .zshenv:
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
That keeps .profile as the single place installers append to, while ensuring zsh (and any scripts it runs) gets the same env as bash. See the Mac Terminal guide for more on the zsh/bash split.
A Simple Rule of Thumb
- Needed by scripts or non-interactive commands? →
.zshenv - Only at login, not every subshell? →
.zprofile - Only when you're typing at a prompt? →
.zshrc
Further Reading
- What should go in .zshenv, .zshrc, .zprofile? - the canonical Stack Exchange answer
- Zsh Configuration Files - Baeldung's overview
- Mac Terminal: .zshrc vs .zprofile - macOS-specific guidance
