Fix: git push rejected – non-fast-forward error
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
mainwhile 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, orgit resetrewrites 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--amendrewrites the commit hash. Your localmainandorigin/mainhave diverged even though the code is identical. Usegit pull --rebaseor--force-with-leaseto 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 rebaseWith 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 mainThis creates a merge commit that joins the two histories together. Resolve any merge conflicts if they appear, then push:
git push origin mainThis 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 mainThis 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 --continueOnce the rebase is complete, push normally:
git push origin mainUse 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 mainThis 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 remote | Yes, unconditionally | Only if remote hasn’t changed since your last fetch |
| Risk of losing others’ work | High | Low (fails if remote was updated) |
| When to use | Personal/throwaway branches only | After 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
--forcewhen you mean--force-with-lease:git config --global alias.pushf "push --force-with-lease". Nowgit pushf origin mainalways 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-branchThis 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-historiesResolve any conflicts, then push:
git push origin mainYou 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 -vvMake 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 mainThen 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-xDo 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: gh CLI Not Working — Auth Scopes, Multiple Accounts, PR Create Errors, and Enterprise Hosts
How to fix GitHub CLI errors — gh auth login token scopes missing, multiple accounts switching, gh pr create permission denied, GHE host auth, gh repo clone vs git clone, and API rate limits.
Fix: error: failed to push some refs to remote
How to fix Git error 'failed to push some refs' caused by diverged branches, remote changes, protected branches, authentication failures, and pre-push hooks.
Fix: Git remote rejected — file exceeds GitHub's file size limit of 100.00 MB
Resolve the GitHub push error when a file exceeds the 100 MB size limit by removing the large file from history, using Git LFS, or cleaning your repository with BFG Repo Cleaner.
Fix: CONFLICT (content): Merge conflict in file — fix conflicts and then commit the result
How to fix Git merge conflicts during merge, rebase, cherry-pick, and pull — resolve conflict markers, use merge tools, accept theirs or ours, abort, and prevent future conflicts.