Skip to content

Which startup file runs

Level: 101 · you put a line in ~/.bashrc, opened a new terminal, and nothing changed

One line: A shell picks its startup files by two questions, whether it is a login shell and whether it is interactive, and each shell answers them with different files. bash reads ~/.bash_profile for a login shell and ~/.bashrc for an interactive one, and a shell that is both reads only ~/.bash_profile. zsh reads ~/.zshenv every time and adds ~/.zprofile and ~/.zshrc as they apply. fish reads config.fish every time and lets the file ask.

Measured

Seven files under a throwaway HOME each print their own name: ~/.bashrc, ~/.bash_profile, ~/.profile, ~/.zshenv, ~/.zprofile, ~/.zshrc and ~/.config/fish/config.fish. Each shell is then started four ways, always with -c true, a command that prints nothing, so every line that appears came from a startup file:

  • -c true alone is the kind of shell a script runs in.
  • -l makes it a login shell, the kind a Mac's Terminal opens (see On a Mac).
  • -i makes it interactive, a shell that expects a person to type.
  • -l -i is both.

stdin is /dev/null, and each shell's stderr is thrown away: an interactive bash with no terminal says it has no job control, and on Linux it also names a process-group number that changes between runs.

Verified output of startup_files_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ cat ~/.bashrc
echo '  ~/.bashrc'

$ bash -c true
  (none)

$ bash -l -c true
  ~/.bash_profile

$ bash -i -c true
  ~/.bashrc

$ bash -l -i -c true
  ~/.bash_profile

$ rm ~/.bash_profile

$ bash -l -i -c true
  ~/.profile

$ cat ~/.bash_profile
echo '  ~/.bash_profile'
[ -r ~/.bashrc ] && . ~/.bashrc

$ bash -l -i -c true
  ~/.bash_profile
  ~/.bashrc

$ bash --norc -i -c true
  (none)

$ bash --noprofile -l -c true
  (none)
  • A script reads none of them. bash -c true printed (none).
  • bash answers each question with one file. The login shell read ~/.bash_profile, the interactive shell read ~/.bashrc, and the shell that is both read only ~/.bash_profile. A line that is only in ~/.bashrc never reaches a login shell.
  • ~/.profile is a fallback. After rm ~/.bash_profile, the same login shell read ~/.profile instead. The bash manual ↗ gives the order: ~/.bash_profile, ~/.bash_login, ~/.profile, and the first one that exists.
  • The usual fix is one line. A ~/.bash_profile that sources ~/.bashrc made the login shell read both.
  • --norc and --noprofile switch the two off. The prompt page runs bash with --norc to keep a ~/.bashrc out.

On a Mac

The files in your home folder behave the same way. The three keys on this page are shared: macOS's bash 3.2 read exactly the files that Ubuntu's bash 5.2 read, and so did the two zsh builds and the two fish builds.

The files in /etc run first, and they do differ. With a HOME that holds nothing but an empty ~/.zshrc (so zsh does not offer to write one), the prompt each shell starts with is whatever a system file set, or else the shell's own default:

Verified output of startup_system_prompt_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.

$ bash -i -c 'echo "$PS1"'
${debian_chroot:+($debian_chroot)}\u@\h:\w\$ 

$ bash -l -i -c 'echo "$PS1"'
${debian_chroot:+($debian_chroot)}\u@\h:\w\$ 

$ zsh -i -c 'print -r -- "$PS1"'
%m%# 

$ zsh -f -i -c 'print -r -- "$PS1"'
%m%# 

Verified output of startup_system_prompt_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.

$ bash -i -c 'echo "$PS1"'
\s-\v\$ 

$ bash -l -i -c 'echo "$PS1"'
\h:\W \u\$ 

$ zsh -i -c 'print -r -- "$PS1"'
%n@%m %1~ %# 

$ zsh -f -i -c 'print -r -- "$PS1"'
%m%# 
  • bash on macOS sets no prompt for a shell that is not a login shell, so it keeps bash's built-in default, \s-\v\$. A login bash gets \h:\W \u\$, the prompt macOS's /etc/bashrc sets. bash on Ubuntu gets \u@\h:\w\$, behind a debian_chroot test, the prompt Ubuntu's /etc/bash.bashrc sets, whether it is a login shell or not.
  • zsh on macOS gets %n@%m %1~ %#, the prompt macOS's /etc/zshrc sets. zsh on Ubuntu has no system prompt, so zsh -i starts with zsh's own %m%#, the same prompt as zsh -f, which reads no files at all.
  • They do more than set a prompt. Read, not run here: macOS's /etc/zprofile rebuilds PATH with path_helper, and Ubuntu's /etc/bash.bashrc prints a hint about sudo to members of the sudo group unless ~/.hushlogin exists, which is why the bash scripts on these two pages create one. The rest of this library's examples run zsh with -f and fish with --no-config, so that no system file can change their output.

Terminal.app opens a login shell; a Linux desktop terminal usually does not. Neither is measured by CI, which has no terminal windows, so both are documented behaviour:

  • On a Mac, a new Terminal window runs your shell as a login shell: About bash_profile and bashrc on macOS ↗ explains that Terminal runs .bash_profile where other systems run .bashrc. On the Mac this page was written on (macOS 26.6, checked with ps on 2026-09-13), Terminal's child process was login -pf with the user's name, and its child was -fish. A shell whose name starts with - is a login shell (bash manual, Invoking Bash ↗).
  • On Linux, a terminal emulator starts a plain shell, not a login shell, unless it is told to: Greg's Wiki, DotFiles ↗ walks through xterm starting /bin/bash without the -, and its -ls flag for a login shell. GNOME Terminal keeps the choice as a profile setting, Run command as a login shell (GNOME Terminal help ↗).

So a line in ~/.bashrc, the file a Linux book names, is read by a Linux terminal window and skipped by a Mac's. In bash on a Mac, put the line in ~/.bash_profile, or give ~/.bash_profile the one-line fix above. And unless you changed it, the shell in a Mac's Terminal is zsh, not bash.

In zsh and fish

zsh has three files where bash has two, and they add up instead of standing in for each other:

Verified output of startup_files_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ cat ~/.zshrc
echo '  ~/.zshrc'

$ zsh -c true
  ~/.zshenv

$ zsh -l -c true
  ~/.zshenv
  ~/.zprofile

$ zsh -i -c true
  ~/.zshenv
  ~/.zshrc

$ zsh -l -i -c true
  ~/.zshenv
  ~/.zprofile
  ~/.zshrc

$ zsh -f -l -i -c true
  (none)
  • ~/.zshenv runs every time, zsh -c included, and that is the kind of zsh a script runs in. A line that prints something, or takes a while, belongs in one of the other two.
  • ~/.zprofile is for a login shell and ~/.zshrc for an interactive one, and a shell that is both reads both. A Mac's Terminal opens a login shell that is also interactive, so it reads ~/.zshrc: in zsh, the book's advice works with the file name changed.
  • zsh read neither ~/.profile nor ~/.bashrc in any of the four runs.
  • -f reads nothing. It is how this library runs every zsh example.

fish reads one file every time, and the file asks what kind of shell is reading it:

Verified output of startup_files_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ cat ~/.config/fish/config.fish
echo '  ~/.config/fish/config.fish'
status is-login; and echo '    status is-login'
status is-interactive; and echo '    status is-interactive'

$ fish -c true
  ~/.config/fish/config.fish

$ fish -l -c true
  ~/.config/fish/config.fish
    status is-login

$ fish -i -c true
  ~/.config/fish/config.fish
    status is-interactive

$ fish -l -i -c true
  ~/.config/fish/config.fish
    status is-login
    status is-interactive

$ fish -c true
  ~/.config/fish/conf.d/prompt.fish
  ~/.config/fish/config.fish

$ fish --no-config -l -i -c true
  (none)
  • config.fish ran all four times, fish -c included. status is-login and status is-interactive are how it tells the four apart, so a line meant only for a person at a prompt goes inside if status is-interactive … end.
  • Files in ~/.config/fish/conf.d/ run too, before config.fish.
  • fish read none of bash's or zsh's files.
  • --no-config reads nothing. It is how this library runs every fish example.
  • A prompt does not go in any of these. funcsave fish_prompt writes it to ~/.config/fish/functions/, and fish loads it from there by itself: see PS1 belongs to bash.

The four runs, side by side:

bash zsh fish
a script, -c nothing ~/.zshenv config.fish
login, -l ~/.bash_profile, else ~/.profile ~/.zshenv, ~/.zprofile config.fish; status is-login succeeds
interactive, -i ~/.bashrc ~/.zshenv, ~/.zshrc config.fish; status is-interactive succeeds
both, -l -i ~/.bash_profile only all three config.fish; both succeed
read nothing --norc, --noprofile -f --no-config

If you are coming from another library

  • Rust. Fuzzy finding ↗ sets up fzf's key bindings with one line in config.fish, ~/.bashrc or ~/.zshrc. In bash on a Mac, the ~/.bashrc line needs the ~/.bash_profile fix above before a Terminal window sees it.
  • Encodings. Locale and LC_CTYPE covers Terminal's Set locale environment variables on startup, which puts LANG into the environment of the shell Terminal starts, before any of these files run.

See also