One morning I was glancing over the diffs across three repositories and noticed a file I did not recognize sitting in each of them: next.config.ts.bak. Tracing the commit history, I found that the previous night's redirect-integrity pass had committed and pushed that backup right alongside the real .ts file.
The build was green. The sites were serving. And that is exactly why nothing stopped it.
I have been an indie developer since 2014, running apps and blogs in parallel, and at Dolice Labs I update four technical blogs every day through scripts driven by Claude Code. As you automate the "mechanical but scary if it breaks" chores — formatting, link fixes — this kind of silent contamination becomes the real hazard. Mistakes you make by hand are easy to catch. The junk that automation carries along, quietly, is not.
What actually slipped in — the backup that --fix leaves behind
The trigger was a homegrown integrity checker. It inspects whether redirects for deleted articles collide with the MDX of articles that have since come back, and I had it auto-correcting with a --fix flag.
Before rewriting, the script saves the original next.config.ts as next.config.ts.bak in the repo root. A safety backup. The problem was the commit step that ran right after it.
# What the tail of the pipeline was doing
python3 redirect_integrity.py "$REPO" --fix
git add -A # ← this sweeps up the .bak too
git commit -m "Fix redirect integrity"
git push origin maingit add -A stages every change in the working tree. The freshly created next.config.ts.bak is, of course, a "new file," so it gets added with no warning at all. A .bak is harmless to a Next.js build, so neither CI nor Cloudflare lights up red. The result: backup files took up residence in three repos — gemilab, antigravitylab, and rorklab.
Why it is so hard to notice
The essence of this bug is that nothing breaks.
One extra .bak file, and the site still renders fine. Tests still pass. No error appears in the logs. The repo just gets slightly dirtier, and that dirt does not clean itself up — it only gets overwritten the next time the same --fix runs. It accumulates, quietly.
Worse, a .bak is almost identical to the original, so even staring at a diff later, it rarely jumps out as "wait, what is this?" I only caught it because I happened to be comparing diffs across three repos at once and saw the same filename lined up three times.
In automation, an exit code of 0 and a green log do not mean "correct." The fact that git push raised no error and the fact that the repository matches your intent are two different things. Seen as the same class of "silent failure," git push reporting success while nothing reaches the remote shares the same root: in automation, the green log is precisely the thing to distrust.
Reproducing it
You can reproduce it in a few seconds.
mkdir -p /tmp/bak-demo && cd /tmp/bak-demo
git init -q
echo "export const config = {}" > next.config.ts
git add next.config.ts && git commit -qm "init"
# A classic in-place tool that leaves a .bak
sed -i.bak 's/{}/{ trailingSlash: true }/' next.config.ts
ls
# -> next.config.ts next.config.ts.bak
git add -A
git status --short
# A next.config.ts.bak <- staged without you asking
# M next.config.tssed -i.bak is a common form on macOS, and it always leaves a .bak. GNU sed's sed -i (no suffix) makes no backup, but I genuinely see sed -i '' on BSD/macOS mistyped into sed -i.bak in the field all the time. Homegrown --fix scripts often write a backup for safety too, and they step on the same trap.
The fix — decide what to stage by addition, not subtraction
The most reliable move is to stop using git add -A and instead list exactly what you want to commit.
# Before: everything, including the .bak
git add -A
# After: add only the paths you are allowed to touch
git add next.config.ts content/
git status --short # eyeball it for strays
git commit -m "Fix redirect integrity"For a blog-update pipeline, name only the directory you generate or edit, like git add content/. For a process that touches a config file, write that filename directly. "Add everything, then remove the extras" causes far more accidents than "add only what you need."
If you have a real reason to keep git add -A, clean up the backup right after the fix tool and before staging.
python3 redirect_integrity.py "$REPO" --fix
rm -f "$REPO"/next.config.ts.bak # remove the saved copy first
git add -APreventing recurrence — close it structurally with .gitignore
Cleanup and scoped adds are "breaks the moment you forget" rules. A human turning the crank by hand can remember; a pipeline that runs every day will quietly relapse the first time the rule slips. The proper fix is structural.
Put the backup extensions into the repo's .gitignore, and they will never be staged again, even if you use git add -A.
# Backups left by in-place fix tools
*.bak
*.orig
*.tmp
*~Drop the same block into all four repos, and no matter which script leaves which backup, it is excluded from commits automatically. If you have ever wrestled with how ignore rules interact with file lookups, Claude Code reporting that a .gitignore'd file cannot be found is worth reading alongside this.
As an extra layer, add a guard that mechanically rejects "unexpected files" after staging and before commit. Dropping these few lines at the tail of the pipeline lets you halt the moment a saved file like a .bak sneaks in.
# Right before commit: abort if any backup is staged
if git diff --cached --name-only | grep -qE '\.(bak|orig|tmp)$|~$'; then
echo "Stray backup file is staged. Aborting commit."
git diff --cached --name-only | grep -E '\.(bak|orig|tmp)$|~$'
exit 1
fi
git commit -m "Fix redirect integrity"The less human oversight a process has, the more this kind of final checkpoint earns its keep. Since adding these lines, I have not repeated the same contamination once.
A .bak that already slipped in will not vanish just because you added it to .gitignore. You have to stop tracking it separately.
git rm --cached next.config.ts.bak # keep history, drop tracking
git commit -m "Remove stray backup file from tracking"Make "additive add" the default for automation
This contamination grew out of a single way of writing one commit command. git add -A is convenient when you are working interactively, but in an automated pipeline with no human in the loop, I believe an explicit git add <path> should be the default. Even when you wire up something that lets Claude Code generate commit messages, you want to keep your own hand on what gets staged (I touch on that design in operating auto-generated commit messages).
Automation faithfully repeats the right steps. By the same token, it faithfully repeats the wrong ones. The one decision I want to keep in my own hands, not hand to the machine, is what actually goes into the repository.
I hope this helps anyone wrestling with the same problem.