Which file loads depends on the shell type, not on chance
Bash loads a different combination of configuration files on every start, depending on whether it runs as a login shell, an interactive non-login shell, or a non-interactive script execution. Anyone who writes PATH extensions or environment variables into the wrong file gets a locally working setup and later wonders why that exact same variable is suddenly missing in a cron job or deployment script.
Table of Contents
- 1. Three shell types, three different loading paths
- 2. .bash_profile and .profile: when login shells load them
- 3. .bashrc: when interactive non-login shells load it
- 4. Why .bash_profile often re-sources .bashrc explicitly
- 5. Platform differences: macOS Terminal.app versus Linux desktop
- 6. Non-interactive scripts: why they load nothing by default
- 7. Common mistake: defining an alias in .bashrc and expecting it in a script
- 8. Why deployment scripts should not rely on ~/.bashrc
- 9. The configuration files side by side
- 10. Summary
- 11. FAQ
1. Three shell types, three different loading paths
Bash fundamentally distinguishes between three operating modes at startup, each loading a different combination of configuration files: a login shell, typically created when logging in via SSH or at a text console, an interactive non-login shell, such as the one a new terminal window on the desktop opens, and a non-interactive shell, which runs a script with no user interaction at all, for example from a cron job or a deployment process.
This distinction is not an academic detail, it is the root cause of one of the most common Bash mistakes: an environment variable or a PATH entry that works reliably while working interactively in a terminal but is silently missing in an automated script, because that script runs as a non-interactive shell and loads an entirely different configuration file, or none at all.
2. .bash_profile and .profile: when login shells load them
When Bash starts as a login shell, it looks for exactly one of the following files, in this order, and runs only the first one it finds: /etc/profile first, system-wide, then from the user's home ~/.bash_profile if that file exists, otherwise ~/.bash_login, and only if that is missing too, ~/.profile. That cascade means an existing ~/.bash_profile completely shadows an existing ~/.profile, which easily causes confusion during system migrations when settings end up in the ignored file.
~/.profile is the cross-shell variant, also read by sh, dash, or ksh, and should therefore avoid Bash-specific syntax like arrays or [[ ]] tests, while ~/.bash_profile applies explicitly only to Bash and can safely use Bash-specific constructs. For plain PATH extensions and environment variables meant to also be understood by other shells, ~/.profile is therefore often the more portable choice.
# ~/.bash_profile -- loaded ONCE per login shell (SSH login, tty login)
# Good place for PATH extensions and env vars that should exist for the
# entire session, including any non-interactive scripts started from it.
export PATH="$HOME/.local/bin:$PATH"
export EDITOR="vim"
# Source .bashrc explicitly, since login shells do NOT load it automatically
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
3. .bashrc: when interactive non-login shells load it
If Bash instead starts interactively but not as a login shell, for example when opening a new terminal tab within an already-running graphical desktop session, ~/.bashrc loads instead, and exclusively that file, not ~/.bash_profile. That is exactly where all settings that only make sense for interactive use belong: aliases, the PS1 prompt definition, shell options via shopt, and autocomplete extensions.
The reason for this split is historical: a login shell traditionally was allowed to go through a slower, one-time initialization, while .bashrc runs again every time a new terminal window opens and should therefore stay lean. Anyone who places expensive commands like network lookups in .bashrc feels that as a noticeable delay on every new terminal window, while the same operation in .bash_profile only costs once per session.
# ~/.bashrc -- loaded on EVERY new interactive, non-login shell
# (new terminal tab, `bash` invoked manually). Good place for aliases,
# prompt customization, and shell options -- NOT for PATH/env vars that
# other tools (cron, systemd, deploy scripts) might also need.
alias ll="ls -alF"
alias gs="git status"
PS1='\u@\h:\w\$ '
shopt -s histappend
shopt -s checkwinsize
4. Why .bash_profile often re-sources .bashrc explicitly
Because a login shell does not automatically load .bashrc, a new SSH connection without an extra measure loses exactly the file with aliases and prompt definition, even though an SSH session feels completely normal and interactive to the user. That is why the de-facto standard has become that ~/.bash_profile explicitly re-sources ~/.bashrc at the end via source, or its shorthand ., as shown in the previous section's code example.
That chain makes an SSH login session feel like a normal interactive shell to the user, with all the usual aliases and the usual prompt, even though two separate files technically load in a specific order. If that re-sourcing line gets removed from .bash_profile or accidentally deleted, all aliases seemingly vanish for no reason on the next SSH login, without anything else having changed in the configuration.
5. Platform differences: macOS Terminal.app versus Linux desktop
A common source of confusion between operating systems is that macOS's Terminal.app traditionally starts every new window as a login shell, while most Linux desktop terminal emulators like GNOME Terminal or Konsole open new windows as an interactive non-login shell by default. A setup that works fine on macOS because ~/.bash_profile runs on every new window can fail on Linux if the same settings accidentally live only in .bash_profile instead of also in .bashrc.
That discrepancy also explains why many cross-platform dotfile repositories maintain both files and consistently have .bash_profile re-source .bashrc: that guarantees the same configuration applies regardless of whether a new terminal window starts as a login shell or as an interactive non-login shell, without the user having to worry about the platform-specific difference.
6. Non-interactive scripts: why they load nothing by default
When Bash runs a script, for example with ./deploy.sh or as a cron entry, the shell runs neither as a login shell nor as an interactive shell, but as a non-interactive shell. In that mode, Bash loads none of the mentioned files automatically by default, with one exception: if the BASH_ENV environment variable is set, the file it references gets loaded before the script starts, a mechanism rarely used in practice but present nonetheless.
That explains the classic cron job problem: an alias or a PATH extension defined in ~/.bashrc that works flawlessly when tested manually in a terminal simply does not exist in the cron job, because the cron daemon starts scripts as non-interactive, non-login shells and loads none of the interactive configuration files. A script that depends on a shortcut defined only as an alias, or on a PATH extension set only in .bashrc, fails in the cron job with 'command not found', even though it runs flawlessly in the terminal.
# crontab -e
# This entry runs as a non-interactive, non-login shell -- neither
# .bashrc nor .bash_profile is loaded automatically.
0 3 * * * /opt/scripts/nightly-backup.sh >> /var/log/backup.log 2>&1
# nightly-backup.sh MUST set its own PATH and env vars explicitly,
# it cannot rely on anything defined only in ~/.bashrc
#!/usr/bin/env bash
set -euo pipefail
export PATH="/usr/local/bin:/usr/bin:/bin"
export DATABASE_URL="postgres://backup_user@localhost/app"
7. Common mistake: defining an alias in .bashrc and expecting it in a script
A particularly common misunderstanding is assuming an alias defined in .bashrc is automatically available in every started script too. Even if a script accidentally re-sources ~/.bashrc explicitly via source, aliases still do not take effect in non-interactive shells by default, because Bash disables alias expansion in scripts for safety and consistency reasons, unless shopt -s expand_aliases is explicitly set.
The robust solution is to fundamentally never use aliases in scripts, using full function definitions or direct command invocations instead, because functions, unlike aliases, work reliably in non-interactive shells too, and even in child processes after being exported with export -f. That clear separation between 'convenient shortcut for a human in a terminal' and 'reliable building block for a script' avoids the most common source of errors on this topic from the outset.
8. Why deployment scripts should not rely on ~/.bashrc
A deployment script that assumes PATH extensions, version managers like nvm, or environment variables from ~/.bashrc only works reliably as long as it is started interactively by a human in a terminal. The moment the same process gets triggered by a CI pipeline, a systemd service, or a cron job, it is guaranteed to run as a non-interactive shell, where .bashrc never loads, and the script fails with unclear 'command not found' errors that look different and hard to reproduce depending on the trigger.
The robust fix is to write every deployment script so it explicitly sets every needed PATH entry and environment variable itself at the top of the script, rather than implicitly relying on an inherited interactive shell environment. That self-sufficiency also makes the script more portable: it then runs identically whether started manually in a terminal, from cron, from systemd, or from a CI pipeline.
#!/usr/bin/env bash
set -euo pipefail
# Never assume ~/.bashrc has run -- set everything the script needs explicitly.
export PATH="/usr/local/bin:$HOME/.local/bin:$PATH"
export NODE_ENV="production"
# If a version manager is truly required, source it explicitly with a
# guard, rather than assuming it was already initialized by .bashrc
if [ -s "$HOME/.nvm/nvm.sh" ]; then
. "$HOME/.nvm/nvm.sh"
nvm use 20 --silent
fi
npm run build
9. The configuration files side by side
Seeing all four files and their respective loading conditions laid out clearly side by side avoids most of the pitfalls described above from the start, because it immediately becomes obvious which file is meant for which concrete use case and which one is simply unsuitable for automation.
| File | Loaded on | Recommended content | Suitable for automation |
|---|---|---|---|
/etc/profile |
Every login shell, system-wide | Global PATH base, system variables | No, applies only to login shells |
~/.bash_profile |
Login shell (SSH, tty) | PATH, env vars, re-source .bashrc | No, applies only to login shells |
~/.bashrc |
Interactive non-login shell | Aliases, PS1, shopt, autocomplete | No, not loaded for scripts |
~/.profile |
Login shell, if .bash_profile is missing | Portable, cross-shell settings | No, applies only to login shells |
Mironsoft
Shell automation, DevOps tooling and deployment infrastructure
Shell scripts that hold up in production?
We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.
Code Review
ShellCheck analysis and manual review for critical Bash pattern violations.
Refactoring
Retrofitting error handling, logging and safe file operations.
CI Integration
Wiring ShellCheck and BATS into pipelines and building regression tests.
10. Summary
.bashrc, .bash_profile, and .profile: The Essentials at a Glance
Login shell
Loads exactly one file from /etc/profile, ~/.bash_profile, ~/.bash_login, or ~/.profile, in that order.
Interactive shell
Loads only ~/.bashrc, ideal for aliases, prompt, and shell options for daily terminal use.
Non-interactive shell
Loads none of these files by default, unless BASH_ENV is explicitly set. Scripts must set PATH and env themselves.
Deployment rule
Set PATH entries, version managers, and environment variables inside the script itself, never rely implicitly on .bashrc.