You installed something by hand and it needs a PATH line. Two files could hold it; they look interchangeable and are not. The rule: the PATH entry belongs wherever the thing it points to lives. Hand-installed on this machine only → ~/.bashrc. Installed automatically on every machine → the dotfiles repo (dotfiles-architecture).
The big picture
~/.bash_aliasesis a pointer into git. Appending to it is a repo edit → needs a branch first.~/.bashrcis a plain local file. Git has never heard of it; it dies with a reflash.- The test: if I set up a brand-new laptop tomorrow, should this line appear on it by itself? Yes → dotfiles. No →
.bashrc.
1. The trap: one file is a symlink
ls -la ~/.bashrc ~/.bash_aliaseslrwxrwxrwx 1 xuebin xuebin 40 Aug 31 13:18 .bash_aliases -> /home/xuebin/dotfiles/shell/bash_aliases
-rw-r--r-- 1 xuebin xuebin 4429 Sep 4 15:03 .bashrc- First character:
l→ symlink ·-→ ordinary file. - Writing to
~/.bash_aliaseswrites~/dotfiles/shell/bash_aliases→ git sees it → a tracked edit. ~/.bashrc→ belongs to this machine; nothing tracks it.
echo 'export PATH=...' >> ~/.bash_aliases # really a commit-in-waiting in ~/dotfilesSame-looking edits, completely different kinds
Appending to
~/.bash_aliasestrips the “never edit onmain” rule. Branch first.
2. Why .bash_aliases exists at all
Not a bash feature: an Ubuntu convention. The stock ~/.bashrc (line 104) sources it if present:
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi- Separation: your customisations live outside the file the distro owns.
- A distro upgrade can replace
.bashrcwholesale and destroy nothing of yours. Nothing to merge.
3. Side by side
~/dotfiles/shell/bash_aliases (tracked) | ~/.bashrc (local) | |
|---|---|---|
| Under git / needs a branch first | yes | no |
| Survives a reflash | yes, via git clone + bootstrap.sh | no, gone |
| Appears on a new machine | automatically | never |
| History of why you added it | git log | none |
| Right for | things every machine of yours has | things only this machine has |
The reflash row is the one that bites
A local edit exists on exactly one disk; reinstall the OS and it’s gone with no record. Fine when the software it pointed at is gone too, a silent loss when it wasn’t.
4. The decision test
- Yes → dotfiles: aliases, prompt settings, editor choice,
~/.local/binon PATH. They describe you. - No →
.bashrc: a PATH entry to software unpacked by hand into/usr/local. On a fresh machine that directory won’t exist: a promise the machine can’t keep. - A dead PATH entry is harmless (bash skips missing directories), but machine-specific lines turn the repo into a pile of exceptions. Its value is that
bootstrap.shgives a working machine. - Promotion path: when a manual install becomes automated (added to
ansible/provision.yml, see ansible), the PATH line moves into dotfiles at the same time. Install and PATH entry are one fact.
5. Worked example: Go 1.27.1, 2026-09-09
sudo tar -C /usr/local -xzf go1.27.1.linux-amd64.tar.gz # from the a2_shenzen_bundle robot bundle
cat >> ~/.bashrc <<'EOF'
# Go toolchain, installed from tarball to /usr/local/go (go1.27.1).
export PATH="$PATH:/usr/local/go/bin"
EOF
source ~/.bashrc- Not reproducible: one bundle, one job, this laptop. No other machine has
/usr/local/go, and Ansible doesn’t know Go exists → local. - Contrast
~/.local/bin→ tracked, because pipx puts tools there on every machine and Ansible installs pipx everywhere. Same kind of line, opposite home. - If Go becomes standard tooling rather than one bundle’s dependency, it gets an Ansible task and the PATH line moves across. Not before.
6. Append or prepend?
export PATH="$PATH:/usr/local/go/bin" # append: existing entries win
export PATH="/usr/local/go/bin:$PATH" # prepend: this one wins
which go # → /usr/local/go/bin/go- PATH is searched left to right; first match wins.
- Append = safer default (Go’s own docs use it): a package-managed
goin/usr/binwould win, giving the version your system expects. - Prepend only to deliberately override a packaged copy.
whichshows a surprise?echo $PATHand read left to right.
7. The idempotence guard
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac- Used in the tracked file. Without it, every
source ~/.bashrc, nested shell andtmuxinsidetmuxappends again →echo $PATHbecomes an unreadable wall. - The colons around
":$PATH:"and the pattern make the match exact:/usr/local/go/bincan’t match/usr/local/go/bin-old. - A plain
export PATH="$PATH:..."duplicates on re-source: cosmetic, not harmful. Worth the guard once a file has more than a couple of PATH lines.
8. .bashrc isn’t read by everything
| File | Read by |
|---|---|
~/.profile | login shells, and the desktop session at graphical login |
~/.bashrc | interactive non-login shells, i.e. every terminal window |
~/.bash_aliases | nothing directly; sourced by .bashrc (§2) |
PATH set only in .bashrc is invisible to:
ssh thishost 'go version'→ non-interactive, no.bashrc- GUI-launched apps → inherit the session environment, built from
.profileat login systemd --userservices and cron jobs
Works in the terminal, not in a GUI tool or systemd unit?
This table is why. Put the PATH line in
~/.profile, then log out and back in. (Terminals are fine: Ubuntu’s.profilesources.bashrcfor bash anyway.)
Quick reference
ls -la ~/.bashrc ~/.bash_aliases # l = symlink into dotfiles (tracked), - = local file
export PATH="$PATH:/usr/local/go/bin" # append (default): existing entries win
export PATH="/usr/local/go/bin:$PATH" # prepend: deliberately override
which go # which copy won?
echo $PATH # read left to right
source ~/.bashrc # reload in this terminal