which bash startup file runs, and when
Bash decides which startup files to read from how it was started, and the three cases barely overlap — which is why environment variables keep going missing in exactly the place you didn't test.
Login shell. Started at a console login, by bash -l, or by ssh host with no command. It reads /etc/profile, then the first that exists of ~/.bash_profile, ~/.bash_login, ~/.profile, and stops at the first one it finds. It does not read ~/.bashrc unless one of those files sources it.
Interactive, non-login. A new terminal window or tab on most Linux setups, or plain bash. It reads ~/.bashrc (and, on many distros, /etc/bash.bashrc). It does not touch the profile files.
Non-interactive. Running a script (bash script.sh), or ssh host 'some command'. It reads neither the profile chain nor ~/.bashrc. The only hook is $BASH_ENV: if that's set, bash sources the file it names, and nothing else.
The traps live in the gaps:
- Put
export PATH=...only in~/.bashrc, and it's absent fromssh host cmd, from cron, and from scripts — anything non-interactive. - Put it only in
~/.bash_profile, and it's absent from new non-login interactive shells. This one is platform-dependent: macOS Terminal opens login shells, so.bash_profileworks there; most Linux terminal emulators open non-login interactive shells, so it doesn't. Same dotfiles, opposite outcome. ssh hostruns a login shell (profile chain);ssh host cmdruns a non-interactive shell ($BASH_ENVonly). So something present in your interactive SSH session can vanish the moment you script the same command over SSH.
The convention that sidesteps all of it: keep environment — PATH, exports — in ~/.profile or ~/.bash_profile; keep interactive niceties — aliases, prompt, completion — in ~/.bashrc; and have ~/.bash_profile source ~/.bashrc so a login shell gets both. One place for "always", one for "interactive", and you stop guessing which file a given invocation will read.