Manage Your Dotfiles with Nothing but Git
You’ve put real effort into that setup by now. The .zshrc you finally
understand, the tmux
config, the Starship
prompt — hours of tuning, all of it sitting on
exactly one machine, one fat-fingered rm or dead SSD away from oblivion. And when the next
machine arrives, you’ll recreate it from memory, badly, over about a month.
Dotfiles are code, and code belongs in version control. The good news: if you have git, you already have everything you need.
What to track (and what to keep out)
Section titled “What to track (and what to keep out)”Worth tracking:
~/.zshrcand friends~/.tmux.conf~/.config/starship.toml~/.gitconfig- Your terminal’s config —
~/.config/ghostty/config,wezterm.lua,alacritty.tomland so on, depending on which one won your heart - Editor config, if you can face the commitment
Keep out:
- Secrets. API tokens,
~/.ssh/,~/.aws/,.envfiles. More on this below. ~/.zsh_history. It’s a log, not config — and it contains everything you’ve ever typed, including that token you pasted at 2am.- Caches and clones. The zsh plugins you
git cloned in the minimal setup are someone else’s repository; don’t vendor them into yours. Note the install commands in a README instead.
Three ways to do it
Section titled “Three ways to do it”| Approach | Extra tooling | Best for |
|---|---|---|
| Bare git repo | None — just git | Most people; one config set, minimal fuss |
| GNU Stow | stow |
Symlink tidiness; one package directory per app |
| chezmoi | chezmoi |
Multiple machines, per-OS differences, secrets |
The honest recommendation: start with the bare repo. It takes five minutes, uses tooling you already trust, and you can graduate to chezmoi later without losing anything.
The bare-repo method
Section titled “The bare-repo method”The trick is a bare repository — one with no working tree of its own — because your entire
home directory plays that role. No symlinks, no copying files into place; the real ~/.zshrc
is the tracked file.
-
Create the bare repo:
Terminal window git init --bare -b main ~/.dotfiles -
Add an alias so you can talk to it, and put the same line in your
~/.zshrcso it survives. Call itdotfiles, ordots, or anything butgit, for reasons that should be obvious:Terminal window alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME' -
Stop git helpfully reporting that your entire home directory — all several hundred thousand files of it — is untracked:
Terminal window dotfiles config --local status.showUntrackedFiles no -
Track your first files:
Terminal window dotfiles add ~/.zshrc ~/.tmux.conf ~/.config/starship.tomldotfiles commit -m "Initial dotfiles" -
Push to a private GitHub repo — private, because dotfiles have a way of accumulating things you’d rather not open-source:
Terminal window dotfiles push -u origin main
Day to day, it’s ordinary git with a funny name — and unlike ordinary git, it works from any directory, because the work tree is always your home:
dotfiles statusdotfiles add ~/.zshrcdotfiles commit -m "Tweak history settings"dotfiles pushSetting up a new machine
Section titled “Setting up a new machine”This is the moment the whole exercise exists for:
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'dotfiles checkoutdotfiles config --local status.showUntrackedFiles noOne gotcha: if the machine already has a stock ~/.zshrc, git quite reasonably refuses to
trample it — error: The following untracked working tree files would be overwritten by checkout. Move the offenders aside (mv ~/.zshrc ~/.zshrc.stock) and re-run the checkout.
Then add the alias to your freshly checked out .zshrc, which is a pleasingly circular way to
finish.
Keeping secrets out
Section titled “Keeping secrets out”Some config inevitably references things that can’t be committed. The pattern is the same everywhere: track the file, source an untracked local sibling.
For git itself, use an include:
# ~/.gitconfig (tracked)[include] path = ~/.gitconfig.localFor the shell:
# ~/.zshrc (tracked)[[ -f ~/.zshrc.local ]] && source ~/.zshrc.localAnd if a secret ever does land in a commit: rotate it immediately. Deleting the file in a later commit doesn’t remove it from history, and “private repo” is a comfort blanket, not a vault.
When you outgrow it
Section titled “When you outgrow it”chezmoi is the full-service option: templating for per-machine and per-OS differences
({{ if eq .chezmoi.os "darwin" }} and friends), and proper integrations for pulling secrets
from a password manager at apply time. Written in Go, because the Rust toolchain can’t compile
everything.
brew install chezmoichezmoi initchezmoi add ~/.zshrc# later, on a new machine:If “work laptop versus personal machine versus a homelab box running Debian” describes your life, chezmoi earns its install. If that sentence didn’t describe you, the bare repo is enough.
GNU Stow has been managing symlink farms since the early nineties and remains better at
it than most of what came after. Configs live in per-app package directories; stow zsh
symlinks the package into place:
mkdir -p ~/dotfiles/zshmv ~/.zshrc ~/dotfiles/zsh/cd ~/dotfiles && stow zsh # ~/.zshrc is now a symlink into the repoWorth it if you like each app’s config in its own directory and don’t mind living life symlinked.
Where next
Section titled “Where next”Your setup is now one git clone away from immortal. If any of the files mentioned above
don’t exist yet, that’s what the rest of the site is for: the
.zshrc, the tmux
config, the Starship
prompt, and a
terminal worth configuring in the first place.