I had a long-running autoresearch loop — a script that crawls for sales leads, writes results, and commits them to git every iteration without me touching it — chugging along in the background. In a different terminal I was cleaning up an old pull request. I typed git checkout main && git pull --ff-only in what I thought was a quiet repo. It wasn’t quiet. It was the same repo the loop was committing into.
Here’s the part that stings: nothing broke loudly. The loop kept running. It kept committing. It just started committing to main. By the time I noticed, my local main was 22 commits ahead of origin, every one of them autoresearch noise that had no business being on the branch I ship from.
If you’re not a git person: think of main as the official, published copy of a project — the one everybody trusts. The loop was supposed to be scribbling in a private notebook (a side branch). My one command quietly handed it the official copy and said “write here instead,” and it happily did.
The root cause is dumber than the symptom. Each iteration, the loop ran git rev-parse HEAD — basically “where am I right now?” — and committed there. It never pinned itself to a named branch. It trusted that wherever HEAD pointed was where it belonged. So when I moved HEAD with git checkout, the loop didn’t get confused or error out. It just followed me. It was hostage to whatever moved HEAD, and the thing that moved HEAD was me, in another window, not thinking.
My first instinct was to blame myself for being careless with terminals — and sure, partly. But “be more careful” is a terrible fix. The real fix is structural: a running process that commits to git owns its working directory, and you do not run checkout, pull, reset, or rebase in there. Ever. Not while it’s alive.
The trick I should’ve used from the start is git worktree. It lets one repo have multiple checked-out directories at once, each on its own branch, sharing the same history underneath. So all my PR-merge work happens in a throwaway directory and the loop’s HEAD never twitches.
Worktree for branch work + a self-defending loop give me the detail
Do all your branch-switching in a separate, disposable checkout instead of the live one:
git worktree add /tmp/pr-cleanup origin/main
cd /tmp/pr-cleanup
git pull --ff-only # HEAD in the loop's dir never moves
# when done:
git worktree remove /tmp/pr-cleanupAnd make the loop refuse to be hijacked. Pin it to a branch each iteration and bail if HEAD drifted off it:
EXPECTED="refs/heads/autoresearch-v9"
ACTUAL=$(git symbolic-ref -q HEAD)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "HEAD drifted to $ACTUAL, refusing to commit" >&2
exit 1
fi
git commit -am "lead batch $(date -u +%FT%TZ)"symbolic-ref asks “what branch am I on,” not “what commit,” so a detached HEAD or a sneaky checkout trips the guard instead of getting a silent commit.
The general rule I’d hand anyone running an agent or daemon that commits on a loop: its working directory is off-limits for HEAD-moving commands, and the loop itself should assert where it is before every commit, not assume. A process that reads its location instead of declaring it will go exactly where you accidentally send it.