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 MYVAR
echo "$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 PATH wins. 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 export exists 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 typebash readszsh 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"
EOF
bash -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 .bashrc is not slowed or broken by prompts and colours.
  • A startup file that fails midway leaves the whole shell subtly broken — append an echo marker 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.

Users, groups and privilege Scheduling work: cron and systemd timers

Last refreshed 2026-09-18.