skip to content

Configuration and Frameworks

How a zsh session is assembled: which of the startup files runs when, and what oh-my-zsh or a plugin manager layers on top. Interviewers ask because slow, mysterious shells almost always trace back to this load order.

part ofCommand-line shellsoverview, primer and where to startread it →
on this pageshow

questions

6

Name zsh's user startup files in the order they are read, and say which of them are read by a login shell, by an interactive non-login shell, and by a shell that is only running a single command.

level: middleimportance: must knowfreq 70%

answer

  1. two flags: login and interactive
  2. env, profile, rc, login
  3. only one file runs for every shell
  4. rc means interactive
  5. zlogin comes after zshrc

basics

~10 s

zsh always reads ~/.zshenv; ~/.zprofile and ~/.zlogin are read only by login shells, and ~/.zshrc only by interactive ones. The order is .zshenv, .zprofile, .zshrc, .zlogin, with .zlogout on logout of a login shell.

solid answer

~40 s

zsh reads up to five user files, and two independent properties of the shell decide which ones apply. `~/.zshenv` is read by *every* zsh, interactive or not, login or not. `~/.zprofile` and `~/.zlogin` are read only by login shells; `~/.zshrc` only by interactive ones. The order is `.zshenv` → `.zprofile` → `.zshrc` → `.zlogin`, so `.zlogin` deliberately runs *after* your interactive configuration, and `.zlogout` runs when a login shell exits. In practice: an interactive login shell reads four files, a new terminal tab inside an existing session reads two (`.zshenv` and `.zshrc`), and `ssh host 'somecmd'` reads only `.zshenv`. Each file has an `/etc` counterpart that is read first, and if `ZDOTDIR` is set the user files are looked for there instead of `$HOME`.

code

bash · 8 lines
bash
for f in ~/.zshenv ~/.zprofile ~/.zshrc ~/.zlogin; do
  echo "echo loaded: $f >&2" >> "$f"
done

zsh -c 'true'          # non-interactive, non-login: .zshenv only
zsh -i -c exit         # interactive:                .zshenv .zshrc
zsh -l -i -c exit      # login + interactive:        all four
zsh -f -i -c exit      # skips the user startup files entirely

go deeper

for a junior

Be able to name ~/.zshenv, ~/.zprofile, ~/.zshrc and ~/.zlogin, and say plainly that aliases and prompt settings belong in ~/.zshrc.

for a middle

Explain the two independent flags — login and interactive — and walk the read order out loud, including the fact that ~/.zlogin runs after ~/.zshrc rather than before it.

for a senior

Show that you use the split deliberately: environment every process needs in ~/.zshenv, interactive-only work in ~/.zshrc, and be ready to prove on a broken host which files actually ran.

for a principal

Own the convention for a team: what belongs in the machine-wide /etc files, whether dotfiles relocate via ZDOTDIR, and how the always-read file is kept cheap and safe for everything that is not a terminal.

## The five files zsh has an unusually rich set of startup files, and each one exists because it answers a different question about *when* configuration should apply. - **`~/.zshenv`** — read by every single zsh invocation. Interactive or not, login or not, script or prompt. This is the only unconditional file. - **`~/.zprofile`** — read by login shells only, before `~/.zshrc`. - **`~/.zshrc`** — read by interactive shells only. - **`~/.zlogin`** — read by login shells only, after `~/.zshrc`. - **`~/.zlogout`** — run when a *login* shell exits. ## The two flags that decide everything "Login" and "interactive" are independent properties, not points on a scale. A shell is **interactive** when it reads commands from a terminal — you type at it. `zsh -i` forces it. A shell is a **login** shell when it is the first shell of a session: the shell `sshd` gives you on an interactive connection, a console login, or anything started with `zsh -l` or with a leading `-` in `argv[0]`. Terminal emulators differ: many open a plain interactive shell, while some are configured to open a login shell for each tab. The four combinations: | Shell | Files read | |---|---| | interactive login (`ssh host`, console login) | `.zshenv`, `.zprofile`, `.zshrc`, `.zlogin` | | interactive non-login (a new tab or pane) | `.zshenv`, `.zshrc` | | non-interactive login (rare; `zsh -l -c cmd`) | `.zshenv`, `.zprofile`, `.zlogin` | | non-interactive non-login (`ssh host 'cmd'`, a script) | `.zshenv` | ## Why `.zlogin` exists when `.zprofile` already runs The two login files bracket `.zshrc` on purpose. `.zprofile` is there for people who like the ksh ordering — environment set up before the interactive configuration. `.zlogin` is the csh-style hook and runs last, which makes it the right place for anything that must see the finished interactive shell: a session banner, a message of the day, starting something once per login. ## The `/etc` counterparts and `ZDOTDIR` Every user file has a system-wide sibling that is read first: `/etc/zshenv`, `/etc/zprofile`, `/etc/zshrc`, `/etc/zlogin`, `/etc/zlogout` (some distributions place them under `/etc/zsh/`). This is how a machine imposes defaults on everyone, and it is worth checking when a shell behaves oddly on one host only. If the `ZDOTDIR` parameter is set, zsh looks for the user files in `$ZDOTDIR` rather than `$HOME`. Because `ZDOTDIR` has to be set before the files can be found, it is normally set in `/etc/zshenv` or inherited from the environment — or `$HOME/.zshenv` sets it and the remaining files are then read from the new location. It is the clean way to keep dotfiles out of `$HOME` or to test an alternative configuration without touching your own. ## Deciding what goes where The split is not decoration; it is what makes a shell configuration survive being run by something that is not you. - **`~/.zshenv`**: environment that *any* zsh process needs — `PATH` entries that scripts and remote commands depend on, `EDITOR`. Keep it small, silent and fast, because it runs for everything. - **`~/.zshrc`**: everything that only makes sense with a human at the keyboard — aliases, key bindings, prompt, completion setup, plugin managers, options that change line editing. - **`~/.zprofile`** / **`~/.zlogin`**: once-per-session work — starting an agent, printing a summary, anything you do not want repeated for every subshell. ## Skipping the files `zsh -f` (also `--no-rcs`) starts a shell that ignores the startup files, which is the fastest way to prove a problem comes from your own configuration. `zsh -d` skips only the global `/etc` files. `/etc/zshenv` is always read regardless, so a machine-wide policy there cannot be opted out of. ## Proving it rather than remembering it When a host behaves unexpectedly, do not reason about which file ran — instrument them. Append a line to each file that prints its own name to stderr, then start shells in each mode and read the output. Five minutes of that ends most arguments about why an alias or a `PATH` entry is missing. ```zsh # in ~/.zshrc print -u2 "loaded: ~/.zshrc" ``` Remove the markers afterwards — output from a startup file has its own failure modes.

  • What does setting ZDOTDIR change?
    If `ZDOTDIR` is set, zsh looks for `.zshenv`, `.zprofile`, `.zshrc`, `.zlogin` and `.zlogout` in that directory instead of `$HOME`. It is usually set in `/etc/zshenv` or inherited from the environment, or `$HOME/.zshenv` sets it and the remaining files are read from the new location. Useful for keeping dotfiles out of `$HOME` and for running an isolated configuration.
  • Why does .zlogin exist when .zprofile already runs only for login shells?
    They bracket `.zshrc` deliberately. `.zprofile` is the ksh-style hook and runs before the interactive configuration; `.zlogin` is the csh-style hook and runs after it. Put things that must see the finished interactive shell — a banner, a once-per-login summary — in `.zlogin`, and environment setup in `.zprofile`.
  • How do you start a zsh that ignores this configuration entirely?
    `zsh -f` (`--no-rcs`) skips the startup files, which is the standard baseline when you suspect your own configuration. `zsh -d` skips only the global `/etc` files. `/etc/zshenv` is always read, so a site-wide policy placed there still applies.
  • A terminal emulator opens tabs that read ~/.zprofile — is that a zsh behaviour?
    No. zsh reads `.zprofile` because the shell was started *as a login shell*; the emulator chose that, usually via a "run command as a login shell" setting or by invoking `zsh -l`. It explains why two people on the same machine see different files run for a new tab.

Think of them as three concentric jackets: .zshenv is worn by every process that ever becomes a zsh, .zprofile/.zlogin only when you arrive for the session, and .zshrc only when a human is at the keyboard.

saying these in an interview costs you the question

  • Assumes ~/.zshrc runs for every zsh invocation
  • Thinks ~/.zprofile is read by ordinary interactive tabs
  • Believes .zlogin runs before .zshrc
  • Cannot say which file a remote one-shot command reads
  • Puts prompt and alias setup in ~/.zshenv

context

open as a page

A deploy job runs `ssh build@host 'mytool --version'` and fails with `zsh: command not found: mytool`, yet logging into that same account interactively and typing the command works. The PATH entry is exported from ~/.zshrc. Why does the remote command miss it, and where does the export actually belong?

level: seniorimportance: must knowfreq 48%

basics

~20 s

sshd runs a remote command in a non-interactive, non-login zsh, which reads only ~/.zshenv and never ~/.zshrc. Move the PATH export to ~/.zshenv, and keep that file silent and fast because every zsh process runs it.

open as a page

In zsh, what is the `setopt` builtin for, how does it relate to `unsetopt`, and how would you find out which shell options are currently enabled?

level: juniorimportance: should knowfreq 38%

basics

~20 s

setopt turns a named zsh behaviour on and unsetopt turns it off; setopt NO_NAME means the same as unsetopt NAME. Typed with no arguments, setopt lists the options currently on and unsetopt lists those currently off.

open as a page

Opening a new terminal tab takes roughly two seconds before the zsh prompt appears. How would you find out what part of the startup is slow, and what typically turns out to be responsible?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Measure first: time zsh -i -c exit against a zsh -f baseline to confirm the cost is your own configuration, then load the zsh/zprof module at the top of ~/.zshrc and call zprof at the bottom to see which functions consume the time.

open as a page

Your team wants one standard zsh setup across everyone's laptops and the CI images. How would you weigh oh-my-zsh against a plugin manager such as zinit or antidote against a hand-written ~/.zshrc, and what would you standardise regardless of which you pick?

level: principalimportance: should knowfreq 26%

basics

~20 s

Choose by who owns ~/.zshrc and when code loads: oh-my-zsh takes the file over and sources a large bundle eagerly, plugin managers leave you as the author while adding version-pinned dependencies, and a hand-written file is fastest and fully auditable. Standardise the environment contract, not the taste layer.

open as a page

You put `PROMPT="%~ [$(date +%H:%M)] %# "` in your ~/.zshrc and the clock in the zsh prompt is frozen at the time the shell started. Explain what happened, and what has to change for the prompt to re-evaluate that substitution before every command line.

level: middleimportance: nice to knowfreq 30%

basics

~20 s

The double quotes ran the command substitution once, when ~/.zshrc was sourced, so the value was baked into the string. Assign the prompt single-quoted and enable the PROMPT_SUBST option, or use the native %D{%H:%M} prompt escape.

open as a page