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.
Login and interactive
Two yes-or-no answers pick which files a shell reads. A login shell is 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 one. Interactive means a shell you type into, as opposed to one running a script.
Every shell is some combination of those two flags, and 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 mostly follows the same rule, with one Debian and Ubuntu wrinkle worth knowing. ssh host opens an interactive login shell, so it reads the profile chain. ssh host somecommand is non-interactive, and upstream bash would read only $BASH_ENV. Debian and Ubuntu build bash with SSH_SOURCE_BASHRC, though, so that shell does source ~/.bashrc when it spots an SSH connection. You still get nothing out of it, because the stock Debian ~/.bashrc returns early when the shell is non-interactive. Same outcome as upstream, different reason, and either way it 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. - Keep aliases, functions, the prompt, and key bindings 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 wires it
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 one directory from the login files for your shell, so there is a single place everything loads from. Hand-maintaining that source chain across bash and zsh, login and non-login, Ubuntu and macOS is exactly the fiddly, easy-to-get-wrong work a provisioner should own for you.
Under zsh, the framework is sourced from both ~/.zprofile (login) and ~/.zshrc (interactive), with a load-once guard so the second source is a no-op. That covers every interactive zsh, login or not.
Under bash it is sourced from ~/.profile, and the bash role deletes ~/.bash_profile to make that work. That deletion is load-bearing rather than housekeeping: by the precedence rule above, a login bash reads the first of ~/.bash_profile, ~/.bash_login, ~/.profile, so a pre-existing ~/.bash_profile would shadow ~/.profile and the framework would never load. Removing it lets a login bash fall through to the file the framework is actually wired into.
Where the bash coverage stops
So the honest coverage is: every interactive zsh, and every login bash, including console logins and interactive SSH sessions, the same on Ubuntu and macOS. A non-login interactive bash is the gap. That shell reads only ~/.bashrc, nothing in the collection writes ~/.bashrc, and your aliases really are missing there. The bash table two screens up already proves it. No trick gets around it either; the framework is subject to exactly the same rule as any hand-written config.
In practice it bites when a terminal profile is set to run a plain shell rather than a login shell, since most emulators and every interactive SSH session start a login shell by default. The fixes are the ordinary ones: point the terminal's profile at a login shell, or run bash -l for a one-off. Under zsh the question never comes up, because ~/.zshrc is one of the sourced files.
The shell configuration pattern covers how the os -> unix -> shell dispatch picks those files.
Checking which shell you are in
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.