ZDOTDIR Relocates Zsh Config and Runtime Out of HOME
The Point
After setting ZDOTDIR, zsh reads all startup config files from that directory (instead of $HOME) and writes runtime data (.zsh_history, .zsh_sessions) there too. This means $HOME only needs a single ~/.zshenv as the bootstrap.
Explanation
Zsh’s startup order always reads /etc/zshenv first, then $HOME/.zshenv – this is the only file hardcoded to $HOME. Once ~/.zshenv does export ZDOTDIR=..., all subsequent files (.zprofile / .zshrc / .zlogin) are looked up from $ZDOTDIR instead. At the same time, HISTFILE defaults and macOS’s shell session mechanism both use ${ZDOTDIR:-$HOME} as their base, so history and session restore data also land in ZDOTDIR automatically. The result is that $HOME only has a symlink-level .zshenv, while everything else can be centralized in ~/.config/zsh and managed through dotfiles.
Knowledge Sugar
Three categories of files to distinguish:
- Config files (
.zprofile/.zshrc/conf.d) – go in dotfiles, version controlled. - Runtime data (
.zsh_history,.zsh_sessions/) – machine-specific, may contain sensitive commands, always gitignore. Moving them to ZDOTDIR alongside config is just for tidiness, not for committing. - Bootstrap (
~/.zshenv) – must stay in$HOME. Can itself be a symlink into your dotfiles.
- Config files (
HISTFILE does not automatically follow ZDOTDIR:
HISTFILE’s default is actually~/.zsh_history(bound to$HOME, not ZDOTDIR). To move it, you must explicitly setHISTFILE="$ZDOTDIR/.zsh_history"in conf.d..zsh_sessionsis different – macOS’s/etc/zshrc_Apple_TerminalusesSHELL_SESSION_DIR="${ZDOTDIR:-$HOME}/.zsh_sessions", so it follows ZDOTDIR automatically.Orphan file trap: files like
~/.zprofileand~/.zsh_historygenerated before setting ZDOTDIR become “dead files” – still in$HOMEbut no longer read or written. A typical source is the Homebrew installer, which always appendseval "$(brew shellenv)"to~/.zprofile, but after ZDOTDIR takes effect that file is never sourced. Verify by comparing mtimes of~/.zprofileand$ZDOTDIR/.zprofile.Portability tip: instead of relying on the installer-generated
~/.zprofilefor Homebrew init, write a conditional prefix detection in conf.d:1if [[ -x /opt/homebrew/bin/brew ]]; then 2 eval "$(/opt/homebrew/bin/brew shellenv)" # Apple Silicon 3elif [[ -x /usr/local/bin/brew ]]; then 4 eval "$(/usr/local/bin/brew shellenv)" # Intel 5fiWait, so does that mean…? Since
/etc/zshenvruns before~/.zshenv, a system administrator could technically force-set ZDOTDIR in/etc/zshenv, overriding the user’s choice. This is why security discussions often say/etc/zshenvis the zsh file that should be most trusted and audited.