Skip to content

Fix: git push rejected – non-fast-forward error

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix 'error: failed to push some refs' and '! [rejected] main -> main (non-fast-forward)' in Git. Covers pull, rebase, force push, and edge cases.

The Error

You run git push and get:

To github.com:user/repo.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:user/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Why This Happens

Git is telling you that the remote branch has commits that your local branch does not. Your local history and the remote history have diverged, so Git refuses to push because doing so would overwrite those remote commits.

A “fast-forward” in Git means the remote branch tip is a direct ancestor of your local branch tip. When that is true, Git can simply move the remote pointer forward to match your local pointer without losing any commits — nothing on the remote is being overwritten, just extended. When your local tip and the remote tip have diverged (each side has commits the other does not), the push is no longer a fast-forward. Allowing the push would mean dropping the remote-only commits, which Git refuses to do silently.

Common causes:

  • A teammate pushed to the same branch. Someone else merged or pushed new commits to main while you were working locally.
  • You pushed from another machine. You committed and pushed from your laptop, then tried to push older local state from your desktop.
  • You amended or rebased locally. Running git commit --amend, git rebase, or git reset rewrites commit history. The rewritten commits have different hashes than what the remote already has, so Git sees them as divergent. (Rebasing can also leave you in a detached HEAD state if interrupted.)
  • A PR was squash-merged on GitHub/GitLab. You merged your feature branch through the web UI with “Squash and merge,” then tried to push the original (non-squashed) branch again.

Real-world scenario: You amend your last commit to fix a typo in the commit message, then run git push. The push fails because --amend rewrites the commit hash. Your local main and origin/main have diverged even though the code is identical. Use git pull --rebase or --force-with-lease to resolve this.

In all of these cases the underlying problem is the same: your local branch tip is not a direct descendant of the remote branch tip, so a push would lose commits.

Git Version History: Why pull Behaves Differently Across Versions

The “right” way to recover from this error has shifted across Git versions, and tutorials written even two or three years apart give different commands. Knowing which behavior applies to your version saves wasted experimentation.

Git 2.27 (June 2020) added a noisy warning whenever git pull was run without an explicit --rebase or --no-rebase flag, telling you the default would change in a future release. Git 2.28 (July 2020) then introduced the pull.rebase and init.defaultBranch configuration knobs. From 2.28 onwards the recommended setup is to set pull.ff=only globally so that a non-fast-forward git pull fails fast instead of silently creating a merge commit:

git config --global pull.ff only
git config --global pull.rebase true   # if you prefer rebase

With pull.ff only, a divergent fetch tells you immediately that you need to choose between rebase, merge, or force-push instead of producing a surprise merge commit. This is the modern best practice for solo developers.

Git 2.37 (June 2022) added branch.autoSetupMerge=simple, which only sets up tracking when the local and remote branch names match. This subtly reduces accidental non-fast-forward errors caused by pushing a local branch to the wrong remote. Git 2.41 (June 2023) added clearer hints in the error output itself, which is why newer Git versions show more useful suggestions than older ones. If your error message looks sparse, run git --version — you may simply be on an older client.

Finally, --force-with-lease got a companion in Git 2.30: --force-if-includes. Combining them (--force-with-lease --force-if-includes) gives you stronger guarantees that you have inspected the latest remote tip before overwriting, and it is the safest force-push flag available today.

Fix 1: Pull and Merge (Safest)

The simplest fix. Fetch the remote commits and merge them into your local branch:

git pull origin main

This creates a merge commit that joins the two histories together. Resolve any merge conflicts if they appear, then push:

git push origin main

This is the safest option because no commits are lost or rewritten. The downside is that it adds a merge commit to your history.

Fix 2: Pull with Rebase (Cleaner History)

If you prefer a linear history without merge commits, rebase your local commits on top of the remote ones:

git pull --rebase origin main

This takes your local commits, temporarily removes them, pulls the remote commits, then replays your commits on top. If there are conflicts, Git will pause and ask you to resolve them one commit at a time. After resolving:

git rebase --continue

Once the rebase is complete, push normally:

git push origin main

Use this when you have a small number of local commits and want a clean, linear history.

Fix 3: Force Push with Lease (Safe Override)

If you intentionally rewrote history (for example, after an interactive rebase or amend) and you want the remote to match your local branch, use --force-with-lease:

git push --force-with-lease origin main

--force-with-lease checks that the remote branch is still at the commit you last fetched. If someone else pushed new commits since your last fetch, the push is rejected. This prevents you from accidentally overwriting a teammate’s work.

Only use this when you understand that you are replacing the remote history with your local history. This is appropriate after an intentional rebase or amend, not as a workaround for being behind.

For maximum safety on Git 2.30+:

git push --force-with-lease --force-if-includes origin main

--force-if-includes additionally checks that your local branch has integrated all the commits the remote currently shows in the reflog — protecting against the edge case where you fetched but never inspected the new remote commits.

Fix 4: Force Push (Use with Extreme Caution)

git push --force origin main

This unconditionally overwrites the remote branch with your local branch. It does not check whether someone else pushed in the meantime.

Only use --force on personal branches that nobody else is working on. Never force push to a shared branch like main or develop unless you have coordinated with your team and everyone is aware.

—force vs —force-with-lease

--force--force-with-lease
Overwrites remoteYes, unconditionallyOnly if remote hasn’t changed since your last fetch
Risk of losing others’ workHighLow (fails if remote was updated)
When to usePersonal/throwaway branches onlyAfter intentional local history rewrites

In practice, prefer --force-with-lease whenever you need to force push. There is almost no reason to use --force on any branch that others might be using.

Pro Tip: Set up a Git alias so you never accidentally type --force when you mean --force-with-lease: git config --global alias.pushf "push --force-with-lease". Now git pushf origin main always uses the safer option.

Still Not Working?

Protected branches block force push

GitHub, GitLab, and Bitbucket allow administrators to protect branches. If main is protected, you will see an error like:

remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Cannot force-push to this protected branch.

In this case you cannot force push at all. You must use Fix 1 or Fix 2 to integrate the remote changes, or ask a repository admin to temporarily disable branch protection.

On GitHub, this is under Settings > Branches > Branch protection rules. On GitLab, it is under Settings > Repository > Protected branches.

Diverged branches after a rebase

If you rebased a feature branch that was already pushed, your local and remote copies of that branch have diverged. A normal push will fail. This is expected. Use --force-with-lease to update the remote:

git push --force-with-lease origin feature-branch

This is a normal part of rebase workflows. Just make sure no one else is basing work on that branch, or coordinate with them first.

First push to a new remote with existing commits

If you initialized a repo locally and are pushing to a new remote that already has commits (for example, GitHub created a repo with a README), the histories are completely unrelated. You will see:

! [rejected]        main -> main (fetch first)

Fix this by pulling with --allow-unrelated-histories (see our full guide: Fix: fatal: refusing to merge unrelated histories):

git pull origin main --allow-unrelated-histories

Resolve any conflicts, then push:

git push origin main

You fetched but the error persists

If git pull --rebase still fails, your local branch might be tracking the wrong remote branch. Verify with:

git branch -vv

Make sure your branch is tracking origin/main (or whichever remote branch you intend). If it is not, set the upstream:

git branch --set-upstream-to=origin/main main

Then pull and push again. If you get a Permission denied (publickey) error when pushing, see Fix: git permission denied (publickey).

A squash-merge on GitHub left you stranded

If your PR was squash-merged, the remote main now has a single new commit that replaces all of your feature-branch commits with a different SHA. Pushing your feature branch’s old tip back to the remote will fail, and rebasing onto main re-applies your already-merged work. The right fix is to delete the local feature branch and re-create it from the updated main:

git fetch origin
git checkout main
git pull --ff-only
git branch -D feature-x

Do not try to “salvage” the old branch with a force push. The squash-merge is the authoritative history now.

A CI bot pushed a new commit while you were preparing your push

Many teams run CI bots that push version bumps, changelogs, or formatted code back to main automatically. If you commit, then a Dependabot or release-please bot pushes during the same minute, your push will hit the non-fast-forward error. The fix is git pull --rebase — the bot’s commit is almost always a single file change that rebases cleanly. If conflicts do appear (for example, both you and the bot updated the same package.json), trust the bot’s version of the lockfile and your version of the source code, then resolve manually.

The error appears only on Windows but not on macOS/Linux

If the same local repo behaves differently on different machines, check core.autocrlf and core.ignorecase. A Windows clone with autocrlf=true may rewrite line endings on every checkout, producing local commits the remote does not have. Run git config --get core.autocrlf and align the setting across machines, or set it to input if you only work with LF line endings.

A LFS pointer mismatch is masquerading as a non-fast-forward

If your repo uses Git LFS, occasionally the LFS pointer object on the remote and your local copy will disagree, and the regular push fails because the underlying tree object hashes differ. Run git lfs fetch origin --all and then retry the push. Confirm LFS is healthy with git lfs ls-files.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles