Skip to content

Bash Startup Files and Shell Types


Open a terminal and your prompt, aliases, PATH additions, and any custom environment variables are all just… there. SSH into a server to run one remote command and none of that shows up at all. Same Bash, wildly different behavior — and the reason is that Bash decides which configuration files to read based on how it was started, not just that it was started. This chapter covers that decision: the interactive/non-interactive and login/non-login distinctions, which specific files get read in each case, and a genuinely common production gotcha that comes from getting this wrong.

Interactive vs. Non-Interactive Shells

An interactive shell is one where a human is directly typing commands and reading output — the shell you get when you open a terminal. Bash shows a prompt, supports command history, job control, and so on.

A non-interactive shell is one running without a human at the keyboard — most commonly, a shell running a script. No prompt, no history expansion, nothing designed for a person watching in real time.

You can check which kind of shell you’re in by inspecting the special variable $-, which lists the shell’s currently active option flags:

echo "$-"
himBHs

If that string contains an i, the shell is interactive. A script running non-interactively won’t have i in $- at all — this is the standard, reliable way to test interactivity from inside a script or a startup file, and you’ll use it directly in this chapter’s safety section.

Login vs. Non-Login Shells

Independently of interactivity, a shell is also either a login shell or a non-login shell:

  • A login shell is the one created when you first log in — a fresh terminal session at a physical or virtual console, or the shell an SSH connection starts for you.
  • A non-login shell is any shell started from within an existing session — opening a new terminal tab in a desktop environment, for instance, typically starts a non-login interactive shell, not a fresh login. Running a script is a non-login (and non-interactive) shell too.

These two properties — interactive/non-interactive and login/non-login — combine into the real cases Bash distinguishes between, and each combination reads a different set of startup files.

    flowchart TD
    A["New Bash process starts"] --> B{"Login shell?"}
    B -- yes --> C["Reads /etc/profile,<br/>then the first of:<br/>~/.bash_profile, ~/.bash_login, ~/.profile"]
    B -- no --> D{"Interactive?"}
    D -- yes --> E["Reads /etc/bash.bashrc (if present),<br/>then ~/.bashrc"]
    D -- no --> F["Reads nothing by default,<br/>unless BASH_ENV is set"]
  

The Startup Files, One By One

FileRead ByScope
/etc/profileLogin shellsSystem-wide
~/.bash_profile, ~/.bash_login, ~/.profileLogin shellsUser-specific — Bash reads only the first of these three that exists, not all of them
/etc/bash.bashrcInteractive, non-login shells (where present — this file is a Debian/Ubuntu convention; some other distributions don’t ship it)System-wide
~/.bashrcInteractive, non-login shellsUser-specific

Two details in that table matter a lot in practice:

Only one of the three login-shell candidate files is read. If you have both ~/.bash_profile and ~/.profile, and ~/.bash_profile exists, Bash reads that one and ignores ~/.profile entirely — it doesn’t merge them or read both.

A login shell does not automatically also read ~/.bashrc, even though you’d usually want the same aliases, functions, and PATH additions available whether you logged in fresh or opened a new tab. This is why you’ll commonly find ~/.bash_profile ending with a line like:

if [[ -f ~/.bashrc ]]; then
    source ~/.bashrc
fi

This explicitly sources ~/.bashrc from within the login startup file, using the source builtin, so that a login shell picks up the same interactive configuration a non-login shell would get automatically. This pattern is extremely common — enough that many systems ship it in the default ~/.bash_profile already — but it’s worth recognizing as a deliberate choice, not something Bash does on its own.

What About Non-Interactive, Non-Login Shells?

This is the case scripts fall into, and it’s the one with the simplest rule: by default, none of the files above are read at all. A script you run with ./script.sh or bash script.sh starts with essentially none of your interactive shell’s aliases, functions, or custom PATH — only whatever was actually exported into the environment, as covered in Chapter 2.

There’s one exception: if the environment variable BASH_ENV is set, a non-interactive shell will source whatever file it points to, even though it would otherwise read nothing. This is occasionally used deliberately — some deployment or CI systems set BASH_ENV to inject configuration into every script they run — but it’s uncommon in day-to-day scripting.

Best Practices

  • Put PATH additions and exported environment variables in a login-shell file (~/.bash_profile or ~/.profile), since those should generally be set once per session and inherited by everything after. Put purely interactive conveniences — aliases, prompt customization, shell options meant only for a human typing at a prompt — in ~/.bashrc.
  • Don’t assume a script can see your interactive shell’s aliases or functions. If a script genuinely needs something you’ve defined interactively, it needs to be an actual exported variable, or defined directly inside the script — not something living only in ~/.bashrc.
  • If you rely on the .bash_profile sourcing .bashrc pattern, guard it with an existence check (as shown above) rather than assuming the file is always there — a fresh account or a stripped-down system may not have one yet.
  • Never make .bash_profile and .bashrc source each other in both directions. One should source the other, one-way only; sourcing both ways creates infinite recursion the moment either file loads.

Shell-Safety Considerations

A startup file that unconditionally prints output is a common, genuinely disruptive mistake — and it’s a direct consequence of the interactive/non-interactive distinction covered above, since some tools that don’t look like “opening a terminal” still end up sourcing these files.

Here’s the naive version: a .bashrc with an unconditional welcome banner.

# ~/.bashrc
echo "Welcome back! Today is $(date)."

This looks completely fine in an ordinary terminal. The problem shows up with tools that use an interactive-style shell to run a single command remotely and capture its output — some SSH-based automation and remote-command patterns fall into exactly this case. If the shell they invoke happens to read .bashrc, that banner text gets mixed directly into whatever output the actual command produced, silently corrupting anything that expected clean output — a script parsing the result, a file being written from redirected output, and so on.

The fix is the $- check from earlier in this chapter — only produce interactive-only output when the shell is actually interactive:

# ~/.bashrc
case $- in
    *i*) echo "Welcome back! Today is $(date)." ;;
esac

Now the banner only ever prints when a human is actually looking at the prompt. Any non-interactive invocation that happens to source this file runs it silently, with no stray output to corrupt anything downstream.

Warning

This same interactive check is worth applying to anything in a startup file that isn’t pure configuration — custom prompts, PATH changes are fine unconditionally, but anything that produces output deserves the guard.

Hands-On: Watching Startup File Selection Happen

This walkthrough uses an isolated fake HOME directory so you can watch exactly which files get read in each scenario, without touching your real configuration at all.

1. Set up an isolated fake home.

mkdir -p /tmp/shell-course-demo/fake-home
cd /tmp/shell-course-demo

2. Create marker startup files.

cat > fake-home/.bash_profile << 'EOF'
echo "Loaded: .bash_profile"
EOF

cat > fake-home/.bashrc << 'EOF'
echo "Loaded: .bashrc"
EOF

3. Simulate a login shell. --login always reads the login-shell files, regardless of interactivity:

env -i HOME="$(pwd)/fake-home" bash --login -c 'echo "(shell ran)"'
Loaded: .bash_profile
(shell ran)

Only .bash_profile loads — .bashrc is never touched, exactly as the table above describes, since nothing inside this .bash_profile sources it.

4. Simulate an interactive, non-login shell. Bash’s -i flag forces interactive status even with -c:

env -i HOME="$(pwd)/fake-home" bash -i -c 'echo "(shell ran)"'
Loaded: .bashrc
(shell ran)

This time only .bashrc loads — the reverse of step 3.

5. Simulate an ordinary script (non-interactive, non-login).

env -i HOME="$(pwd)/fake-home" bash -c 'echo "(shell ran)"'
(shell ran)

Neither file loads. This is the default state every script in this course runs under.

6. See BASH_ENV override that default.

env -i HOME="$(pwd)/fake-home" BASH_ENV="$(pwd)/fake-home/.bashrc" bash -c 'echo "(shell ran)"'
Loaded: .bashrc
(shell ran)

Even fully non-interactive, .bashrc now loads — because BASH_ENV explicitly told Bash to source it.

7. Reproduce the stray-output problem, then apply the interactive guard. Step 6 already showed how easily a non-interactive shell can end up sourcing .bashrc via BASH_ENV — the same path automation tools sometimes take. With an unconditional banner in place, that stray text would land right next to real output. Add the guard from the safety section above and compare interactive vs. non-interactive side by side:

cat > fake-home/.bashrc << 'EOF'
case $- in
    *i*) echo "Welcome back!" ;;
esac
EOF

echo "--- interactive ---"
env -i HOME="$(pwd)/fake-home" bash -i -c 'echo "actual-command-output"'

echo "--- non-interactive, via BASH_ENV ---"
env -i HOME="$(pwd)/fake-home" BASH_ENV="$(pwd)/fake-home/.bashrc" bash -c 'echo "actual-command-output"'
--- interactive ---
Welcome back!
actual-command-output
--- non-interactive, via BASH_ENV ---
actual-command-output

The guarded .bashrc prints the banner only in the interactive case — in the non-interactive case, actual-command-output comes through completely clean.

8. Clean up.

cd ~
rm -rf /tmp/shell-course-demo

You’ve now watched Bash choose between login and non-login, interactive and non-interactive startup behavior directly, rather than taking the rules on faith — and reproduced the exact mechanism behind a real, common source of corrupted output in automated tooling. The next chapter moves from shell configuration to the commands themselves: redirection, pipelines, and the standard streams every command reads from and writes to.

Last updated on