Skip to content

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.

Worth tracking:

  • ~/.zshrc and friends
  • ~/.tmux.conf
  • ~/.config/starship.toml
  • ~/.gitconfig
  • Your terminal’s config — ~/.config/ghostty/config, wezterm.lua, alacritty.toml and so on, depending on which one won your heart
  • Editor config, if you can face the commitment

Keep out:

  • Secrets. API tokens, ~/.ssh/, ~/.aws/, .env files. 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.
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 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.

  1. Create the bare repo:

    Terminal window
    git init --bare -b main ~/.dotfiles
  2. Add an alias so you can talk to it, and put the same line in your ~/.zshrc so it survives. Call it dotfiles, or dots, or anything but git, for reasons that should be obvious:

    Terminal window
    alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
  3. 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
  4. Track your first files:

    Terminal window
    dotfiles add ~/.zshrc ~/.tmux.conf ~/.config/starship.toml
    dotfiles commit -m "Initial dotfiles"
  5. Push to a private GitHub repo — private, because dotfiles have a way of accumulating things you’d rather not open-source:

    Terminal window
    dotfiles remote add origin [email protected]:you/dotfiles.git
    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:

Terminal window
dotfiles status
dotfiles add ~/.zshrc
dotfiles commit -m "Tweak history settings"
dotfiles push

This is the moment the whole exercise exists for:

Terminal window
git clone --bare [email protected]:you/dotfiles.git ~/.dotfiles
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
dotfiles checkout
dotfiles config --local status.showUntrackedFiles no

One 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.

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.local

For the shell:

Terminal window
# ~/.zshrc (tracked)
[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local

And 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.

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.

Terminal window
brew install chezmoi
chezmoi init
chezmoi add ~/.zshrc
# later, on a new machine:
chezmoi init --apply [email protected]:you/dotfiles.git

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:

Terminal window
mkdir -p ~/dotfiles/zsh
mv ~/.zshrc ~/dotfiles/zsh/
cd ~/dotfiles && stow zsh # ~/.zshrc is now a symlink into the repo

Worth it if you like each app’s config in its own directory and don’t mind living life symlinked.

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.