bashrc vs bash_profile vs profile: Which File to Put Things In
Your PATH export works in the terminal and not in cron. Your alias works locally and not over ssh. Both have the same cause, and it is not mysterious once you know which shell reads which file.
Two questions decide everything
Bash asks two things about itself at startup.
Is this a login shell? A shell started by the login process, by ssh user@host, by su -, or with bash -l.
Is this interactive? A shell with a terminal attached, where a human is typing.
Those two flags give four combinations, and each reads a different set of files.
# am I a login shell
shopt -q login_shell && echo login || echo not login
# am I interactive
[[ $- == *i* ]] && echo interactive || echo not interactive
Run those in a terminal, in an ssh session, and inside a script. The answers differ, which is the whole point.
What gets read
Login and interactive (console login, ssh, su -):
/etc/profile- then the first of
~/.bash_profile,~/.bash_login,~/.profilethat exists
Only the first one found. This is where people lose an afternoon.
Interactive, not login (a new terminal tab, bash inside a shell):
/etc/bash.bashrcon Debian and Ubuntu,/etc/bashrcon Fedora~/.bashrc
Non-interactive (a script, cron, a systemd unit):
Neither of the above. Bash reads $BASH_ENV if it is set, and it almost never is. Effectively nothing.
Login shell exiting reads ~/.bash_logout.
The consequence that bites everyone
# in ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
Works in your terminal. Then:
# crontab
* * * * * mytool --run
# /bin/sh: mytool: command not found
Cron runs a non-interactive non-login shell. It reads neither file. Your PATH addition does not exist there, and no amount of editing .bashrc will change that.
The fixes, in order of preference:
# absolute path in the script or crontab
* * * * * /home/you/.local/bin/mytool --run
# or set PATH in the crontab itself
PATH=/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin
* * * * * mytool --run
# or in a systemd unit
[Service]
Environment=PATH=/home/you/.local/bin:/usr/bin:/bin
ExecStart=/home/you/.local/bin/mytool --run
Our cron guide covers this specifically, because it is the most common cron problem there is.
Why most distributions chain them
Because remembering the rules is tedious, nearly every distribution has ~/.bash_profile source ~/.bashrc:
# ~/.bash_profile
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
That is why things put in .bashrc appear to work in login shells too. The chain is doing it, not bash.
Debian goes further and ships only ~/.profile by default, with the same include:
# ~/.profile, Debian and Ubuntu
if [ -n "$BASH_VERSION" ]; then
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
If you create a ~/.bash_profile on such a system, ~/.profile stops being read at all, because bash takes the first file it finds. People add a .bash_profile with one export in it and wonder why everything else broke.
If you create one, source the others deliberately.
profile is not bash_profile
~/.profile is the POSIX file. sh, dash, and other shells read it. ~/.bash_profile is bash only.
This matters when /bin/sh is dash, as it is on Debian and Ubuntu. A bash-specific construct in ~/.profile breaks any sh login, so keep that file portable:
# fine in .profile
export EDITOR=vim
PATH="$HOME/bin:$PATH"
# not fine in .profile, these are bash only
shopt -s globstar
[[ -d "$HOME/bin" ]] && PATH=...
source ~/something
Use . instead of source, [ ] instead of [[ ]], and keep bash features in .bashrc.
The rule worth memorising
| Put this | In this file | Because |
|---|---|---|
export variables | ~/.profile or ~/.bash_profile | Inherited by children, needed once |
| Aliases | ~/.bashrc | Not inherited, needed per shell |
| Functions | ~/.bashrc | Same |
PS1 and prompt | ~/.bashrc | Only meaningful interactively |
shopt settings | ~/.bashrc | Per-shell behaviour |
umask | ~/.profile | Inherited |
| Anything that prints | ~/.bashrc, guarded | Breaks scp and rsync otherwise |
The underlying logic: exported variables are inherited by child processes, so they only need setting once at login. Aliases and functions are not inherited, so they need setting in every shell.
Output in these files breaks things
# in ~/.bashrc
echo "Welcome back, $USER"
neofetch
That greeting breaks scp, rsync and sftp. Those tools open a non-interactive shell and expect a clean protocol stream; text printed into it corrupts the transfer, and the error message says nothing about your dotfiles.
Debian’s default .bashrc opens with a guard for exactly this reason:
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
Keep that guard. If you want output, put it after the guard, or gate it explicitly:
if [[ $- == *i* ]] && [[ -z "$SSH_TTY" || -n "$PS1" ]]; then
fastfetch
fi
Graphical applications read none of this
An application launched from your desktop menu is not started by a shell. It inherits its environment from the session manager, which means nothing in .bashrc or .profile applies.
That is why an editor launched from the menu cannot find a tool that works fine in your terminal.
# for systemd user sessions
mkdir -p ~/.config/environment.d
printf 'PATH=%s/.local/bin:%s\n' "$HOME" '$PATH' > ~/.config/environment.d/path.conf
# check what a graphical app actually sees
systemctl --user show-environment
~/.config/environment.d/*.conf is read by the systemd user session and applies to graphical applications. It takes simple assignments only, with no shell syntax. Log out and back in for it to take effect.
Some display managers also read ~/.profile, and some do not. The environment.d route is the one that works consistently.
Organising it properly
Rather than one growing file, split it:
# ~/.bashrc, at the end
for f in ~/.config/bash/*.sh; do
[ -r "$f" ] && . "$f"
done
unset f
~/.config/bash/
├── aliases.sh
├── functions.sh
├── prompt.sh
└── work.sh
Easier to read, easier to keep in version control, and easier to exclude one machine’s specifics. Our dotfiles guide covers syncing them across machines.
Debugging what actually ran
# trace a login shell's startup
bash -lxc exit 2>&1 | head -60
# trace an interactive one
bash -ixc exit 2>&1 | head -60
# which files exist
ls -la ~/.bash* ~/.profile
# is a variable exported or just set
declare -p PATH
env | grep '^PATH='
The bash -lxc exit trace is the definitive answer to “which file set this”. It prints every line as it executes.
For a variable that is set but not exported, declare -p shows it while env does not, and that difference is why a child process cannot see it.
Other shells, briefly
zsh uses .zshenv for all shells, .zprofile for login, .zshrc for interactive, .zlogin after zshrc. .zshenv is read by everything, including scripts, which makes it powerful and easy to misuse.
fish uses ~/.config/fish/config.fish plus conf.d/, and sets universal variables with set -Ux, which persist without a config file at all.
Our shell comparison covers the differences that follow from this.
Frequently Asked Questions
What is the difference between bashrc and bash_profile?
bash_profile runs for login shells, once per login, and is the right place for environment variables. bashrc runs for interactive non-login shells, every time you open a terminal, and is the right place for aliases, functions and prompt settings. Most distributions make bash_profile source bashrc so both apply.
Why does my PATH work in the terminal but not in cron or a systemd service?
Because cron and systemd run non-interactive non-login shells, which read neither bashrc nor profile. Nothing you set in those files exists there. Set the variable explicitly in the crontab or the unit file, or use absolute paths in the script.
Which file should I edit to set an environment variable?
For a variable that graphical applications also need, use a systemd user environment file or your display manager environment, because those load before your shell. For shell use only, bash_profile or profile is correct. Putting it in bashrc works in terminals and is technically the wrong stack.
Why do my aliases not work in a shell script?
Because scripts run non-interactive shells, which do not read bashrc, and because bash disables alias expansion in non-interactive mode anyway. Use a function in a sourced file, or define what you need inside the script.
What is the difference between profile and bash_profile?
profile is the POSIX file read by many shells including sh and dash. bash_profile is bash specific. Bash prefers bash_profile and only falls back to profile if it is absent, so a bash_profile that does not source profile silently disables it.
Why does my terminal read bashrc when it is supposed to be a login shell?
Terminal emulators differ. Most open a non-login interactive shell, so bashrc applies. macOS Terminal and some configurations open a login shell instead, which is why advice that works on one machine fails on another. Check with shopt -q login_shell, which succeeds only in a login shell.