Environment, dotfiles and shell configuration
Variables and PATH precedence, which startup file runs when, and keeping dotfiles reproducible across machines.
Variables, export and PATH
NAME=ada # shell variable: not visible to child processes
export NAME # now it is in the environment of every child
export EDITOR=vim LOG_LEVEL=info # multiple at once
env | sort | head # the whole environment
printenv PATH
printenv LANG || echo unset
MYVAR=local bash -c 'echo $MYVAR' # scoped to one command only
unset MYVARecho "$PATH" | tr ':' '\n' # one entry per line, in search order
export PATH="$HOME/.local/bin:$PATH" # prepend: your version wins
export PATH="$PATH:/opt/tool/bin" # append: fallback
type -a python3 # every definition: alias, function, file
command -v python3 # the file that would run
hash -r # forget the shell's command cache after a move- The first matching directory in
PATHwins. Prepending is how a user-local tool overrides a system one. - Export a variable in a parent shell and every child inherits it; a variable set without
exportexists only in that shell. - Never put API keys in
.bashrc— it is read by every shell, backed up by your dotfiles repo, and often world-readable.
Which file runs when
| Shell type | bash reads | zsh reads |
|---|---|---|
Login shell (console, ssh without a command) | /etc/profile, ~/.bash_profile, then ~/.profile | /etc/zprofile, ~/.zprofile, ~/.zshrc |
| Interactive non-login (a new terminal tab, tmux window) | ~/.bashrc | ~/.zshrc |
Non-interactive (script, ssh host cmd, cron) | None by default | ~/.zshenv |
| Logout | ~/.bash_logout | ~/.zlogout |
⚠️
A non-interactive shell — cron,
ssh host 'cmd', most CI steps — reads no rc file at all. Anything a script needs must be set by the script, by the scheduler, or by BASH_ENV. This single distinction explains most 'works in my terminal, fails in the job' reports.Keeping dotfiles reproducible
# a plain git repo with symlinks - no tool required
git init --bare "$HOME/.dotfiles"
alias dotfiles='git --git-dir="$HOME/.dotfiles" --work-tree="$HOME"'
dotfiles config status.showUntrackedFiles no
dotfiles add .bashrc .vimrc .config/nvim/init.vim
dotfiles commit -m "initial dotfiles"
# stow does the symlink bookkeeping for you
cd ~/dotfiles && stow -t "$HOME" bash git nvim
stow -D nvim # remove the links again
# machine-specific extras, ignored by the repo
cat >> ~/.bashrc <<'EOF'
[ -f "$HOME/.bashrc.local" ] && . "$HOME/.bashrc.local"
EOFbash -n ~/.bashrc # syntax check without running it
bash -x ~/.bashrc 2>&1 | head -30 # trace what actually executes
echo "$PROMPT_COMMAND"- Keep the repo free of secrets: use environment files, a password manager, or
sops-encrypted files. - Guard interactive-only settings with
[[ $- == *i* ]]so a script sourcing.bashrcis not slowed or broken by prompts and colours. - A startup file that fails midway leaves the whole shell subtly broken — append an
echomarker temporarily to find the last line that ran.
FAQ
I edited PATH but new terminals still have the old one?
The terminal may start a login shell that reads
~/.bash_profile and never ~/.bashrc. Put shared settings in one file and source it from the other, or use ~/.profile as the single entry point.Where should an environment variable for a service go?
To the service, not your shell. Use
EnvironmentFile in a systemd unit, an env file in a Compose service, or the platform's secret store. A variable in .bashrc has no effect on daemons.Related
Users, groups and privilege Scheduling work: cron and systemd timers
Last refreshed 2026-09-18.