bernardo@cotrim.dev ~
~ / blog / #002
2026.04.22 · essay · 5 min read

The Day an AI Agent Wiped My Branch


I opened GitHub on Monday morning to finish a PR I’d been working on the previous week and found it closed. The branch had been force-pushed back to the tip of trunk. Four commits of real work, gone from the remote.

No one had touched the branch but me, and a Claude Code Desktop session I’d been letting run in the new Auto mode while I was away from the keyboard.

Spoiler: the commits weren’t actually gone. But the path from “heart skips a beat” to “everything is fine” is a story about one of git’s quieter features, and a cautionary tale about handing destructive commands to AI agents.

The force-push

I was working on a PR with a tricky E2E test that only failed in CI. I couldn’t reproduce it locally, so I’d spent a couple of days iterating with Claude Code Desktop in Auto mode: push an attempt, wait for CI, read the logs, try again.

I didn’t see the moment it happened. Somewhere in a long iteration loop, between rebases and resets and cleanups, the agent force-pushed the branch back to the trunk SHA. Clean slate on the remote. GitHub auto-closed the PR with its helpful “branch has been reset to match base branch” message.

And here’s the thing: from the agent’s point of view, this was a win.

CI was red. The E2E test was failing. So it reset the branch to match trunk, and the failing test went away. CI green. PR no longer broken. Problem solved. Smart AI.

Technically correct. The worst kind of correct.

My first instinct was to check the local branch:

$ git log --oneline my-branch
# matched what was on the remote. The local branch got reset too.

Not great. My branch pointer was at trunk, not at the tip of my work.

That would have been the moment to panic, if git didn’t have one of its best features quietly keeping a paper trail.

Enter git reflog

Git doesn’t actually delete commits when you reset or force-push a branch. The commits are still sitting in the object store, orphaned, waiting for garbage collection to come clean them up (which takes weeks).

And git keeps a log of every state every ref has ever been in. That log is the reflog.

$ git reflog my-branch
4e34f5a29 HEAD@{0}: reset: moving to 4e34f5a29
66103323b HEAD@{1}: commit: Fix flaky E2E test, take four
a444d00e5 HEAD@{2}: commit: Fix flaky E2E test, take three
6455b07df HEAD@{3}: commit: Fix flaky E2E test, take two
a68b8e22e HEAD@{4}: commit: Fix flaky E2E test, take one
...

Every reset, rebase, merge, and checkout had a line. Every time the branch pointer moved, it was recorded. I walked backwards through the entries until I found the last state I trusted: 66103323b, which had four commits of good work on top of an older trunk.

One command:

$ git reset --hard 66103323b

And the branch was back. Commits restored, days of work not lost.

I still had to rebase onto the current trunk (13 commits had landed in the meantime), but that’s a different kind of tedious than “rewrite it from scratch.”

What I’m changing

This was a near-miss. Everything was recoverable because git is forgiving and the reflog hadn’t expired. But a few things are worth changing going forward:

  • AI agents with push rights need guardrails. Most commands a coding agent runs are idempotent or easy to undo. Install a package, edit a file, run the tests. A force-push is neither. The agent doesn’t know which commands are destructive unless you tell it, and it won’t hesitate to run git push --force if it thinks that’s the right move.
  • Be specific about the goal. “Make CI green” and “ship a working PR” are very different prompts. The first one has a much shorter solution if you’re willing to delete the work.
  • Constrain the dangerous commands. I’m adding a pre-push hook that rejects --force and --force-with-lease unless I’ve set an environment variable. If I want to force-push, I’ll do it myself.
  • Rebase locally only. Once a branch is on the remote, I’m done letting the agent rebase it. The value of a clean history isn’t worth the blast radius.
  • Keep a checkpoint the agent can’t reach. When I’m working with an agent on a long-running branch, I want a read-only copy somewhere the agent can’t write. A second clone, a bundle file, anything. Cheap insurance.
  • Be careful with Auto mode. Auto mode is great when the loop is “edit, run tests, repeat.” It’s less great when the loop can include git push --force. If I’m going to leave the agent unattended, I want it in a sandbox where the worst it can do is rewrite files I can restore.

A small love letter to reflog

If you haven’t used git reflog before, learn it now. In calm weather, not when something has already gone wrong.

Every time HEAD or a branch moves, git writes a line to the reflog. Resets, rebases, merges, checkouts. They’re all there. Entries for reachable commits stick around for 90 days by default; unreachable ones for 30.

The flow when something goes wrong:

# See where a branch has been
git reflog <branch-name>
# Or for HEAD
git reflog
# Recover
git reset --hard <sha-from-reflog>

And if the reflog has already expired, orphaned commits can still be found with:

git fsck --lost-found

Conclusion

The PR is closed. The branch is safe. I’ll open a new one this week. CI will probably be red again, and this time I’ll fix it the slow way.

AI coding agents are genuinely useful, but their blast radius is the whole repo. Treat them like a junior engineer with sudo.

They’re great at a lot of things, but not to be trusted with destructive commands unsupervised. And when something does go wrong: don’t panic, check the reflog.

Links