
I run the same Claude Code setup on a small fleet of machines, and I keep it honest with a deploy script. Push a change, every box converges. At least that is what I believed, until I asked three boxes which plugins they had installed and got three different answers.
A plugin is an add-on that gives the agent new skills or commands. I had a standard set I wanted everywhere. One box had the full set. One had two of them. One had none. Nothing had errored. Nothing had warned me. The boxes just quietly disagreed about what my agents could do.
What I tried first
My first guess was that a deploy had failed partway through. So I re-ran it on the box with zero plugins and watched it finish green.
Still zero plugins.
That is when I actually read my own deploy script instead of trusting my memory of it. The script synced every config file I cared about, and it contained exactly zero lines about plugins. It had never installed a plugin in its life. Every plugin on every box had been installed by hand, once, on whatever day I happened to set that box up.
The reason I had never noticed: plugins do not live in the config files the
deploy manages. Each config directory keeps its own runtime state on disk, in
files like plugins/installed_plugins.json plus a cache of the plugin code
itself. That state survives every deploy, because the deploy is careful not to
delete things it does not know about. Surviving is not the same as being kept
correct. The deploy preserved each box’s drift perfectly.
The rule I had been violating
I already knew this rule in another costume. I wrote about it when my dotfiles started deploying themselves, one runner per box: the git repo is the source of truth, and machines converge to it. And I version-control my AI’s brain for the same reason.
Plugins sat outside both. They were state that existed only because someone, meaning me, had typed an install command into a terminal at some point in the past. No file in any repo said which plugins a box should have. So there was nothing to converge to.
That is the whole lesson, and I will name it because it applies far beyond plugins: drift by default. Any state your deploy does not explicitly own will drift by default. Not because anything is wrong, but because time passes, boxes get set up on different days, and hand-done steps are done by hand. The only states that match across a fleet are the ones something forces to match.
The fix: make the deploy own it
The fix was small. The deploy script grew an ensure_plugins step. For each
config directory on the box, it runs the plugin installer against that
directory:
CLAUDE_CONFIG_DIR=<dir> claude plugin install <name>@<marketplace>
The install is idempotent, which means running it twice is the same as running
it once. A box that already has the plugin does nothing. A box that is missing
it gets it. The step verifies by reading installed_plugins.json back and
checking the standard set is actually listed, because an install command that
prints success is a claim, and the state file is the evidence.
Now the list of plugins a box should have lives in the repo, next to everything else the deploy owns. Adding a plugin to the fleet is a one-line change and a push, not a tour of five machines.
The audit that found it was cheap
The embarrassing part is how I found the bug. No monitoring caught it. I asked the question by hand, on a hunch, while debugging something else.
So I added the question to the deploy. The same step that installs the plugins reports what it found, and a box whose state does not match the repo shows up in the deploy output instead of in a lucky hunch six weeks later. It is the same posture I landed on in the stale pointer that looked like a dead login: compare the file that is read against the file that is the source of truth, and let the difference tell you what broke.
If you run more than one machine, more than one config directory, or more than one of anything, try the hunch yourself. Pick one thing you believe your deploy keeps in sync and check two boxes by hand. If they disagree, the deploy never owned that thing. It was drifting by default, and the fix is not to be more careful. It is to make the deploy own it.