.bashrc, .bash_profile, and .profile: Really Understanding the Differences
AI generated
$_
#!/
Bash · Shell Configuration · Login Shell · Deployment
.bashrc, .bash_profile, and .profile
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.

16 min read login shell · interactive shell · PATH Bash 4.x · 5.x · macOS · Linux

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.

11. FAQ: .bashrc, .bash_profile, and .profile: The Essentials at a Glance

1What is the difference between .bashrc and .bash_profile?
.bashrc loads on every interactive non-login shell, such as a new terminal window. .bash_profile loads only for a login shell, such as an SSH login, and usually only once per session.
2Why does my SSH session load my aliases even though they are in .bashrc?
Because .bash_profile usually explicitly re-sources .bashrc at the end. Without that line, aliases would actually be missing in an SSH login session.
3When does .profile load instead of .bash_profile?
.profile only loads if neither .bash_profile nor .bash_login exist. If .bash_profile exists, .profile is completely ignored.
4Why doesn't my alias work in a cron job even though it works in the terminal?
Cron jobs run as non-interactive shells that load neither .bashrc nor .bash_profile automatically. Aliases and PATH entries set there simply do not exist in the cron job.
5Which file does a non-interactive shell like a script load?
None of the mentioned files by default. The only exception is a set BASH_ENV environment variable, whose referenced file loads before the script runs.
6Why is macOS Terminal.app different from Linux terminals?
macOS Terminal.app traditionally starts new windows as a login shell and loads .bash_profile. Most Linux desktop terminals open new windows as an interactive non-login shell and load .bashrc.
7Should I set PATH extensions in .bashrc or .bash_profile?
In .bash_profile, because that file runs once per login session and the variable then propagates to every child process started from it. Variables set in .bashrc only apply to that one interactive shell.
8Why shouldn't deployment scripts rely on .bashrc?
Because deployment scripts are often started as a non-interactive shell by CI pipelines, systemd, or cron, where .bashrc never loads. PATH and environment variables must therefore be set inside the script itself.
9Do aliases work in Bash scripts?
Not without extra steps. Bash disables alias expansion in non-interactive shells by default, unless shopt -s expand_aliases is explicitly set. Functions are the more reliable alternative in scripts.
10What happens if both .bash_profile and .profile exist?
For a login shell, Bash only runs the first file found in the order .bash_profile, .bash_login, .profile. If .bash_profile exists, .profile is completely ignored.