Ramblings of an aging IT geek
← Ramblings of an aging IT geek
tooling

dotfiles that survive a new laptop in one command

The setup I finally settled on for managing dotfiles across machines, using a bare git repo and a chezmoi-style split for secrets.

A mechanical keyboard beside a terminal

I have been carrying the same shell config around for the better part of a decade, and for most of that time "carrying it around" meant a folder I tarred up, copied to the new machine, and then spent two days re-fixing the bits that had drifted. Every laptop was a slightly different snowflake. The .zshrc on this one had a fix the others never got; the SSH config on that one had a host the rest had never heard of. It was fine until it wasn't, which was always the morning I needed a working environment in a hurry.

This is the setup I finally landed on, and the reason I am writing it down is that it has now survived two clean installs without me thinking about it, which is the only test that counts.

the bare repository trick

The core of it is the bare-git-repo approach, which I resisted for ages because it sounded clever and clever things break. It is not clever, it is just a git repository whose working tree is your home directory and whose .git lives somewhere out of the way.

git init --bare $HOME/.dotfiles
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
dotfiles config status.showUntrackedFiles no

That last line is the one that makes it bearable. Without it, dotfiles status shows you every file in your home directory as untracked, which is unusable. With it, the repo only ever talks about files you have explicitly added. From there it is just git: dotfiles add .zshrc, dotfiles commit, dotfiles push. No symlinks, no stow farm, no tool to learn beyond the git you already know.

On a fresh machine the whole restore is genuinely one clone and one checkout, and you are home.

A code editor open on configuration

the bit everyone gets wrong: secrets

The reason most dotfiles repos are private, or worse, never published at all, is that they have a token or a key sitting in them somewhere. I wanted mine to be safe to make public, which forces a discipline I should have had years ago: nothing secret lives in a tracked file, ever.

The split I use is simple. Tracked files contain structure and reference variables. Secrets live in a single untracked file that is sourced at the end of .zshrc and explicitly git-ignored:

# .zshrc, near the end
[ -f "$HOME/.secrets.env" ] && source "$HOME/.secrets.env"

.secrets.env is never committed. On a new machine it is the one manual step, pulled out of my password manager. Everything else, the aliases, the prompt, the editor config, the ten years of accumulated muscle memory, comes down with the clone. I briefly evaluated chezmoi, which does this split properly with templating and an encrypted store, and it is genuinely good. For my needs the one-file source was simpler and I could explain it to myself at 2am, which is my actual bar for tooling.

machine-specific without forking

The other thing that always rotted was per-machine difference. The work laptop needs a corporate proxy; the home desktop does not. The old approach was a slightly different .zshrc on each, which is how drift starts.

The fix is one conditional include at the bottom of the shared config:

# Load a per-host file if it exists
[ -f "$HOME/.zshrc.$(hostname -s)" ] && source "$HOME/.zshrc.$(hostname -s)"

Now there is exactly one shared .zshrc that every machine runs, and a small .zshrc.workbox that only the work machine has. The shared file never knows or cares which host it is on. When I fix something in the shared part, every machine gets the fix on the next pull, and the per-host files stay tiny, which keeps them honest.

A terminal showing shell configuration

what I actually keep in there

Not everything. The temptation is to track your whole home directory, and that way lies a repo full of cache files and editor state you will fight forever. What earns a place:

  • Shell config: .zshrc, the prompt, aliases, functions.
  • .gitconfig and a global gitignore, because nothing says "new machine" like committing a .DS_Store.
  • SSH config (the hosts, never the keys).
  • Editor config, the genuinely portable parts.
  • A short bootstrap.sh that installs the handful of packages I always want, so a fresh box is one script from usable.

That is the whole thing. It is not sophisticated and it does not need to be. The win is not the cleverness, it is that the next time I open a brand new laptop, the environment I have spent ten years shaping is back in about ninety seconds, and it is the same environment everywhere, and I never have to remember which machine had the good config. After a decade of snowflakes, having them all be the same boring machine is the nicest thing I have done for my own sanity in a while.