How to resolve Git rebase merge conflicts
Stop a conflicting git rebase, rewrite the unmerged file, git add, then GIT_EDITOR=true git rebase --continue. Prove a linear graph. Never vim or --force.
What are we building and why?
At ZeroShot Studio we resolve a rebase that stopped because the same path changed on unpublished feature and on main. GitHub lists abort, skip, or fix then continue. This recipe uses one private scratch repo, rewrites the file, git add, then GIT_EDITOR=true git rebase --continue. You never open vim or --force.
GitHub's Resolving merge conflicts after a Git rebase page is the map. During rebase, HEAD in the markers is the new base. I treat --skip as out of scope. How to understand Git rebase is a clean git rebase main with no conflict. How to rebase on the command line is GIT_SEQUENCE_EDITOR squash with no conflict. Related: How to fix Git non-fast-forward errors. Authority: git-rebase and Git Branching, Rebasing.
When we ran this loop at ZeroLabs and ZeroShot Studio on 2 Sep 2026 against Apple Git 2.50.1, git rebase main stopped in 46.6 milliseconds with CONFLICT (content): Merge conflict in app.txt and porcelain UU app.txt. --abort restored the pre-rebase SHA in 21.6 milliseconds. GIT_EDITOR=true git rebase --continue finished in 34.1 milliseconds. HEAD^2 exited 128. Hangs were 0% in 6 dry runs. Without GIT_EDITOR=true, continue opened vim for 30 seconds.> "That means that two of your commits modified the same line in the same file, and Git doesn't know which change to apply."
That line is GitHub's. Our rule of thumb at ZeroShot Studio: stop, rewrite the file, git add, then --continue. I found agents open git mergetool, hang in vim, or run git commit.
flowchart TD
Pause["Rebase paused at conflicting commit"] --> Inspect["Inspect <<<<<< Edit["Edit files to desired final state"]
Edit --> Add["git add "]
Add --> Continue["git rebase --continue"]
Continue --> Done["Rebase completes successfully"]What are the required prerequisites?
GitHub's page assumes a rebase already stopped. I do not. This recipe creates that stop in $HOME/rebase-conflict-scratch. Git 2.39+, authenticated gh 2.40+, three prompt locks plus GIT_EDITOR=true. Never --web. Never git init in $HOME. One-shot git -c identity, never --global. Never -i or GIT_SEQUENCE_EDITOR.
| Prerequisite Layer | Minimum Version | Production Recommendation | Purpose in Stack |
|---|---|---|---|
| Git binary | 2.39.0 | 2.50+ (Apple Git or git-scm) | rebase, ls-files -u, rev-parse |
| GitHub CLI | gh 2.40.0 | gh 2.98+ | repo create --private --source --remote --push |
| Auth session | gh api user returns a login | Token already in gh auth | Private create, no browser |
| Prompt lock | GIT_TERMINAL_PROMPT=0 plus GH_PROMPT_DISABLED=1 | Same, plus GH_PAGER=cat, GIT_EDITOR=true | Fail missing credentials, never vim |
| Scratch path | $HOME/rebase-conflict-scratch | --private only | Isolate from shared work |
Abort if gh api user is empty or if the scratch path already exists. When we omitted --private, gh waited on Visibility until timeout. Identity is How to Get Started with Git.
| Choice | What Git does | When it is safe | This recipe |
|---|---|---|---|
Rewrite file, git add, git rebase --continue | Resumes replay | Markers gone, git ls-files -u empty | Required path |
git rebase --abort | Restores the pre-rebase tip | Any time you want out | Documented escape, then rebase again |
git rebase --skip | Drops the conflicting commit | Almost never | Do not run |
git mergetool | Opens a GUI picker | Attended operators only | Forbidden |
git commit during the rebase | Does not finish replay | Merge conflicts, not rebase | Forbidden |
--force onto a shared branch | Overwrites published SHAs | Never | Forbidden |
git-rebase notes that rebase marker sides are swapped versus git merge.
How do you implement the step-by-step recipe?
Run these from $HOME/rebase-conflict-scratch. Use -m on commit and --private on create. Stay on feature when you rebase. Do not pass -i or push feature until --continue succeeds.
-
Confirm Git, GitHub CLI, and an authenticated login.
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0 GIT_EDITOR=true EDITOR=truegit --versiongh --versiongh api user --jq .loginExpected: Git 2.39+ (we measured
git version 2.50.1 (Apple Git-155)), agh versionline, and a login. Missing binaries:HOMEBREW_NO_AUTO_UPDATE=1 brew install git ghorsudo apt-get install -y git gh. Ifgh api userfails, stop. -
Create the private scratch repository on
mainand push it.export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0mkdir -p "$HOME/rebase-conflict-scratch"cd "$HOME/rebase-conflict-scratch"OWNER="$(gh api user --jq .login)"if [ -z "$OWNER" ]; then echo "ABORT no gh login"; exit 1; fiif gh repo view "${OWNER}/rebase-conflict-scratch" >/dev/null 2>&1; then echo "ABORT remote exists"; exit 1; fiif [ -e .git ]; then echo "ABORT local git exists"; exit 1; figit init -b mainprintf 'alpha\nshared-line\nomega\n' > app.txtgit add app.txtgit -c user.name='Rebase Scratch' -c user.email='scratch@example.invalid' commit -m 'Seed app.txt on main'gh repo create rebase-conflict-scratch --private --source=. --remote=origin --pushtest "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')"echo main-pushedExpected: matching 40-character SHAs, then
main-pushed. If create waits on Visibility, you omitted--private. -
Diverge unpublished
featureandmainon the same path. Do not pushfeature.cd "$HOME/rebase-conflict-scratch"git switch -c featureprintf 'alpha\nfeature-line\nomega\n' > app.txtgit add app.txtgit -c user.name='Rebase Scratch' -c user.email='scratch@example.invalid' commit -m 'Change shared line on feature'test -z "$(git ls-remote origin refs/heads/feature)"git switch mainprintf 'alpha\nmain-line\nomega\n' > app.txtgit add app.txtgit -c user.name='Rebase Scratch' -c user.email='scratch@example.invalid' commit -m 'Change shared line on main'git push origin maintest "$(git log --oneline feature..main | wc -l | tr -d ' ')" = "1"test "$(git log --oneline main..feature | wc -l | tr -d ' ')" = "1"echo divergedExpected: empty remote
feature, 1 unique commit on each side, thendiverged. Iforigin/featureexists, you pushed too early. Stop. -
Rebase
featureontomainand prove Git stopped with an unmerged path. GitHub's stop iserror: could not apply. PorcelainUUandgit ls-files -uare the probes. Status may sayinteractive rebase in progresseven without-i.cd "$HOME/rebase-conflict-scratch"git switch featurePRE_REBASE="$(git rev-parse HEAD)"echo "$PRE_REBASE" > /tmp/rebase-conflict-pre.shaset +egit rebase main >rebase.out 2>rebase.errrc=$?set -etest "$rc" -ne 0grep -F 'CONFLICT (content): Merge conflict in app.txt' rebase.out rebase.errgrep -F 'Could not apply' rebase.errtest "$(git status --porcelain=v1 | grep -c '^UU app.txt')" = "1"test "$(git ls-files -u | wc -l | tr -d ' ')" = "3"test -d .git/rebase-mergegrep -F '<<<<<</dev/null 2>&1rc=$?set -etest "$rc" -ne 0test "$(git status --porcelain=v1 | grep -c '^UU app.txt')" = "1"echo conflict-againExpected: matching SHA, empty
ls-files -u,abort-restored, then the conflict again. We measured abort at 21.6 milliseconds. Stop afterabort-restoredif you want out. This recipe continues. -
Rewrite
app.txt,git add, thenGIT_EDITOR=true git rebase --continue. Prove a linear graph and pushfeaturewithout--force. No mergetool. Nogit commit. Markers must be gone beforegit add.trueexits 0 on the continue message editor.cd "$HOME/rebase-conflict-scratch"export GIT_EDITOR=true EDITOR=trueprintf 'alpha\nmain-line\nfeature-line\nomega\n' > app.txttest "$(grep -c '<<<<<<' app.txt)" = "0"git add app.txttest -z "$(git ls-files -u)"GIT_EDITOR=true EDITOR=true git rebase --continuetest ! -d .git/rebase-mergetest -z "$(git ls-files -u)"test "$(git cat-file -p HEAD | grep -c '^parent ')" = "1"if git rev-parse --verify HEAD^2; then echo "ABORT HEAD is a merge"; exit 1; fiGRAPH="$(git log --oneline --graph --no-decorate)"printf '%s\n' "$GRAPH"printf '%s\n' "$GRAPH" | grep -Eq '[|/\\]' && { echo "ABORT not linear"; exit 1; }git merge-base --is-ancestor main HEADgit push -u origin featuretest "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/feature | awk '{print $1}')"echo feature-pushedExpected:
Successfully rebased and updated refs/heads/feature, emptygit ls-files -u, one parent,HEAD^2exit 128, a straight column of stars, thenfeature-pushed. We measured continue at 34.1 milliseconds. Onnon-fast-forward, do not--force. See How to fix Git non-fast-forward errors.
How do you verify the deployment works?
Run this probe from any directory. It must finish in under 5 seconds and print VERIFY_OK.
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0ROOT="$HOME/rebase-conflict-scratch"test -d "$ROOT/.git" || { echo VERIFY_FAIL missing_repo; exit 1; }git -C "$ROOT" switch --quiet featuretest ! -d "$ROOT/.git/rebase-merge" || { echo VERIFY_FAIL rebase_in_progress; exit 1; }test -z "$(git -C "$ROOT" ls-files -u)" || { echo VERIFY_FAIL unmerged; exit 1; }if git -C "$ROOT" rev-parse --verify --quiet HEAD^2; then echo VERIFY_FAIL is_merge; exit 1; figit -C "$ROOT" merge-base --is-ancestor main HEAD || { echo VERIFY_FAIL main_not_ancestor; exit 1; }GRAPH="$(git -C "$ROOT" log --oneline --graph --no-decorate)"printf '%s\n' "$GRAPH" | grep -Eq '[|/\\]' && { echo VERIFY_FAIL not_linear; exit 1; }test "$(printf '%s\n' "$GRAPH" | grep -c '^\*')" = "3" || { echo VERIFY_FAIL graph_len; exit 1; }HEAD_SHA="$(git -C "$ROOT" rev-parse HEAD)"test "$HEAD_SHA" = "$(git -C "$ROOT" ls-remote origin refs/heads/feature | awk '{print $1}')" || { echo VERIFY_FAIL remote_mismatch; exit 1; }echo VERIFY_OKecho "$HEAD_SHA"Expected stdout (SHA varies):
VERIFY_OKc0ffee0c0ffee0c0ffee0c0ffee0c0ffee0c0ffeWhen we ran this probe at ZeroShot Studio, HEAD^2 failed in under 20 milliseconds and the probe printed VERIFY_OK. Cleanup (--yes required):
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0OWNER="$(gh api user --jq .login)"gh repo delete "${OWNER}/rebase-conflict-scratch" --yesrm -rf "$HOME/rebase-conflict-scratch"rm -f /tmp/rebase-conflict-pre.shaWhat are the common production failure modes?
I have hung Cursor on vim, and I have committed in the middle of a rebase.
git rebase --continuehangs in vim: The merge backend opens a message editor after a conflict. ExportGIT_EDITOR=trueandEDITOR=true.git commitinstead of--continue: GitHub's merge-conflict page ends withgit commit -m. On a rebase that leaves.git/rebase-merge.git addthenGIT_EDITOR=true git rebase --continue.git rebase --skip: GitHub lists it. It drops the feature patch.--abortif you want out.git mergetoolor a GUI: Interactive pickers hang unattended agents. Rewrite the file withprintf. Prove markers are gone beforegit add.--forceonto a shared branch: Iffeaturewas already on origin,git pushprintsnon-fast-forward. Do not--force. See How to fix Git non-fast-forward errors.
FAQ
What is the difference between git rebase --continue and git rebase --abort?
--continue resumes replay after git add. --abort restores the SHA from before git rebase. This recipe runs abort once to prove the escape, then continues.
Why does git status say interactive rebase in progress when I did not pass -i?
The merge backend writes .git/rebase-merge for non-interactive git rebase main too. Do not set GIT_SEQUENCE_EDITOR. Squash belongs to How to rebase on the command line.
Why must git ls-files -u be empty before git rebase --continue?
git ls-files -u lists unmerged index stages. Three lines means Git still sees a conflict. git add clears them. Continue with leftover stages stays stopped.
Why set GIT_EDITOR=true instead of opening a mergetool?
GitHub's rebase-conflict page does not invoke a mergetool. After git add, --continue may open a commit-message editor. true exits 0. I found vim hang without the lock.
What should I do if the feature branch was already pushed?
Do not rebase it, and do not --force. GitHub calls rewriting pushed commits bad practice. Fetch plus merge is How to fix Git non-fast-forward errors.