Which shell file runs when: the bash and zsh startup order
You add an alias to ~/.bashrc, open a new terminal, and it works. You SSH into the same box, or open a fresh login session, and it is gone. Nothing is broken. A different startup file ran, and your alias was not in it.
The short answer: two flags decide which files run, whether the shell is a login shell and whether it is interactive. A login bash reads ~/.bash_profile and skips ~/.bashrc, so your alias vanishes over SSH unless ~/.bash_profile sources ~/.bashrc; zsh reads ~/.zshenv always and ~/.zshrc when interactive. Placement follows: environment and PATH go in a file every shell reads, aliases and the prompt in the interactive file.
Shell startup order is the source of more "works in one terminal but not another" confusion than almost anything else in a dotfiles setup. It is worth decoding once, and the rest of this post lays out the full order for both shells and the traps inside it.
Two questions decide everything
When a shell starts, which files it reads depends on two yes-or-no questions:
- Login shell? The first shell of a session: a console login, an interactive SSH session, or one started with
--login. Opening a new tab in most terminal emulators is not a login shell. - Interactive? A shell you type into, versus one running a script.
Those two flags pick the files. Bash and zsh answer differently.
Bash
| Shell | Reads, in order |
|---|---|
Login (console, SSH, --login) | /etc/profile, then the first of ~/.bash_profile, ~/.bash_login, ~/.profile |
| Interactive, non-login (new tab) | /etc/bash.bashrc (Debian/Ubuntu), then ~/.bashrc |
| Non-interactive (script) | only the file named in $BASH_ENV, if set |
The classic trap is right there: a login bash does not read ~/.bashrc. Put your aliases there and a console or SSH login never sees them. The fix everyone ends up writing is to source ~/.bashrc from ~/.bash_profile so the two paths converge: [ -f ~/.bashrc ] && . ~/.bashrc.
SSH follows the same rule. ssh host opens an interactive login shell, so it reads the profile chain, while ssh host somecommand is non-interactive and reads only $BASH_ENV. That is why a remote command can run with a PATH your interactive sessions never show.
Zsh
Zsh is more regular, and it always reads zshenv first, in every case:
| Shell | Reads, in order |
|---|---|
| Any shell | /etc/zshenv, ~/.zshenv |
| Login | then /etc/zprofile, ~/.zprofile, then /etc/zshrc, ~/.zshrc, then /etc/zlogin, ~/.zlogin |
| Interactive, non-login | then /etc/zshrc, ~/.zshrc |
| Non-interactive (script) | nothing more than the zshenv pair |
The thing to remember: ~/.zshenv runs for everything, including scripts and non-interactive commands, so keep it small and side-effect free. Interactive niceties (aliases, the prompt, key bindings) belong in ~/.zshrc.
Where to put things
Once you can see the order, placement follows from it:
- Environment variables and
PATHgo in a file that loads for every shell, so a non-interactive command finds them too:~/.zshenvfor zsh, and for bash a file sourced from both~/.bash_profileand~/.bashrc. - Aliases, functions, the prompt, key bindings go in the interactive files (
~/.bashrc,~/.zshrc); a script does not need them.
One footgun while you are in there: never write export PATH=$PATH:/dir when $PATH might be empty. The leading colon it produces (:/dir) puts the current directory on your path. Append to a known value, or guard the variable first.
How Dotfiles Pro sidesteps the maze
Hand-maintaining that source chain across bash and zsh, login and non-login, Ubuntu and macOS, is exactly the kind of fiddly, easy-to-get-wrong work a provisioner should own for you. So Dotfiles Pro does not scatter config across the startup files at all. Every role drops its snippet into ~/.profile.d/, and the unix base role sources that directory from the right login files for your shell:
- For zsh it sources the framework from both
~/.zprofile(login) and~/.zshrc(interactive), with a load-once guard so the second source is a no-op. - For bash it sources from
~/.profile.
Your aliases, functions, environment, and prompt therefore show up in login shells, new-tab shells, and SSH sessions alike, the same on Ubuntu and macOS, because the login files are derived from your shell rather than hand-edited. The "works in one terminal but not another" bug stops happening, because there is one place everything loads from. The shell configuration pattern covers how the os -> unix -> shell dispatch picks those files.
See it for yourself
To find out what kind of shell you are in right now: shopt -q login_shell && echo login in bash, or [[ -o login ]] && echo login in zsh, and [[ $- == *i* ]] && echo interactive in either. When in doubt about whether a file is even being read, drop an echo in it and open a new session of each kind. The startup order stops being a mystery the first time you watch it run.