cli tools

Dotfiles, Grown Up: How I Restructured My Whole Setup

6 min read

In a previous article, I wrote about GNU Stow and how it changed the way I manage my dotfiles. The idea was simple: mirror your home directory structure inside a repo, run stow <package>, and get symlinks in all the right places. It worked. For a while, it was good enough.


But "good enough" has a way of accumulating debt.


My repo grew. More tools, more configs, more edge cases. I was running stow manually on every machine, always unsure if I'd missed something. Work machines needed slightly different setups than personal ones. And I wanted to make the repo public without exposing personal details — which meant some things needed to live elsewhere. At some point I realized I wasn't really managing my dotfiles. I was just storing them.


So I did a full overhaul. Here's what changed and why.


The Old Layout

Before the refactor, the repo was flat. Tool directories lived at the root — nvim/, zsh/, tmux/, and so on. There was no single entry point. Setting up a new machine meant manually running stow on each package, installing Homebrew separately, running brew bundle by hand, and hoping you remembered everything.


It was fine when I had one machine. It stopped being fine when I had three.


The New Structure

The first change was moving everything under a stow/ subdirectory and adding a proper brew/ directory for the Brewfile.


configurations/ ├── setup.sh ├── brew/ │ └── Brewfile ├── scripts/ │ ├── brew.sh │ ├── stow.sh │ └── core/ │ └── util-functions.sh ├── stow/ │ ├── nvim/ │ ├── tmux/ │ ├── zsh/ │ ├── ghostty/ │ └── ...


setup.sh is now the single entry point for bootstrapping any machine. You clone the repo, run ./setup.sh, and it handles everything: installs Homebrew if it's missing, runs brew bundle, and stows all the packages. No manual steps, no guessing what to run next.


Profile Support: Personal vs Work

The trickier problem was work machines. My work laptop might have different SSH keys, different git identity, maybe a whole separate set of tools managed by a work-specific repo.


setup.sh now accepts two flags:

./setup.sh --profile personal ./setup.sh --profile work --work-dir ~/work-dotfiles


When you run it with --profile work, it skips personal-only packages and can delegate to a separate work repo's own setup.sh via --work-dir. The idea is that the work repo defines its own overrides without touching the personal one. Both can coexist cleanly.


This solved a real annoyance. Before, I'd either stow everything and then manually remove what I didn't want, or I'd maintain a mental list of what to skip. Neither was great.


Stow Flags That Actually Matter

Two stow flags came up during this refactor that are worth knowing about.


--restow: Instead of just symlinking, restow tears down existing symlinks and recreates them. I added this to scripts/stow.sh so re-running the bootstrap on an already-configured machine is safe — it won't leave stale links around from a previous layout.


--no-folding: This one tripped me up for a while. By default, Stow "folds" directories — if the entire target directory comes from one package, it creates a symlink to the whole directory instead of symlinking individual files inside it. That sounds fine until you have two packages that both need to write into the same directory.


For tmx and zsh, the personal repo and a work repo both need to drop files into the same target directories under .config/tmx and .config/zsh. With default folding, Stow would symlink the whole directory to the personal package, and the work package would have nowhere to put its files. With --no-folding, Stow symlinks individual files instead, so both packages can coexist in the same target directory without conflict.


The Bootstrap Scripts

The scripts layer is thin by design. Three files:


  • scripts/brew.sh — checks if Homebrew is installed, installs it if not, then runs brew bundle --file brew/Brewfile

  • scripts/stow.sh — iterates over packages in stow/ and runs stow --restow (with --no-folding for packages that need it), skipping profile-excluded ones

  • scripts/core/util-functions.sh — shared print helpers so both scripts can emit consistent colored output without duplicating the same echo calls


setup.sh just parses the flags and calls into these scripts in order. Keeping the logic split this way means you can also run each script independently — useful when you only want to update Homebrew packages without re-stowing everything.


What This Actually Feels Like Now

Setting up a new machine used to be a 30-minute process of running things in the right order, checking what was missing, and fixing whatever broke. Now it's:


git clone git@github.com:vbrdnk/configurations.git ~/configurations cd ~/configurations ./setup.sh --profile personal


That's it. Homebrew gets installed, packages get bundled, everything gets stowed.


On a work machine, I swap --profile personal for --profile work with a --work-dir pointing at the work repo, and that repo's setup.sh handles whatever's specific to that environment. The personal and work configs stay separate but both use the same underlying mechanism.


Lessons Learned

A few things I'd do from the start next time:


Use a stow/ subdirectory from day one. Having tool directories at the repo root feels natural early on but becomes confusing as the repo grows. Moving everything under stow/ makes the intent clear and separates config from scripts.


Think about the work/personal split early. I put this off longer than I should have. Adding profile support after the fact meant touching a bunch of things that would have been cleaner if designed in from the beginning. Even if you only have one machine right now, it's worth deciding how you'd handle a second one.


The bootstrap script pays for itself immediately. Writing it takes an afternoon. Setting up a new machine without it takes longer than that. It's worth it.


If you haven't thought about how you'd bootstrap a fresh machine from your dotfiles repo, it's a good question to ask yourself. The answer will probably reveal a few rough edges worth smoothing out.


Keep in touch and happy hacking, nerds!