I update my dotfiles several times a week. Nineteen commits in the last seven days, fifty-three in the last thirty. That isn't tidiness — it's that the way I build changes every time I hit a limit in the systems I use to get work done, and the fix almost always lands in a .-prefixed file.
Dotfiles are the saved configuration I want every machine in my network to share. Everyone is talking about "cloud agents" right now. I run mine in my own small cloud: a MacBook Pro, a Mac Mini, a WSL box, and a couple of Linux VPSes, stitched together with a Tailscale mesh. What makes that fleet feel like one computer instead of five is a single git repo checked out at ~/.dotfiles on all of them.
[TIMELINE ARTIFACT — placeholder. Milestone data is in the notes; render later.]
How it started, how it grew
I made the first commit in October 2022. It started as a way to preserve some desired state across a laptop upgrade: aliases I'd built muscle memory for, a zshrc I didn't want to reconstruct from memory. I'd been aware of people sharing their dotfiles for years before I did it myself. It took a while for the power of these little files to click. Once it did, I started committing.
On New Year's Eve 2022 I added dotbot and a go script that picked the right config for the machine. For two years the repo stayed small: six commits in 2022, sixteen in 2023, fifty-nine in 2024. It was a personal backup with a nice installer.
Then the agents arrived. The first AI-flavored commit was February 2025 (commit-message generation, of course). In August 2025 I committed a config/claude/ directory — Claude Code settings, a global CLAUDE.md, a notification hook — and the repo changed jobs. It stopped being a backup of my shell and became a control plane for everywhere my agents run. In late 2025 I split the single install config into per-machine layers. In early 2026 the Mac Mini became a headless server, Tailscale entries showed up in the SSH config, then the VPSes. By July 2026 the repo was carrying herdr configuration, launchd agents, and a growing set of docs written for agents to read before they act.
The commit count tells the story better than I can: 211 commits in 2025, 327 so far in 2026. Every one of those is a small adjustment to how a machine, or the agent on it, behaves.
Why bother
Three reasons, in the order I felt them.
New machines are cheap. A fresh laptop, a reinstalled VPS, a new Mac Mini: clone the repo, run ./go, walk away. When a system goes down, the tooling I rely on every day comes back from scratch. This got dramatically easier once agents could drive the install. My repo now ships an unattended installer that hands a failed stage's log to Claude Code, with a small plugin that teaches it the stage layout and the usual failure modes, and lets it finish the job. "Self-healing" is a big word for it, but a machine that can debug its own setup is a real change from a machine that can't.
Muscle memory is preserved. gco, brs, cdx — the aliases are the same on every box, so my hands don't have to know which machine they're typing on. Neither do my agents. A CLAUDE.md rule I write on the laptop is in force on the VPS on the next pull.
There is one place to make a decision. Personalization that matters (git identity, SSH hosts, prompt, editor, the agent's global instructions, the port registry per host) lives in one repo with one history. When I change my mind about how I want to work, I change it once and every machine inherits it. This is the property that turned a backup into a control plane: coherence across the fleet is a property of the repo, not of my memory.
My taste: dotbot, with an escape hatch
I tried a few dotfiles frameworks before settling on dotbot. It won on simplicity and safety: it does symlinks, directories, cleanup, and shell commands, declared in YAML, and very little else. The declarative part is the point. A config file says "this path in $HOME points at this path in the repo," and dotbot makes it true, idempotently, on every run.
- link:
~/.zshrc: zsh/mac_zshrc.zsh
~/.config/nvim:
create: true
path: config/nvim
~/.claude/CLAUDE.md: config/claude/CLAUDE.mdThe second thing that matters is composition. My installer runs a chain of configs per machine: common on everything, then mac on the Macs, then macbookpro or macmini for the specific box. Linux servers get common plus their own layer. Adding a machine is a new YAML file and one line in a table.
The third is the escape hatch. Declarative state gets you most of the way, but real setups have imperative corners: rendering a launchd plist, building a binary, running an interactive tool installer. dotbot's shell directive is where those live, right next to the links they support:
- shell:
- command: scripts/bash/install-shottr-launchd.sh
description: Render and load the screenshot-rename launchd agentI keep that list short on purpose. If a shell step grows, it becomes a script with a name, and the config just calls it.
This week I made two changes while thinking about this post. Homebrew packages now install through the dotbot-brew plugin, so a brewfile: directive sits in the same config as the links it needs, and the per-machine composition applies to packages too. And the monolithic go script became four small stages — detect, submodules, dotbot, verify — plus a full-install.sh that runs them unattended and calls in an agent when one fails. Fable 5.1 shipped this week; the repo got easier for it to work in as a direct result of writing this down.
I'd encourage you to try the other frameworks too. That curiosity is what builds taste for how you want to work, with or without agents, and taste is why I use dotbot.
Where the agents plug in
Two more things live in the repo that wouldn't have three years ago.
herdr is the terminal workspace manager I run my agents inside: workspaces, tabs, panes, and a CLI that lets one agent start, prompt, and read another. Its config is a dotbot link like any other, so a new Mac gets the same layout and keybindings I've tuned on the laptop. (This post was written that way: one Claude session controlling a second one in a split pane that did the dotfiles engineering.)
The other is voice. [VERIFY — Matt to correct the specifics] I talk to my machines through ChatGPT's realtime voice over a remote connection, and the pieces that make that work are configured from the same repo. That deserves its own post; a stub is waiting.
Try it
I pulled the pattern out of my repo into a starter you can clone: dotbot and dotbot-brew vendored as submodules, per-machine config composition with two example machines, the shell escape hatch, the staged installer, and the Claude Code plugin that lets an agent repair a broken install.
[LINK — github.com/mjherich/dotbot-starter, pending publish]
git clone https://github.com/mjherich/dotbot-starter ~/.dotfiles
cd ~/.dotfiles
./go # interactive
./full-install.sh # unattended, hands failures to Claude CodeRename the example machines, point the links at your real files, and commit. Then notice how often you come back to it. That frequency is the signal: your dotfiles are where the way you work actually lives.