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=noIt 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 left | First move | Why |
|---|---|---|
| Real work you want to keep | Commit it first | It lives in history, so nothing gets dropped later |
| Edits from a quick experiment | git restore them | You pull onto a clean, current base |
| Changes you cannot judge yet | git stash them | Nothing 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 mainWhen 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 changesThe 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=2The 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:
- Run
git status, open eachUUfile, keep the side you want, and save git add <file>to mark it resolvedgit stash dropto 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 statusstarts withrebase in progress. Finish it first withgit rebase --continueorgit 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.