The other day I handed a small fix to an agent and let it commit the result. Looking at the diff afterward, right next to the file I actually wanted changed sat a .bak that a formatter had left behind. The commit message was a single word: "Fix".
The code itself was correct. The trouble started later, when I wanted to trace that one change back.
As an indie developer running several apps on my own, the more I let an agent do the hands-on work, the more my mistakes move away from the code itself and toward two other places: what you included in the commit, and what you wrote down about it. This piece is about holding both of those in place with plain mechanics, not willpower.
What sneaks in — the byproducts git add -A collects
When you ask an agent to commit, it almost always reaches for git add -A or git add .. Convenient, but that single move scoops up whatever byproducts happened to land in your working directory.
The awkward part is that most of these byproducts are born right next to the correct output of the task. A formatter's .bak, a temporary log, build output, an editor's ~ file. If any of them is missing from .gitignore, it rides straight into history.
What I actually hit was a .bak that a script left behind on every --fix run. git add -A picked it up each time, and small litter piled up in the repository. Listing the usual suspects makes the target clearer.
| Byproduct | Typical pattern | Source |
|---|---|---|
| Backups | *.bak / *.orig / *~ | Format / replace tools |
| Build output | dist/ / .next/ / build/ | Builds and previews |
| Dependencies | node_modules/ | Package installs |
| Logs and temp | *.log / .DS_Store | Runs and the OS |
Bind staging with an allowlist
The first move is simple: do not let the agent use -A. Hand it the scope it is allowed to touch for this task, and have it stage exactly that scope explicitly.
For a task that only touches article content, I put this in the instructions: "Commit only under content/ using git add content/. Do not use git add -A or git add .." Just fixing the scope in words removes most of the over-collecting.
But a promise in words can be broken. The agent, trying to be helpful, fixes something elsewhere too and accidentally stages a path outside the scope. So we add one more, mechanical net.
A preflight that stops the accident
Right before the commit, slip in a small script that checks whether the staged set stays inside the allowed scope. If a single path falls outside the allowlist, or matches a byproduct pattern, it stops the commit.
#!/usr/bin/env bash
# stage-guard.sh — run it before the agent commits
set -euo pipefail
# Scope you are allowed to touch (allowlist). Anything staged outside it aborts.
ALLOW='^(src/|content/|docs/)'
# Byproducts that tend to sneak in (denylist).
DENY='(\.bak$|~$|\.orig$|\.log$|/node_modules/|/\.next/|/dist/|\.DS_Store$)'
staged="$(git diff --cached --name-only)"
[ -z "$staged" ] && { echo "nothing staged"; exit 1; }
bad=""
while IFS= read -r f; do
if [[ "$f" =~ $DENY ]]; then bad+=" DENY $f"$'\n'; continue; fi
if [[ ! "$f" =~ $ALLOW ]]; then bad+=" OUT $f"$'\n'; fi
done <<< "$staged"
if [ -n "$bad" ]; then
echo "Unexpected files are staged:"
printf '%s' "$bad"
echo "-> remove them with git restore --staged <path>, then retry"
exit 1
fi
echo "all staged paths are within the allowed scope"Rewrite ALLOW and DENY for your own repository. The point is to keep both lists. The denylist rejects known junk; the allowlist rejects unknown paths that grew without you noticing. Thanks to the latter, a new kind of byproduct will not slip through silently.
Put this ahead of git commit and, regardless of how the agent ran add, out-of-scope files stop just before the commit. The idea of separating the workspace itself is covered in Running multiple agents in one repo breaks things — isolating workspaces with worktrees. Read together, you can see the two layers that keep scope small.
Have the agent fill in a message convention
After scope comes the record. A message that only says "Fix" is a problem because six months later you cannot recall anything from that one line. The more you delegate to an agent, the more that message becomes your only handle for tracing a diff.
Asking for free-form text gives uneven results. Handing over a fill-in template keeps it steady.
cat > .gitmessage <<'MSG'
# <type>(<scope>): what changed (present tense, under 50 chars)
# type = add / fix / refactor / chore / docs
#
# Leave one blank line, then one or two sentences on why.
# Write the cause and the fix, not the symptom.
MSG
git config commit.template .gitmessageThen add one line of convention to the agent's instructions: "Write commit messages as <type>(<scope>): summary, and always add one sentence in the body on why." Something like fix(cli): resolve 401 by re-authenticating — with type, scope, and the point all present — changes how a later trace goes entirely.
Keeping the cause and the fix on record is the foundation for isolating a regression quickly. The mindset of operating on the assumption that agents will break things appears in Build on the assumption that agents break things — keeping the blast radius small.
Wrap-up — the one thing to do next
An agent's commits fail on scope and wording more than on the correctness of the code. If you do just one thing today, drop a single stage-guard.sh in place and add two sentences to the instructions for your usual task: "do not use -A" and "follow the message convention."
Once the mechanics exist, you can let the agent work with peace of mind. I am still tidying up my own workflow, but ever since I added these two nets, looking at a diff stopped being something I dreaded. Thank you for reading.