Six Years of Dotfiles
My dotfiles repo has 1,048 commits spread over five years and nine months. If you plot them by year you get a shape that looks like a mistake: 18, then 181, then 16, then 14, then 17, then 132, then 670. I have been staring at that sequence for a while now, because it is the most honest thing my configuration has ever told me about how I actually work.
The conventional story about dotfiles is a tidy one. You start with a shell script, you graduate to symlinks, you eventually adopt a Real Tool, and thereafter your machines are identical forever. That's not what happened. What happened is that I built something in a burst, ignored it for three years while it kept working, and then came back and rebuilt most of it. The ignoring is the part worth writing down.
The Shape of the Graph
The current repo starts on 21 December 2020, and the second commit is titled "Initial project migration from 'dotfiles' repo" (so something older came before it, and this is the renovation). Eighteen commits that December, then 181 through 2021 as I built the thing out in earnest.
Then: 16 commits in 2022. Fourteen in 2023. Seventeen in 2024.
Three years of near-silence. Not abandonment, since I was using it the whole time on every machine I owned. It simply didn't need anything. A dotfiles repo that goes quiet for three years is either dead or finished, and the way you tell the difference is whether you're still running it. I was running it constantly. The quiet was the system working.
Then 132 commits in 2025, and 670 so far in 2026. The second burst is bigger than the first by nearly a factor of four, and it's mostly demolition, which I'll come back to.
What It Looked Like in 2020
The original README opens with "Brad's Bootstrapping & dotfiles Manager" and describes an Ansible playbook for macOS, Fedora, Ubuntu and Pop!_OS. The entry point was a shell script you fetched off the internet:
curl -O https://bradleyfrank.github.io/dotfiles/run.sh && bash run.sh
That script updated the OS, installed Ansible, and ran the playbook. There were two playbooks
(bootstrap.yml and dotfiles.yml), a single role called
dotfiles, and files organised underneath it by category (i.e. cli,
fonts, git, media, scripts). Ninety-six files
in total, and a good fraction of those were Source Code Pro in fifteen weights.
Two things about that design have aged well and one hasn't. The good ones: it was Ansible from the start, and it was declarative from the start. I never went through the symlink-farm era, and I have never once had to answer the question "is the file in my home directory newer than the file in the repo?", because the repo is the source and the machine is the output. That is a one-way street by construction.
The part that hasn't aged is curl | bash off GitHub Pages as the bootstrap path.
It worked, and it was a defensible trade at the time (you need some way to get Ansible
onto a machine that has nothing on it), but the modern equivalent is a checked-in
setup script you run after cloning, which is both less exciting and much easier to
audit.
One Repo, Two Jobs
The biggest structural change was not in the workstation config at all. For years I kept a
second repo called home-servers for the homelab boxes, because I assumed provisioning
a server and configuring a laptop were different problems. They're really not. They are the same
problem with different variables, and keeping them apart meant maintaining two inventories, two
sets of conventions, and two places for a fix to land.
That repo is now superseded, and the survivor does both jobs from one set of roles: bring a
fresh macOS or Linux desktop up right after the OS install, and provision headless homelab boxes,
VPSes and disposable VMs. Everything is pushed from the workstation, which is where the secrets
live. Seven roles now (base, backups, cloud,
onepassword, rsync_net, selfhosted, virt), 539
files, and a just driver in front of the whole thing.
Merging them exposed something I had not expected. Most of what I thought was "workstation-specific" was simply untested on a server, and vice versa. The consolidation found bugs by forcing the same code down both paths.
Writing Down Why
The highest-leverage thing I have added is the least technical. There are eighteen architecture
decision records in docs/decisions/, in lightly-adapted MADR format, and the rule for when to write one is narrow
on purpose: a choice between two-plus real alternatives where the losers had merit, a non-obvious
convention, or a shift away from a previous approach.
That last category earns its keep. The README for the decisions directory puts it better than I can paraphrase:
Never delete or edit an old accepted ADR; write a new one that supersedes it. The record is the value.
The failure mode this prevents is specific, and I've hit it repeatedly: coming back to a decision after eighteen months, seeing only the outcome, deciding the outcome looks wrong, and re-litigating it without the context that made it right. In a solo repo there is nobody to ask. The record is the person you would ask.
My favourite example is the ninth one, because it converts a bug report into a design stance. A
code review flagged that the package-manager dispatch has no dnf.yml, so any distro
resolving to plain dnf instead of dnf5 (older Fedora, or anything in the
RHEL family) fails the include with "file not found." A real fragility, correctly identified.
The fix was not to add the missing file. The fix was to write down that this repo supports only current OS releases (latest Fedora, latest Ubuntu LTS, latest Debian stable, macOS N and N-1) as a rolling window that moves with upstream. The gap is intended. Marking that review finding WONTFIX without the record would have been a shrug; with it, the boundary is documented and the next reviewer, who is probably me, doesn't have to rediscover it.
The Demolition Year
Which brings me back to those 670 commits. Scanning the structural ones, the verbs are telling:
absorb, retire, consolidate, replace, split. The
Docker role was absorbed into base as an ungated task group. The LLM role was
absorbed as a gated one. The Icinga monitoring stack was retired outright and its workarounds
unwound. The weekly Docker prune was retired in favour of the update tool's own cleanup. A notes
repo I had carried for years was dropped.
Almost none of that is new capability. It is the repo getting smaller in concept while getting bigger in file count (fewer moving parts, each doing more). I couldn't have done this work in 2021, because in 2021 I didn't yet know which of those parts I would actually keep using. Three quiet years of just running the thing is what produced the evidence.
There is a general lesson here that reaches past dotfiles. You can't refactor toward the right abstraction until you have lived with the wrong one long enough to feel where it chafes. The dormant years look like neglect on a commit graph and were nothing of the sort. They were the observation period.
What I Would Still Change
I'm not finished, and two things are unresolved.
The first is that dotfiles stop at configuration. They don't install the software the config drives, and the gap between "my shell is set up exactly right" and "this machine has the tools my shell expects" still gets papered over with package lists. Nix Home Manager and Guix Home are the obvious candidates for closing it, and I keep circling without committing.
The second is that I read a very good write-up of a GNU Stow workflow last year (Stow packages for selective application, age plus git clean/smudge filters for in-repo secrets), and I have been quietly jealous of one property of it ever since. Because Stow symlinks, the file you edit is the file in the repo. There's no apply step to forget.
My setup doesn't have that property, and it's the one real cost of the copy-into-place model: edit a live config, forget to bring it back, and the machine drifts. I have been bitten by exactly that. A monitoring config on my server had been hand-edited until it no longer matched the role that templates it, which meant the next playbook run would have silently reverted the lot. I caught it, but I caught it by accident.
So the honest summary after six years is that the architecture is settled and the workflow isn't. The repo knows what it wants to be. I still occasionally forget to tell it what I have done.