Skip to content
woza.ink
← Blog
4 min read

Dotfiles: Version Control Your Environment

Your shell config, editor settings, and tool preferences are code. Treat them like it.

On this page
  1. What dotfiles are
  2. The naive approach and why it breaks
  3. stow
  4. Setting up from scratch
  5. What to put in there
  6. The compounding return

Every time you set up a new machine, you have two options. Spend a day and a half reinstalling things, tweaking configs, and trying to remember that one zsh alias you spent twenty minutes perfecting. Or clone a repo and be back to your exact environment in ten minutes.

Dotfiles are how you get the second option.


What dotfiles are

Dotfiles are the config files that live in your home directory, named with a leading dot so they stay hidden by default. .zshrc, .gitconfig, .vimrc, .config/nvim/, .ssh/config. Every tool you use has one.

Together they define your environment1 — how your shell behaves, how your editor looks, what aliases you've built up, how git formats output. They're personal infrastructure. Most developers let them accumulate on a single machine and lose them when something breaks.

Versioning them in a git repo means your environment is reproducible, diffable, and shareable.


The naive approach and why it breaks

The obvious move is to make ~ a git repo. It works until it doesn't — git status in your home directory becomes noise, you accidentally commit things you shouldn't, and the repo scope bleeds everywhere.

The slightly better approach is symlinking: keep your dotfiles in ~/dotfiles/, and manually ln -s ~/dotfiles/.zshrc ~/.zshrc for each file. This works. It's also tedious to maintain, easy to forget a file, and annoying to replicate on a new machine2.


stow

GNU Stow is the standard solution to this. It's a symlink manager — it takes a directory tree and creates symlinks into a target directory3 (your home folder by default), mirroring the structure exactly.

You structure your dotfiles repo like this:

~/dotfiles/
├── zsh/
│   └── .zshrc
├── git/
│   └── .gitconfig
├── nvim/
│   └── .config/
│       └── nvim/
│           └── init.lua
└── ghostty/
    └── .config/
        └── ghostty/
            └── config

Then running:

cd ~/dotfiles
stow zsh

Creates ~/.zshrc → ~/dotfiles/zsh/.zshrc. One command per package. Or stow */ to stow everything at once.

The package structure (one folder per tool) means you can selectively stow only what you need on a given machine — a server gets git and zsh but not nvim or ghostty4.

To unstow (remove the symlinks):

stow -D zsh

To restow (useful after moving files around):

stow -R zsh

That's basically the whole API.


Setting up from scratch

Install stow — it's in every package manager:

# macOS
brew install stow

# Ubuntu / Debian
apt install stow

Create your dotfiles repo:

mkdir ~/dotfiles
cd ~/dotfiles
git init

Move a config file into the right package folder, then stow it:

mkdir zsh
mv ~/.zshrc zsh/.zshrc
stow zsh
# ~/.zshrc is now a symlink to ~/dotfiles/zsh/.zshrc

Push to GitHub. On a new machine:

git clone git@github.com:you/dotfiles.git ~/dotfiles
cd ~/dotfiles
stow zsh git nvim

Done.


What to put in there

The obvious ones: .zshrc / .bashrc, .gitconfig, editor config, terminal config.

A few less obvious ones worth including:

  • .gitignore_global — a global gitignore for .DS_Store, .env, editor swap files
  • .ssh/config — host aliases and per-host settings (not the keys themselves, obviously)
  • shell functions and aliases — if you've split these into separate files sourced from .zshrc
  • .config/ghostty/config or whatever terminal you use
  • brew bundle — a Brewfile that lists everything installed via Homebrew5, so brew bundle restores your packages on a new machine

What to leave out: secrets, tokens, anything machine-specific that shouldn't roam.


The compounding return

The lasting benefit is the habit of treating your environment as code6. You start reviewing changes before committing them. You write comments explaining why an alias exists. You see the history of how your workflow evolved.

It's also a surprisingly good signal when evaluating tools — if something doesn't have a clean, file-based config, that's worth noting7.

Notes

  1. 1Note

    The most-starred dotfiles repo on GitHub is mathiasbynens/dotfiles with 31K+ stars. Its .macos script that sets "sensible hacker defaults" for macOS is legendary -- it automates hundreds of system preferences that would take hours to configure manually. Other influential repos: holman/dotfiles (topic-based organization) and thoughtbot/dotfiles (vim/zsh/tmux/git baseline with 8K+ stars).

    source →
  2. 2Note

    thoughtbot built rcm specifically to solve the dotfile management problem without requiring the directory-mirroring structure that Stow demands. It handles host-specific and tag-based overrides natively. Their dotfiles repo is designed as a shared baseline that the whole team uses, with personal overrides layered on top via ~/.dotfiles-local.

    source →
  3. 3Context

    GNU Stow was originally written in 1993 by Bob Glickstein for managing software installed from source in /usr/local/stow/. Its use for dotfiles management is a creative repurposing of a tool designed for a completely different problem. The entire implementation is a single Perl script with no dependencies beyond core Perl -- which is why it's available in every package manager and never breaks.

    source →
  4. 4Counter

    For more complex setups, chezmoi solves problems Stow cannot: templating (different email in .gitconfig per machine), secrets management (integrates with 1Password, Bitwarden, etc.), and cross-OS differences. The trade-off is that chezmoi uses a source-of-truth copy rather than symlinks, so edits require chezmoi edit instead of editing files directly. Stow wins on simplicity; chezmoi wins on flexibility.

    source →
  5. 5Note

    A Brewfile is one of the highest-leverage files you can add. Run brew bundle dump to snapshot everything currently installed, then brew bundle on a new machine to restore it all. Combined with Stow for configs, this covers ~90% of machine setup. The pattern: Brewfile for packages, dotfiles for configs, and a small bootstrap script to glue them together.

  6. 6Context

    The next evolution beyond dotfiles + Stow is Nix with Home Manager. Instead of managing symlinks to config files, you declare your entire environment -- packages, configs, shell setup -- in a single Nix expression. Builds are reproducible down to the byte: the same flake.lock produces the exact same environment on any machine. The learning curve is steep, but for developers who manage multiple machines or want CI-testable environments, it is the endgame.

    source →
  7. 7Quote

    Unlike dotfiles repositories, Home Manager supports specifying programs as well as their configurations. Contents are reproducible -- a home will be the exact same every time it is built, and you can have the exact same home on different hosts.

    — Home Manager documentation

    source →