Skip to main content

Managing Zsh Configuration Files

· 4 min read
Calvin Cheng
Shape what gets built and the value it creates.

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:

OrderFileWhen it runs
1.zshenvEvery zsh (interactive, non-interactive, scripts)
2.zprofileLogin shells only (new terminal, SSH)
3.zshrcInteractive shells only
4.zloginLogin shells only (after .zshrc)
5.zlogoutWhen 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:

  • PATH and other env vars (EDITOR, LANG, PAGER)
  • eval "$(brew shellenv)" or equivalent
  • Tool-specific env (e.g. ANDROID_HOME, GOENV_ROOT)
  • Sourcing .profile so 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:

  • PATH modifications 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​