This repo uses Zsh’s startup file system to keep always-loaded exports fast and universal, while keeping interactive UX (plugins, prompt, keybinds) isolated to interactive shells.
Zsh loads different files depending on whether the shell is:
- interactive (
zsh -i) - login (
zsh -l) - non-interactive (
zsh -c '...')
At a high level (user-level files):
| Shell type | Loaded files |
|---|---|
| non-interactive | .zshenv |
| interactive | .zshenv → .zshrc |
| login (non-interactive) | .zshenv → .zprofile |
| login + interactive | .zshenv → .zprofile → .zshrc |
Notes:
zsh -fdisables startup files entirely..zlogin/.zlogoutexist, but this repo doesn’t rely on them.
This repo stores Zsh config under an XDG-style directory:
$HOME/.config/zsh
Zsh uses $ZDOTDIR to locate startup files. The catch is:
.zshenvis loaded first- if you set
ZDOTDIRinside~/.zshenv, Zsh will not “restart” and load$ZDOTDIR/.zshenvautomatically
So the common solution is to keep a tiny ~/.zshenv in $HOME that:
- exports
ZDOTDIR - explicitly sources
$ZDOTDIR/.zshenv
Example (matches the intent of this repo’s setup):
export ZDOTDIR="$HOME/.config/zsh"
if [[ -n "${ZDOTDIR-}" && "$ZDOTDIR" != "$HOME" && -r "$ZDOTDIR/.zshenv" ]]; then
source "$ZDOTDIR/.zshenv"
fiIf you instead export ZDOTDIR before launching Zsh (e.g. via your OS environment), Zsh will
directly load $ZDOTDIR/.zshenv and won’t read ~/.zshenv at all.
Purpose: exports only and fast.
What it does:
- sources
scripts/_internal/paths.exports.zsh - defines core
ZSH_*directory exports (ZSH_CACHE_DIR,ZSH_SCRIPT_DIR, etc.) - defines
HISTFILEundercache/ - sets a minimal, deduplicated
PATH(viatypeset -U path PATH) including:- Homebrew (
/opt/homebrew/bin,/opt/homebrew/sbin) when present - GNU “gnubin” shims (e.g.
coreutilsforshuf) when present - user bins (
$HOME/bin,$HOME/.local/bin)
- Homebrew (
What does not belong here:
- plugin loading
compinit- prompt/terminal UI
- network calls (
curl,git, etc.)
This file runs in places you may not expect (e.g. zsh -c, fzf preview subshells, editor tasks), so
keep it quiet and predictable.
Purpose: login-session environment (one-time-ish setup).
What it does:
- runs
brew shellenvwhen Homebrew exists, which configures more than justPATH(e.g.MANPATH) - sets
HOMEBREW_AUTO_UPDATE_SECS=604800(7 days)
Why this is login-only:
- it’s slightly heavier than just adding
/opt/homebrew/bintoPATH - many tools don’t need the full Homebrew environment in non-login shells
This repo still ensures brew is discoverable in non-login shells via
scripts/_internal/paths.exports.zsh.
Purpose: interactive UX + modular boot flow.
What it does:
- Ensures
scripts/_internal/paths.exports.zsh+scripts/_internal/paths.init.zshare loaded (with a fallback for manual sourcing). - Ensures cached CLI wrappers exist and prepends
$ZSH_CACHE_DIR/wrappers/bintopathso that subshells can use wrapper commands without loading the whole config. - Configures history behavior and a few boot flags (
ZSH_DEBUG,ZSH_BOOT_WEATHER_ENABLED,ZSH_BOOT_QUOTE_ENABLED). - Optionally shows the login banner (weather + quote).
- Sources
bootstrap/bootstrap.zsh, which loads the rest of the repo modules underscripts/. - Optionally prints the enabled feature list when
ZSH_BOOT_FEATURES_ENABLED=true(default: false).
ZSH_DEBUG is a numeric verbosity level for interactive startup diagnostics.
Levels:
0(default): quiet boot (no per-file timing output)1: prints✅ Loaded <label> in <N>msfor each sourced bootstrap/module file2: adds🔍 Loading: <path>and script-group headers3: also prints the full collected/filtered script lists for each group
Notes:
- Levels are additive (higher levels include lower-level output).
- Migration: if you previously used
ZSH_DEBUG=0to see timings, useZSH_DEBUG=1now. - Some modules treat
ZSH_DEBUG>=2as "show warnings" (e.g. feature loader, docker completion). codex-rate-limits -dis roughly equivalent toZSH_DEBUG>=2for keeping stderr / per-account errors.
Related:
ZSH_BOOT_FEATURES_ENABLED=trueshows the one-line enabled feature summary at startup (TTY only).
Quick example:
ZSH_BOOT_WEATHER_ENABLED=false ZSH_BOOT_QUOTE_ENABLED=false ZSH_DEBUG=1 zsh -i -c 'exit'Non-interactive shells should still see the exported paths and core tools:
env -i HOME="$HOME" zsh -c 'print -r -- "$ZDOTDIR"; print -r -- "$ZSH_CACHE_DIR"; print -r -- "$HISTFILE"; command -v brew; command -v shuf'Interactive non-login shells (common in GUI apps like VS Code) should still find Homebrew tools:
env -i HOME="$HOME" ZSH_BOOT_WEATHER_ENABLED=false ZSH_BOOT_QUOTE_ENABLED=false zsh -i -c 'print -r -- "$HISTFILE"; command -v brew; command -v shuf; exit'Login + interactive shells should load everything (including .zprofile):
ZSH_BOOT_WEATHER_ENABLED=false ZSH_BOOT_QUOTE_ENABLED=false zsh -il -c 'print -r -- "login=$options[login] interactive=$options[interactive]"; exit'-
“Why doesn’t
.zprofilerun in VS Code?”
VS Code typically spawns a non-login interactive shell. Usezsh -l, or configure the terminal to start login shells. -
“Why is
brewmissing?”
brew shellenvruns only in login shells here. The fallback path is handled inscripts/_internal/paths.exports.zsh(make sure/opt/homebrew/binexists on your machine). -
“Why is history writing to
$ZDOTDIR/.zsh_history?”
On macOS,/etc/zshrcsetsHISTFILE=${ZDOTDIR:-$HOME}/.zsh_historyfor interactive shells. This repo re-assertsHISTFILEunder$ZSH_CACHE_DIRin$ZDOTDIR/.zshrc. -
“Why keep
.zshenvso minimal?”
Because it runs in non-interactive contexts and must not produce output or introduce slow startup.
README.md(Setup section)scripts/_internal/paths.exports.zshscripts/_internal/paths.init.zshbootstrap/bootstrap.zshdocs/guides/login-banner.md