My personal configuration files, managed with GNU Stow.
In this repo I keep track and backup my dotfiles. Each top-level directory is a
stow package whose contents mirror the layout of my home directory, so
tmux/.config/tmux/tmux.conf gets linked to ~/.config/tmux/tmux.conf.
Packages are installed individually, which means a machine only gets the configs it actually needs.
brew install stow # or: apt install stow
git clone <this repo> ~/dev/dotfiles
cd ~/dev/dotfiles
stow zsh sh tmux tig ghostty nvim git zedOn a work machine, also create ~/.config/git/config.work with the work
identity — git/.gitconfig pulls it in via includeIf for ~/dev/centralo/:
[user]
email = you@work.example
name = Your Name.stowrc already sets --target=~ and --no-folding, so no flags are needed.
--target=~is required because stow defaults to linking into the parent of the repo, which would be~/devrather than~.--no-foldingmakes stow link individual files rather than replacing a whole directory with a single symlink. This keeps directories like~/.config/tmuxreal, so untracked things living beside the tracked files (the tmuxplugins/dir, zed'sprompts/database) don't end up inside this repo.
| package | contents |
|---|---|
zsh |
.zshenv, .zshrc, .zprofile |
sh |
.profile — shared PATH setup, sourced by both the zsh and bash chains |
tmux |
tmux.conf and the tmux-sessionizer script |
tig |
.tigrc |
ghostty |
terminal config and theme |
nvim |
LazyVim config, including lazy-lock.json |
git |
.gitconfig and the global ignore file |
zed |
settings.json only (the prompts DB is runtime state) |
vim |
.vimrc — legacy, kept but not usually installed |
bash |
.bashrc, .bash_profile — legacy, kept but not usually installed |
stow <package> # install
stow -D <package> # uninstall
stow -R <package> # reinstall, after adding or renaming files
stow -n -v <package> # dry runStow refuses to overwrite a real file, so a conflict is reported rather than
silently clobbered. Note that it also ignores .gitignore and .git by
default, so those are never linked.
zsh/.zprofile sources .zshrc for non-interactive login shells. It works and
it is now guarded so it no longer double-loads, but it is still a workaround; the
reason it exists is commented in the file itself.
Reminder of the model — the two flags are independent, which is the whole source of the confusion:
| shell | files read | example |
|---|---|---|
| login + interactive | zshenv, zprofile, zshrc | a terminal window, ssh login |
| login + non-interactive | zshenv, zprofile | zsh -lc — how IDEs probe PATH |
| non-login + interactive | zshenv, zshrc | VSCode's integrated terminal |
| non-login + non-interactive | zshenv only | zsh -c — git hooks, cron |
Where this should end up:
.zshenv— static env and PATH, including version-manager shim dirs. The only file that fixes the last row, so plain script shells stop being a special case..zshrc— interactive only: oh-my-zsh, aliases, prompt, thenvmfunction and itschpwdhook, completions..zprofile— login-only, and on macOS the right place to re-assert PATH priority (see below). Should no longer need to source.zshrcat all.
Two things that make this more than a tidy-up, so budget for them:
- macOS
path_helper./etc/zprofilerunspath_helper, which rebuilds PATH with the system paths in front, and it runs after~/.zshenv. Measured: a directory prepended in.zshenvends up at position 13 in a login shell. So.zshenvalone is not enough; priority has to be re-asserted in.zprofile.typeset -U pathin.zshenvis what keeps that from duplicating entries. - nvm. The
nvmfunction is too slow to load in.zshenvfor every script, so script shells need the default node'sbinon PATH by some other route (a$NVM_DIR/currentsymlink, or a shim).
The dead asdf, sdkman and s3-utils blocks have since been removed and rbenv init is now guarded, so zsh/.zshrc holds only things that actually run.