My fix reverted itself after an update, four times, and the grep that guarded it missed a third of the damage
A settled configuration decision came back undone in eight files after a routine vendored-directory sync, and nothing noticed for two days. Three more instances of the same shape followed: a plugin cache, a set of generated git hooks, and a marketplace bundle. If your fix lives in a file someone else owns, it is not a fix, it is a lease.
TL;DR · THE FIX
Any file you edit that a tool also writes will be overwritten, and the overwrite is silent because it arrives inside a routine update. The four cures, in order of how well they hold: move the fix to a file you own (override at the call site instead of editing a vendored one), patch the generator rather than the generated output, keep an explicit list of vendored files carrying local edits, and guard the rule with a grep you run before committing any sync. The grep is the weakest of the four, and mine missed four of twelve violations for weeks because it required a keyword prefix that the code did not always use.
The symptom
There is a rule in my setup that a particular model is never used for dispatched work. I had enforced it, committed the fix, and moved on. Weeks later I ran the check for an unrelated reason:
$ git grep -n "^model: sonnet" -- '*.md'
.agents/skills/workflow-builder/agents/researcher.md:3:model: sonnet
.agents/skills/workflow-builder/agents/drafter.md:3:model: sonnet
.agents/skills/workflow-builder/agents/critic.md:3:model: sonnet
... 8 hits
Eight files, all of which had been fixed. The fix was in the history with a commit message, and the working tree did not have it. Nothing had failed or warned. The setting had gone back to what it was two days earlier, and I only noticed because I happened to look.
What was happening
Two commits, three weeks apart:
f6d115d remove model pin from workflow-builder agents
ebb0cc4 sync .agents/skills from upstream
ebb0cc4 re-synced the whole .agents/skills/ directory from upstream, which is a routine and correct thing to do. It is also the whole bug: a vendored directory is upstream’s file, and “sync” means “discard local”. The problem was where I had put the fix. A file that another process rewrites on a schedule gives any edit an expiry date, and I did not know mine.
Three more of the same thing
Once I had the shape, it was everywhere.
A plugin’s cached agent frontmatter. Several installed plugin agents hardcode the model I do not want, so I hand-edited the plugin cache. The next plugin update erased every edit, and on the way it dirtied the git state of the marketplace clone the cache is built from, so the update after that warned about local changes to files I had no business touching. The fix is to stop editing that file. The dispatch call takes an explicit model parameter, and a parameter at the call site outranks the agent’s own frontmatter:
// wrong: edit ~/.claude/plugins/cache/<plugin>/agents/foo.md
// right: override where the work is dispatched, in a file I own
Agent({ subagent_type: "codex:codex-rescue", model: "opus", prompt: "..." })
The proof that the override wins:
$ grep -rl "^model: sonnet" ~/.claude/plugins/cache/
# still finds the pins
# and dispatches still run the model I asked for
The pins are still there and no longer matter.
Generated git hooks. I had a fix for Windows console windows popping up on every commit. It went into .git/hooks/post-commit and .git/hooks/post-checkout, it worked, and it came back a week later. The package that installs those hooks regenerates them on upgrade from a template inside its own site-packages directory, so the fix had to land in three places: both hooks and graphify/hooks.py, the generator. I only found the generator on the second pass, after the symptom returned and I went looking for what could be rewriting a file in .git/. A patch to a generated file lasts until the next regeneration. Patch the generator, and write down the symptom that means it came back, because you will see the symptom again long before you remember the cause.
A marketplace bundle. I install a curated set of skills file by file rather than taking the publisher’s bundle. That looks like pointless friction until you take the bundle once: it re-adds everything you pruned and overwrites every local patch, in one command, with a success message. One skill in that set carries a local patch a full refresh would silently discard.
The fix, in order of how well it holds
- Move the fix into a file you own: a call-site override, a config layer, a wrapper, an environment variable. This is the only durable option, and it is available far more often than it first appears. Ask what the last thing is that reads this value before it matters, and put the fix there.
- If it must live in generated output, patch the generator, then check the generated output too, because the copy on disk is stale until something regenerates it.
- Keep an explicit list of vendored files carrying local edits, in a file in the repo, so a refresh becomes a conscious re-apply instead of a silent loss.
- Guard it with a grep you run before committing any sync. This is the weakest of the four and the one people reach for first.
The guard was wrong too
I had the grep. It was documented, it was in the project rules, and it returned zero.
git grep -n "^model: sonnet" -- '*.md'
git grep -nE "model: *'?\"?sonnet" -- '*.js' '*.ts' '*.py' '*.yaml' '*.json'
Both patterns require the token to follow a model: key. When I re-measured properly the real count was twelve, and the four extra looked like this:
plan.append(("draft angles", "sonnet", "..."))
Passed positionally, with no model: anywhere near it. Both documented checks walk straight past that line, and they had been reporting clean the whole time. The third pattern, the one that should have been there from the start, matches the value instead of the keyword, because the value is what reaches the runtime:
git grep -nE "['\"]sonnet['\"]" -- '*.js' '*.mjs' '*.ts' '*.py'
A grep written from how you remember the code looking will match the shape you had in mind and miss every other spelling of the same mistake. The way to test a guard is to break the rule on purpose, in the ugliest form you can think of, and confirm the guard catches it. Mine had only ever run against a clean tree, had returned zero for weeks, and was missing a third of the problem.
The lesson
If a file is written by anything other than you, your edit to it is temporary, and you find out either from a grep or from the bug coming back.
What makes this nastier than an ordinary regression is that the revert arrives inside a routine, correct, desirable action. Nobody is suspicious of sync from upstream or plugin update or pip install --upgrade. The diff is enormous and mostly legitimate, so nobody reads it, and the reverted setting usually breaks nothing loudly. Mine sat for two days and could as easily have sat for two months.
The rule I follow now costs about fifteen seconds: before committing any vendored sync, plugin update, or dependency bump, grep the incoming diff for the exact string you last fixed. The commit is the last cheap moment.
Discussion
Powered by GitHub. Sign in to leave a comment.