◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-11-05Intermediate

Stopped by "cannot pull with rebase: You have unstaged changes" — and the autostash fix that exits 0 with a conflict

How to triage git pull --rebase stopping on unstaged changes in a repo an agent has touched, and why --autostash can return exit code 0 while leaving conflict markers behind. Reproduced on git 2.34.1.

git17pull-rebaseautostashagent-operationstroubleshooting131

The morning after I let an agent work on an app update, I ran git pull --rebase origin main and got two lines back:

error: cannot pull with rebase: You have unstaged changes.
error: please commit or stash them.

As an indie developer, I meet this message often. A session ended halfway the day before, and the working tree still holds uncommitted edits.

The thing I most want you to take away is that this error is git protecting you, not breaking. The shortcut I learned first for switching that protection off had a small trap in it, though. Here is what I reproduced on git 2.34.1, in the order I checked it.

First, look at what is left behind

One command before any pull:

git status --short --untracked-files=no

It lists only tracked-file changes. A line like M notes.md is your culprit. Untracked files do not block pull --rebase, so they are left out on purpose.

Then you have three branches to choose from:

What was leftFirst moveWhy
Real work you want to keepCommit it firstIt lives in history, so nothing gets dropped later
Edits from a quick experimentgit restore themYou pull onto a clean, current base
Changes you cannot judge yetgit stash themNothing is deleted, and the pull can still go ahead

The third row is where my hands usually stall, and it is exactly where --autostash comes in.

--autostash is handy, and slightly risky

With --autostash, git stashes, pulls, and restores in one go:

git pull --rebase --autostash origin main

When the dirty file did not overlap with what changed upstream, it finished quietly:

Created autostash: cc3d9ae
Fast-forward
 config.txt | 1 +
Applied autostash.

The trouble starts when the same file changed on both sides. In a throwaway repo I edited the same spot in notes.md upstream and locally, then ran the same command:

Created autostash: cc3d9ae
Fast-forward
 notes.md | 1 +
Applying autostash resulted in conflicts.
Your changes are safe in the stash.
You can run "git stash pop" or "git stash drop" at any time.

Right after that, echo $? printed 0.

And the file itself now contains conflict markers:

n
<<<<<<< Updated upstream
remote note
=======
agent leftover
>>>>>>> Stashed changes

The pull succeeded, so the exit code is 0. The conflict happened in the "restore the stash" step. Nothing is lost — your edits are still in stash@{0} — but if a script or an agent chains the next step with &&, a file full of markers travels straight into the build or the push.

This is where I was wrong. I had assumed autostash made things safe, and I had written scripts that trusted the exit code to start the next stage.

Split the failure into two shapes

Since then I keep two versions of the sync step.

The first stops when the tree is dirty. This is my default for anything that runs unattended:

#!/bin/bash
# Check the tree first; if it is dirty, stop and say why
set -euo pipefail
branch="${1:-main}"
 
dirty="$(git status --porcelain --untracked-files=no)"
if [ -n "$dirty" ]; then
  echo "Uncommitted changes found. Aborting the pull:" >&2
  echo "$dirty" >&2
  exit 2
fi
 
git pull --rebase origin "$branch"
echo "sync ok: $(git rev-parse --short HEAD)"

On a dirty tree it stops with exit code 2 and prints the offending file:

Uncommitted changes found. Aborting the pull:
 M notes.md
exit=2

The log tells you what was left, so you do not hunt for the cause the next morning.

The second version is for me at the keyboard. It allows autostash, but checks for a conflict itself instead of trusting the exit code:

#!/bin/bash
# With autostash, verify the restore step yourself
set -euo pipefail
branch="${1:-main}"
 
git pull --rebase --autostash origin "$branch"
 
conflicts="$(git diff --name-only --diff-filter=U)"
if [ -n "$conflicts" ]; then
  echo "Conflict while restoring stashed changes (your edits are still in the stash):" >&2
  echo "$conflicts" >&2
  exit 3
fi
echo "sync ok: $(git rev-parse --short HEAD)"

Run against the conflicting case, it names notes.md and exits with 3. --diff-filter=U picks up only unmerged paths, so it stays empty when everything went well.

Why two versions? Because for unattended runs, stopping is the safe outcome, while for a human watching, pushing ahead and fixing afterwards is faster. Unattended steps refuse a dirty tree; supervised steps accept it and verify the way back. That is the line I draw today.

Cleaning up after an autostash conflict

I walked the cleanup through the same throwaway repo:

  1. Run git status, open each UU file, keep the side you want, and save
  2. git add <file> to mark it resolved
  3. git stash drop to discard the stash that has done its job

If you skip step three, an autostash entry stays in git stash list. During my own testing I started a second experiment with a leftover entry from the first, and spent a minute puzzled by a line I did not recognize. A quick git stash list before and after each pull prevents that.

If it still will not go through

Two more things to check:

  • A rebase is already in progress. git status starts with rebase in progress. Finish it first with git rebase --continue or git rebase --abort
  • You are on a different branch. Check git branch --show-current. An agent may have switched to a work branch and never switched back

Both show up with different wording than "unstaged changes", so they are easy to tell apart.

Your next step

Start with git status --short --untracked-files=no and see what is actually blocking you. Then use the first script for unattended runs and the second for your own sessions.

Do not read exit code 0 as "finished cleanly." Since I started handing procedures to agents, that is a rule I hold to more firmly than before.

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

▣ App Dev2026-10-30
After I Closed the Image Viewer with Esc, the Page Behind It Stopped Scrolling: What I Counted When Moving to the dialog Element
While fixing the full-screen image viewer on my own site, I worked through three problems in order: Esc did nothing, the page behind kept scrolling, and the close button sat on top of the artwork. Even after moving to dialog, one bug remained, with a check script and numbers.
▣ App Dev2026-10-28
I Fetched All 155 Sitemap URLs. Every One Returned 200, but 93% of the lastmod Dates Were the Same Day
A record of fetching every sitemap URL and cross-checking status, canonical, hreflang and lastmod. Full standard-library Python scripts and the mismatch I found.
▣ App Dev2026-10-22
Translations Drifted Apart Without Me Noticing: A 60-Line strings.xml Check to Run Before Asking an Agent to Review
A small Python script that finds missing keys, empty strings, and placeholder mismatches across Android locales, plus how to hand only its findings to Antigravity for fix proposals.