ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-07-15Intermediate

When Your Agent Commits a .bak File: Why Fix-Tool Artifacts End Up in Git

Backup files like .bak and .orig slip into commits after an agent runs a --fix tool. Here are the reproduction conditions, the real root cause, and three fixes: narrowing the staged scope, wrapping the fixer, and adding a pre-commit extension gate.

Antigravity332AI agents24Git7automation83troubleshooting108

I was scrolling through a diff after a deploy when a filename caught my eye.

next.config.ts.bak.

I had handed a config-file fix to an agent, the gates went green, and I pushed. Production was fine. That was exactly the problem — nothing broke, so nothing prompted me to look.

As an indie developer I run these repositories alone, so there is no reviewer to catch what I miss. I only spotted the file a week later, while running ls for something unrelated.

The symptom: things you did not change, sitting in your diff

Ask an agent to run a --fix tool and commit the result, and files like these tend to ride along.

ArtifactProduced byWhy it hurts
*.bakIn-place fix scriptsPreserves the pre-fix file verbatim, secrets included
*.orig / *.rejpatch, merge conflictsConfuses the next merge
*.tmp / *.swpEditors, scratch writesEnough of them and diff review stops working
__pycache__/Running check scriptsEnvironment-specific bytecode lands in the repo

In my case it was a single .bak. The build passed. Tests passed. CI stayed green.

Nothing breaking is what makes this one nasty.

Reproduction: "fixing" and "collecting" are two different tools

Three conditions have to line up.

  1. The fix tool writes a backup before editing.
  2. It does not remove that backup afterward.
  3. The staging step before the commit covers the whole worktree.

Condition three pulls the trigger. Ask an agent to "fix this and commit it" and you will usually get git add -A or git add .. The agent understands the change it made. It does not draw a line between the change and the debris the change produced.

# 1. A common portable in-place edit (works with both GNU and BSD sed)
sed -i.bak 's/foo/bar/g' next.config.ts
ls next.config.ts*
# => next.config.ts  next.config.ts.bak
 
# 2. The staging an agent tends to reach for
git add -A
 
# 3. Commit without looking at what got staged
git commit -m "Fix: config"
 
# 4. Find out afterward
git show --stat HEAD
#  next.config.ts     | 2 +-
#  next.config.ts.bak | 47 ++++++++++++++   ← not intended

sed -i.bak is the standard portable spelling because macOS ships BSD sed. The artifact exists as a side effect of caring about portability, which is a fairly ordinary thing to care about.

The root cause: .gitignore does not work retroactively

My first suspect was .gitignore. There was no *.bak entry, so one line seemed like the whole answer.

That was half right.

.gitignore only applies to untracked files. Once a file is committed and tracked, adding it to .gitignore changes nothing — you need git rm --cached to get it back out.

Here is the other half. .gitignore is a list of extensions, and anything not on the list walks straight through. I closed .bak and a month later .rej did the same thing.

The real cause is not the extension. It is the scope of what gets staged.

git add -A means "everything that changed in the worktree." In agent-driven work, what changed always contains both the deliverable and the leftovers. As long as the scope is everything, the distinction rests entirely on how fine the .gitignore mesh happens to be.

Fix 1: stage by path, not by wildcard

This was the one that mattered. I removed git add -A from the agent's instructions and named the directories it is allowed to touch.

# ✗ what lands here is decided after the fact
git add -A
 
# ✓ what can land here is decided in advance
git add content/ src/config/

The argument to add becomes a whitelist. Wherever a .bak falls, it stays out unless it falls inside a named directory — and Fix 3 covers that case.

I added one line to my agent conventions: staging is path-scoped, -A and . are not used. Eight weeks in, artifact leakage has been zero.

Fix 2: wrap the fixer so cleanup cannot be skipped

Rather than calling --fix bare, make the edit and the cleanup a single command.

#!/usr/bin/env bash
# scripts/safe-fix.sh — never let the edit and the cleanup drift apart
set -euo pipefail
 
TARGET="${1:?usage: safe-fix.sh <file>}"
BACKUP="${TARGET}.bak"
 
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
 
sed -i.bak 's/foo/bar/g' "$TARGET"
 
if [ -f "$BACKUP" ]; then
  # Keep it for rollback, but move it out of Git's line of sight
  mv "$BACKUP" "$TMPDIR/$(basename "$BACKUP")"
  echo "backup moved out of repo: $TMPDIR"
fi
 
if git diff --quiet -- "$TARGET"; then
  echo "no change: $TARGET"
fi

The trap ... EXIT means a mid-script failure leaves nothing behind. The point is that the backup is relocated rather than destroyed: you keep your local rollback, and Git stops seeing it.

Your own scripts you can rewrite. Third-party codemods you cannot. For those, append find . -name '*.bak' -newer <reference> -delete, or lean on Fix 3.

Fix 3: gate staged filenames at commit time

The last line of defense. Look only at the names of staged files and refuse the commit.

#!/usr/bin/env bash
# .git/hooks/pre-commit — stop artifacts mechanically
set -euo pipefail
 
DENY='\.(bak|orig|rej|tmp|swp)$|(^|/)__pycache__/|(^|/)\.DS_Store$'
 
# Without -z, non-ASCII paths come back octal-escaped and slip past the check
BAD="$(git diff --cached --name-only -z --diff-filter=ACM \
       | tr '\0' '\n' | grep -E "$DENY" || true)"
 
if [ -n "$BAD" ]; then
  echo "Staged artifacts detected:" >&2
  echo "$BAD" | sed 's/^/  - /' >&2
  echo "" >&2
  echo "Fix: git restore --staged <file>" >&2
  exit 1
fi

--diff-filter=ACM limits the check to additions, copies, and modifications so deletions do not trip it.

The -z is there because I got burned. My first version omitted it, and Git handed back octal-escaped strings for paths containing Japanese characters. The hook checked the wrong names and reported a clean tree.

A gate that lies is worse than no gate. I hit the same trap while working on files an agent writes disappearing into .gitignore, so I now reach for -z reflexively whenever a script walks filenames.

One caveat: .git/hooks/ is not part of the repository. Point git config core.hooksPath .githooks at a tracked directory and fresh clones pick the hook up automatically.

Prevention: one line, placed before the commit

If you only adopt one of the three, adopt Fix 1. With a narrow staging scope, the rest becomes insurance.

Beyond that, one check in the agent's procedure earned its place:

# After add, before commit — put it in front of your eyes once
git diff --cached --name-only

That is all. When an agent reads back the list of files it staged, it notices the ones that do not match its intent. Combined with path-scoped add, this caught nearly everything for me.

Agent-driven Git failures tend to arrive wearing a normal face — the same way a push reports success while an unset git identity quietly drops the commit on the floor. Deciding where to insert a mechanical check, rather than relying on noticing, turned out to be the cheaper path.

Cleaning up an artifact you already committed:

# Untrack it; the local file stays
git rm --cached next.config.ts.bak
echo '*.bak' >> .gitignore
git add .gitignore
git commit -m "Remove: backup artifact from repository"

Purging it from history needs git filter-repo, but if the file holds no secrets, untracking is enough in practice. I read mine first and decided a history rewrite was not worth it.

Where to go next

Check what your agent's last three commits actually contain.

git log -3 --stat --oneline

If an unfamiliar extension shows up even once, there is a git add -A somewhere in the procedure that produced it. Rewriting that one call to be path-scoped is the fastest place to start.

For gating the content of a commit rather than its filenames, I wrote up stopping agent commits at pre-commit. Thank you for reading.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-07-12
What to Delegate to an Antigravity Agent and What to Keep by Hand, After Two Weeks
After two weeks of handing my daily solo-dev tasks to Antigravity agents, a clear line emerged between the work I was glad to delegate and the work I had to pull back. A retrospective with the operational log.
Agents & Manager2026-07-10
Tracing Why an Agent Wrote That Line Six Months Ago — Commit Granularity and Provenance Trailers
When an agent packs 14 files and 800 lines into a single commit, git blame tells you nothing six months later. Here is how I split commits at intent boundaries, recorded provenance as machine-readable Git trailers, and built a one-command path from a blamed line back to the design decision behind it.
Agents & Manager2026-07-10
When Your Agent's Files Vanish Into .gitignore — A Pre-Commit Detection Gate
When an agent writes files that match .gitignore, the diff review looks perfect but nothing lands in the commit. Here is a gate script that catches ignored build output before you push, plus the tuning it needs.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →