← Back to DevBytes

Zsh Scripting: Startup Files Complete Guide

Zsh Scripting: Startup Files Complete Guide

Zsh (Z shell) is one of the most powerful and customizable shells available for Unix-like systems. One of its greatest strengths lies in its flexible startup file system, which allows you to configure behavior for different types of shell sessions. Understanding these files is essential for any developer who wants to automate workflows, manage environment variables, define aliases, and keep a clean, portable configuration across machines.

This guide walks through every Zsh startup file, explains when each one is sourced, shows practical examples, and ends with best practices you can adopt immediately.

Why Zsh Startup Files Matter

Every time you open a terminal, SSH into a server, or run a script, Zsh goes through a specific sequence of file reads. Each file has a distinct purpose:

Misplacing configuration in the wrong file is a common source of bugs — for example, defining an alias in .zshenv means it is loaded even by scripts that never display a prompt, wasting time and potentially breaking automation.

The Zsh Startup File Order

Zsh reads files in a specific order depending on the type of shell being launched. The five user-level files, in order of execution, are:

  1. ~/.zshenv — Always sourced, for every shell.
  2. ~/.zprofile — Sourced only by login shells, after .zshenv.
  3. ~/.zshrc — Sourced by interactive shells, after .zprofile.
  4. ~/.zlogin — Sourced by login shells, after .zshrc.
  5. ~/.zlogout — Sourced by login shells, at exit.

There are also global equivalents in /etc/ that are sourced before the user versions: /etc/zshenv, /etc/zprofile, /etc/zshrc, /etc/zlogin, and /etc/zlogout. These are typically managed by your operating system or distribution.

Understanding Shell Types

To know which files will be loaded, you must understand the three main shell types:

A shell can be both login and interactive (like opening a terminal in macOS), login but non-interactive (like ssh user@host 'command'), or interactive but not a login shell (like launching zsh from within an existing shell).

File-by-File Breakdown

1. ~/.zshenv — The Universal File

.zshenv is sourced by every Zsh invocation, including scripts, cron jobs, and subshells. Because of this, it must be fast and free of any output. This is the right place for environment variables that every process needs.

Keep it minimal. Avoid aliases, functions, prompt setup, or anything that produces output. Loading a heavy framework here will slow down every script you run.

# ~/.zshenv

# Essential environment variables
export EDITOR="nvim"
export VISUAL="$EDITOR"
export PAGER="less"
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"

# Extend PATH with user-local binaries
typeset -U path
path=(
  $HOME/.local/bin
  $HOME/.cargo/bin
  $HOME/.npm-global/bin
  $path
)

# Set default umask for all processes
umask 022

The typeset -U path line is a Zsh-specific trick that keeps the path array free of duplicates automatically. Note that path (lowercase) is tied to PATH (uppercase), so modifying the array updates the environment variable.

2. ~/.zprofile — Login Shell Setup

.zprofile is sourced only by login shells, right after .zshenv. It is the Zsh equivalent of Bash's .bash_profile. Use it for tasks that should happen once per session, such as starting a background agent, printing a welcome message, or running commands that depend on a TTY.

# ~/.zprofile

# Start ssh-agent only for login shells
if [[ -z "$SSH_AUTH_SOCK" ]]; then
  eval "$(ssh-agent -s)" >/dev/null
  ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi

# Print system information on login
echo "Welcome back, $USER!"
echo "System: $(uname -sr)"
echo "Uptime: $(uptime -p)"
echo

# Run keychain for git signing
if command -v keychain >/dev/null 2>&1; then
  eval "$(keychain --eval --quiet id_ed25519)"
fi

Because .zprofile runs only once at login, it is ideal for expensive operations that do not need to repeat in every new terminal tab.

3. ~/.zshrc — The Interactive Workhorse

.zshrc is the most commonly edited file. It is sourced by interactive shells after .zprofile. This is where you define aliases, functions, prompt themes, completions, history settings, and load frameworks like Oh My Zsh or Prezto.

# ~/.zshrc

# History settings
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt APPEND_HISTORY
setopt SHARE_HISTORY
setopt HIST_IGNORE_DUPS
setopt HIST_IGNORE_SPACE
setopt HIST_VERIFY
setopt EXTENDED_HISTORY

# Useful options
setopt AUTO_CD
setopt CORRECT
setopt NO_BEEP
setopt INTERACTIVE_COMMENTS

# Aliases
alias ll='ls -lah'
alias gs='git status'
alias gd='git diff'
alias gp='git push'
alias gl='git log --oneline --graph --decorate -20'
alias ..='cd ..'
alias ...='cd ../..'
alias reload='source ~/.zshrc'

# Functions
mkcd() {
  mkdir -p "$1" && cd "$1"
}

extract() {
  case "$1" in
    *.tar.gz|*.tgz) tar xzf "$1" ;;
    *.tar.bz2|*.tbz2) tar xjf "$1" ;;
    *.zip) unzip "$1" ;;
    *.rar) unrar x "$1" ;;
    *) echo "Unknown archive format: $1" ;;
  esac
}

# Load completion system
autoload -Uz compinit && compinit
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

# Prompt (simple two-line prompt)
autoload -Uz colors && colors
PROMPT='%F{cyan}%~%f %F{green}%#%f '
RPROMPT='%F{yellow}[%?]%f'

This is also the right place to load third-party plugins. If you use a framework, it typically manages this file for you, but understanding what it does helps you debug issues.

4. ~/.zlogin — Post-Interactive Login Setup

.zlogin is sourced by login shells, but after .zshrc. This makes it useful for tasks that depend on interactive configuration already being loaded. Most users do not need this file, and many setups leave it empty or omit it entirely.

# ~/.zlogin

# Start a tmux session automatically on remote login
if [[ -n "$SSH_CONNECTION" ]] && [[ -z "$TMUX" ]]; then
  tmux attach-session -t default || tmux new-session -s default
fi

A common pattern is to use either .zprofile or .zlogin, not both, to avoid confusion. The difference is timing: .zprofile runs before .zshrc, while .zlogin runs after.

5. ~/.zlogout — Login Shell Cleanup

.zlogout is sourced when a login shell exits. Use it to clean up temporary files, kill background processes, or clear the screen.

# ~/.zlogout

# Clear the screen on exit
clear

# Remove temporary files created during the session
rm -f /tmp/$USER-session-* 2>/dev/null

# Stop ssh-agent if we started it
if [[ -n "$SSH_AGENT_PID" ]]; then
  eval "$(ssh-agent -k)" 2>/dev/null
fi

Putting It All Together: A Complete Example

Here is a minimal, well-organized set of startup files you can use as a starting point. Together they demonstrate the separation of concerns that makes Zsh configuration maintainable.

.zshenv

# ~/.zshenv — sourced by every shell
export EDITOR="nvim"
export PAGER="less"
export LANG="en_US.UTF-8"

typeset -U path
path=($HOME/.local/bin $HOME/.cargo/bin $path)

.zprofile

# ~/.zprofile — login shells only, before .zshrc
if [[ -z "$SSH_AUTH_SOCK" ]]; then
  eval "$(ssh-agent -s)" >/dev/null
fi

.zshrc

# ~/.zshrc — interactive shells only
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt SHARE_HISTORY HIST_IGNORE_DUPS

alias ll='ls -lah'
alias gs='git status'

autoload -Uz compinit && compinit
PROMPT='%F{cyan}%~%f %# '

.zlogin

# ~/.zlogin — login shells, after .zshrc
if [[ -n "$SSH_CONNECTION" ]] && [[ -z "$TMUX" ]]; then
  tmux attach || tmux new
fi

.zlogout

# ~/.zlogout — login shell exit
clear

Debugging Startup File Execution

If you are unsure which files are being loaded or in what order, you can trace execution by adding echo statements temporarily, or by running Zsh with verbose flags.

# Trace which files are sourced
zsh -xlic exit 2>&1 | head -50

# Show the path Zsh searches for startup files
echo $fpath

# Check if current shell is interactive
[[ -o interactive ]] && echo "interactive" || echo "non-interactive"

# Check if current shell is a login shell
[[ -o login ]] && echo "login" || echo "not login"

Another useful technique is to add a marker line at the top of each file:

echo "Sourced ~/.zshenv" >&2

This lets you see the exact order in which files load when you start a new shell.

Best Practices

Example: Machine-Specific Overrides

# At the end of ~/.zshrc
if [[ -f ~/.zshrc.local ]]; then
  source ~/.zshrc.local
fi

This pattern lets you keep shared configuration in version control while allowing each machine to override or extend settings locally without conflicts.

Conclusion

Zsh's startup file system may seem complex at first, but it follows a clear and logical design. By placing each piece of configuration in the correct file — environment variables in .zshenv, login-time setup in .zprofile or .zlogin, interactive features in .zshrc, and cleanup in .zlogout — you gain precise control over how your shell behaves in every context. This separation keeps your configuration fast, portable, and maintainable. Once you internalize the file order and the shell types that trigger each one, you can build a dotfiles setup that works reliably across laptops, servers, and containers alike. Start simple, version-control your files, and refine them over time as your workflow evolves.

— Ad —

Google AdSense will appear here after approval

← Back to all articles