Shell Initialisation & Loading Order¶
This page explains which shell config files run when, and why. Understanding this is essential for knowing where to put environment variables, aliases, and tool initialisations — and for debugging "command not found" issues.
For how this repo uses these concepts, see the Repository Mapping and Shell Modules pages.
Shell Types¶
Shells have two independent properties — login vs non-login and interactive vs non-interactive — that combine:
| Combination | When | Example |
|---|---|---|
| Login + Interactive | macOS terminal windows, SSH | ssh user@host, opening Terminal.app |
| Non-login + Interactive | Linux terminal windows | Opening GNOME Terminal (login already happened at GDM) |
| Non-login + Non-interactive | Script execution | ./script.sh, bash -c "..." |
The key distinction: macOS terminals default to login + interactive (every new window), while Linux terminals default to non-login + interactive (login happened at GDM/SDDM). This affects which config files are read.
Zsh Loading Order¶
This repo uses zsh as the primary shell. The loading order determines where to put things:
| File | Scope | What goes here |
|---|---|---|
.zshenv |
Always (every invocation, including scripts) | $PATH, Homebrew, environment variables. Use a load guard to avoid redundant work in subshells. |
.zprofile |
Login only | Re-runs brew shellenv to survive path_helper reordering (see macOS). Other login-shell setup lives here. |
.zshrc |
Interactive only | Aliases, prompts, completion, keybindings. This repo uses a modular .zshrc.d/ pattern instead of editing .zshrc directly. |
.zlogin |
Login only | Final setup after initialisation. Not used by this repo. |
Each user file has a system-wide counterpart (/etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin) that runs
before the user file.
Loading sequences:
-
Login Shell (macOS default):
/etc/zshenv→~/.zshenv→/etc/zprofile→~/.zprofile→/etc/zshrc→~/.zshrc→/etc/zlogin→~/.zlogin
-
Interactive Non-Login Shell (Linux terminal default):
/etc/zshenv→~/.zshenv→/etc/zshrc→~/.zshrc
-
Script (Non-Interactive):
/etc/zshenv→~/.zshenv
Load Guard Pattern¶
Because ~/.zshenv runs for every zsh invocation (including subshells and scripts), it should use a guard variable to
avoid redundant work:
# Prevent double loading in subshells that inherit the environment
if [[ -n "$__ZSHENV_LOADED" ]]; then
return
fi
export __ZSHENV_LOADED=1
The return statement is safe here because ~/.zshenv is always sourced (never executed directly), so return exits
the sourced file without terminating the shell.
Bash Loading Order¶
Included for reference — this repo uses zsh, but understanding bash loading is useful for debugging scripts and login shells.
| File | Scope | Purpose |
|---|---|---|
.bash_profile |
Login only | Bash reads only the first file it finds: |
~/.bash_profile, ~/.bash_login, then ~/.profile. |
||
.bashrc |
Interactive only | Aliases, prompts, shell options. Not read by login shells |
unless explicitly sourced from .bash_profile. |
||
.profile |
Login only | Generic POSIX login config. Only read if .bash_profile and |
.bash_login don't exist. |
Loading sequences:
-
Login Shell (macOS default):
/etc/profile→ first of~/.bash_profile/~/.bash_login/~/.profile
-
Interactive Non-Login Shell (Linux terminal default):
/etc/bash.bashrc→~/.bashrc
-
Script (Non-Interactive):
$BASH_ENV(if set, sources the file it points to; otherwise nothing)
Note: Bash login shells do not read ~/.bashrc by default. Most users source ~/.bashrc inside their
~/.bash_profile to ensure consistency.
macOS vs. Linux¶
macOS¶
- macOS terminals (Terminal.app, iTerm2, Ghostty) launch every new window as a login shell.
- This means
~/.zprofileand~/.zshrcare both read for every window. path_helper: macOS runs/usr/libexec/path_helperfrom/etc/zprofile(since Catalina, when zsh became the default shell). It reads/etc/pathsand/etc/paths.d/*, builds aPATHwith system paths first, then appends any existingPATHentries. The effect: Homebrew bins that~/.zshenvplaced at the front of$PATHget pushed behind/usr/bin,/bin, etc.- How the dotfiles handle this:
~/.zshenvsets up Homebrew viabrew shellenv(essential for non-login shells and scripts, which never see/etc/zprofile). For login shells,~/.zprofilere-runsbrew shellenv— it fires after/etc/zprofile, so its prepend wins overpath_helper's reorder. The duplication is intentional: each copy serves a different shell type. See the comment block at the top ofhome/dot_zprofile.tmplfor the canonical explanation alongside the implementation.
Linux¶
- Desktop environments (GNOME, KDE) treat the session login as the login shell. Terminal windows start non-login + interactive shells.
- This means
~/.zprofileis skipped for terminal sessions, and many display managers source~/.profilebut not~/.zprofile. This is why environment variables belong in~/.zshenv.
Repository Mapping¶
How the shell concepts above map to actual files in this repo:
| Concept | Repo File | Notes |
|---|---|---|
~/.zshenv |
dot_zshenv.tmpl |
$PATH, Homebrew init (for non-login shells / scripts), |
| environment variables, load guard | ||
~/.zprofile |
dot_zprofile.tmpl |
Re-runs Homebrew init to survive path_helper's reorder |
| on login shells. See macOS. | ||
~/.zshrc modules |
dot_zshrc.d/*.zsh |
Modular configs sourced via loop |
| Bootstrap | .chezmoiscripts/run_once_before_01-setup-zshrc-sourcing.sh.tmpl |
Appends .zshrc.d sourcing block |
to ~/.zshrc |
||
| External plugins | .chezmoiexternals/externals.toml.tmpl |
Oh My Zsh, zsh plugins, Nerd Fonts |