Shell Configuration Files (.bashrc, .bash_profile)

Every time you open a terminal, log into a Linux server, or launch a new shell, Bash decides which configuration files to read based on how that shell was started — and getting this wrong is one of the most common sources of “it works in one terminal but not another” confusion. Shell configuration files like ~/.bashrc and ~/.bash_profile are just Bash scripts that Bash automatically runs at startup to set up your environment: your PATH, aliases, functions, prompt, and environment variables. Understanding exactly when each file runs is the key to configuring your shell correctly instead of guessing.

Overview: How Bash Decides Which Files to Read

Bash classifies every shell session along two independent dimensions: login vs. non-login, and interactive vs. non-interactive. Which configuration files get read depends on where a session falls on those two axes.

A login shell is one started as if you were logging into the machine: a text console login, an SSH connection into a remote server, or a shell started with bash --login. A non-login shell is one started from inside an already-running session — opening a new terminal window or tab in your desktop environment, or typing bash at an existing prompt, both start non-login shells, because you never “logged in” for those; you were already logged in.

Separately, a shell is interactive if it presents a prompt and reads commands you type, and non-interactive if it is running a script or a single command with no human at the keyboard (a cron job, a shell invoked with bash -c, or a script executed as ./deploy.sh). Configuration meant only for interactive use, like aliases and prompt colors, is not meant to run for non-interactive shells.

On a standard Ubuntu/Debian system, Bash’s startup logic works like this:

Shell type Files read, in order
Login (console login, SSH login, bash --login) /etc/profile, then the first of ~/.bash_profile, ~/.bash_login, or ~/.profile that exists
Interactive, non-login (new terminal window/tab, typing bash) /etc/bash.bashrc, then ~/.bashrc
Non-interactive (running a script, bash -c) Nothing automatically, unless the BASH_ENV variable points to a file
Login shell exiting ~/.bash_logout, if it exists

Notice the asymmetry: Bash only reads one of ~/.bash_profile, ~/.bash_login, or ~/.profile for a login shell — never more than one, and it stops at the first one it finds. That is why, on Ubuntu, the default new-user setup ships a ~/.profile (not a ~/.bash_profile), and that ~/.profile contains a small block of code that manually sources ~/.bashrc if the shell is interactive. This is how most distributions paper over the login/non-login split: your login shell’s profile file explicitly pulls in ~/.bashrc, so your aliases and prompt customizations end up available everywhere, whether or not you’d otherwise have gotten there.

Why the Split Exists

The historical reason for keeping “login” setup separate from “interactive” setup is that login files are meant to run once per session — they set up things like environment variables and PATH that every child process should inherit — while ~/.bashrc runs every time you start a new shell, including every new terminal tab and every nested bash you launch from inside another shell. If you put expensive or session-scoped setup, like appending to PATH, directly in ~/.bashrc without care, it can run over and over as you open nested shells, sometimes duplicating entries. Environment variables marked with export are copied into a process’s environment block at fork/exec time and inherited by every child process from then on, so they only need to be set once, in a file read early such as a login file. Aliases and functions, on the other hand, exist only inside the shell that defines them — they are never inherited by child processes — so they must be (re-)defined in every interactive shell, which is exactly what ~/.bashrc is for.

Running a file’s commands into your current shell, without starting a new process, is what the source builtin (or its shorthand, a single dot) does. source ~/.bashrc reads and executes that file’s commands directly in your existing shell, so any new export, alias, or function becomes immediately available. Running ~/.bashrc as a script instead would execute it in a brand-new child process whose environment changes disappear the moment that process exits — this is a very common point of confusion.

Syntax: What Goes Inside These Files

These files aren’t a special syntax — they are ordinary Bash scripts. Anything valid in a script is valid here.

export VAR_NAME="value"       # environment variable, inherited by child processes
alias name='command'          # shell-only shortcut, never inherited by children
name() { command; }           # shell function, also shell-only
PATH="$HOME/bin:$PATH"        # prepend a directory to PATH
  • export VAR="value" — defines an environment variable and marks it for inheritance by any process this shell later starts (programs, scripts, subshells).
  • alias name='command' — defines a text-substitution shortcut recognized only by interactive Bash; not available to scripts or other programs.
  • name() { ...; } — defines a shell function, useful for anything an alias can’t express, such as arguments or conditionals.
  • PATH="dir:$PATH" — prepends dir so it is searched before the rest of PATH; append instead with PATH="$PATH:dir" to search it last.

Examples

The following examples assume a normal desktop Linux setup where you have a terminal open and a home directory at ~.

Example 1: Permanently adding a directory to PATH

Suppose you keep personal scripts in ~/bin and want to run them by name from anywhere, in every future shell.

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
echo "$PATH"

Output:

/home/ana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

The first line appends a new line to ~/.bashrc without disturbing anything already there. source ~/.bashrc re-runs the whole file in the current shell, so the new export takes effect immediately — you don’t need to close and reopen the terminal. The final echo "$PATH" confirms ~/bin is now first in the search path. Because this line lives in ~/.bashrc, it will also apply automatically the next time you open a new terminal or SSH in, through Ubuntu’s default ~/.profile sourcing ~/.bashrc.

Example 2: Adding aliases

echo "alias ll='ls -alF'" >> ~/.bashrc
echo "alias gs='git status'" >> ~/.bashrc
source ~/.bashrc
ll ~/projects

Output:

total 24
drwxr-xr-x  5 ana ana 4096 Aug  3 10:12 ./
drwxr-xr-x 18 ana ana 4096 Aug  1 09:03 ../
drwxr-xr-x  8 ana ana 4096 Jul 28 14:50 course-agent/
drwxr-xr-x  4 ana ana 4096 Jul 30 16:21 dotfiles/

Each alias line becomes a shortcut recognized only by this interactive shell: ll now expands to ls -alF before Bash runs it. Aliases are not commands in their own right and are not visible to scripts or child processes — they only exist for interactive use, inside a shell that has sourced the file defining them.

Example 3: Checking whether your shell is a login shell

shopt -q login_shell && echo "login shell" || echo "not a login shell"

Output:

not a login shell

shopt -q login_shell checks Bash’s internal login_shell option, set once at startup based on how the shell was invoked, and exits with status 0 if true. A terminal emulator tab typically starts a non-login shell, which is why ~/.bashrc is what matters there, while a fresh SSH connection or console login starts a login shell, where ~/.bash_profile or ~/.profile run instead, unless they source ~/.bashrc themselves.

How It Works, Step by Step

Walking through what happens when you open a new local terminal window versus SSH-ing into a server:

  1. Your terminal emulator (or SSH client) forks a new process and execs /bin/bash into it, passing flags that mark it login or non-login.
  2. Bash checks those flags. For a login shell, it reads /etc/profile first, the system-wide settings for all users, which on Debian/Ubuntu typically loops over scripts in /etc/profile.d/ too.
  3. Still in the login-shell branch, Bash looks for exactly one personal file, in this fixed order, and stops at the first match: ~/.bash_profile, then ~/.bash_login, then ~/.profile.
  4. If that file contains a line sourcing ~/.bashrc, as Ubuntu’s default ~/.profile does, guarded by an interactive check, your aliases and prompt setup load too — otherwise a login shell would never see them.
  5. For a non-login interactive shell, such as a new terminal tab, Bash skips straight to /etc/bash.bashrc and then ~/.bashrc — no profile files are read at all.
  6. For a non-interactive shell running a script, none of these files are read automatically, which is why scripts can’t rely on aliases or interactive-only settings from your interactive shells; only exported environment variables, inherited from whatever process launched the script, are visible.
  7. When a login shell exits, Bash finally runs ~/.bash_logout if it exists, often used to clear the screen or clean up temporary files.

Common Mistakes

Mistake 1: Overwriting the file with > instead of appending with >>. > truncates a file to empty before writing; >> appends to the end. Using > on an existing config file destroys everything already in it.

# WRONG: truncates ~/.bashrc, deleting every alias and setting already there
echo 'export EDITOR=vim' > ~/.bashrc
# CORRECT: appends the new line, keeping existing content intact
echo 'export EDITOR=vim' >> ~/.bashrc

If this happens to you, check for a backup, since many editors keep one, or restore from /etc/skel/.bashrc, which is the default template Ubuntu copies into new home directories.

Mistake 2: Editing the file but forgetting to reload it. Bash only reads ~/.bashrc at shell startup. Appending a new alias doesn’t retroactively change a shell that is already running — you must either open a new terminal or explicitly re-run the file with source.

# WRONG: alias is only defined in the file, not yet loaded into this shell
echo "alias ll='ls -alF'" >> ~/.bashrc
ll ~/projects
# CORRECT: source the file so the new alias is loaded into the current shell
echo "alias ll='ls -alF'" >> ~/.bashrc
source ~/.bashrc
ll ~/projects

Mistake 3: Putting interactive-only setup in the wrong file. Aliases, prompt customization, and shell functions belong in ~/.bashrc, not in a login-only file like ~/.bash_profile. If you put them only in ~/.bash_profile, they will work the moment you SSH in, but silently vanish in every new terminal tab you open afterward, since new tabs are non-login shells that never read ~/.bash_profile at all.

Best Practices

  • Put environment variables and PATH changes that every shell should inherit in a login file, ~/.bash_profile or ~/.profile, since they are read once and then inherited by every child process.
  • Put aliases, functions, and prompt customization in ~/.bashrc, since those are shell-local and must be redefined in every interactive shell.
  • If you have a ~/.bash_profile, have it source ~/.bashrc, guarded by an interactive check, so login shells get your aliases too, instead of maintaining two separate, drifting configurations.
  • Always use >>, never >, when adding a line to an existing configuration file from the command line.
  • After editing, reload with source ~/.bashrc, or open a new terminal, rather than assuming the change is already active.
  • Keep ~/.bashrc fast, since it runs on every new shell — avoid slow network calls or heavy computation in it; if it grows large, split it into a separate file such as ~/.bash_aliases and source that file from ~/.bashrc.
  • Guard interactive-only content at the top of ~/.bashrc so non-interactive tools that happen to source it don’t choke on prompt-related commands:
case $- in
    *i*) ;;
      *) return ;;
esac
  • Before editing, back up the file with cp ~/.bashrc ~/.bashrc.bak so a mistake is easy to undo.

Practice Exercises

  1. Add an alias called ll for ls -alF and a PATH entry for ~/bin to your ~/.bashrc, without closing your terminal. Verify both took effect using type ll and echo "$PATH".
  2. Determine whether the shell in your default terminal application is a login shell or not, using shopt -q login_shell. Then open an SSH connection to a remote machine, or run bash --login locally, and check again. Explain, in your own words, why the two results differ.
  3. Add the non-interactive guard shown in Best Practices to the very top of your ~/.bashrc. Confirm it doesn’t break your interactive shell by opening a new terminal and checking your aliases still work, then confirm a non-interactive invocation like bash -c 'echo hi' still runs cleanly.

Summary

  • Bash reads different files depending on whether a shell is a login or non-login shell, and whether it is interactive or not.
  • Login shells read /etc/profile, then the first existing one of ~/.bash_profile, ~/.bash_login, or ~/.profile.
  • Non-login interactive shells, such as new terminal tabs, read /etc/bash.bashrc and ~/.bashrc instead — never the profile files.
  • Non-interactive shells running scripts read neither, by default, so they only inherit already-exported environment variables.
  • Exported variables are inherited by child processes; aliases and functions are not, and must be redefined by ~/.bashrc in every interactive shell.
  • Use source ~/.bashrc, or open a new terminal, to load changes into a currently running shell — editing the file alone changes nothing until it is reloaded.
  • Always append with >> rather than overwrite with > when adding to an existing configuration file.