Git Push Rejected Non-Fast-Forward? The Fix, Step by Step
Fix 'git push rejected (non-fast-forward)': why the remote is ahead, pull with rebase or merge, and when --force-with-lease is safe after a rebase.

Git rejected the push because the branch on the remote contains at least one commit that your local copy of that branch doesn't. Git only lets a push fast-forward a branch, which means the remote's current tip must be an ancestor of the commit you're pushing. Yours isn't, so Git refuses rather than throw someone's commits away.
The fix in nearly every case is to bring those remote commits in first and then push again: git fetch origin, then git pull --rebase origin main (or git pull --no-rebase origin main if you'd rather keep a merge commit), resolve any conflicts, then git push origin main. The one legitimate exception is a branch only you work on whose history you rewrote on purpose, for example after git rebase -i. There, git push --force-with-lease is the right tool. Plain --force on a shared branch is how teammates lose work.
Here's the full error as Git prints it:
$ git push origin main
To github.com:acme/widgets.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:acme/widgets.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.
What non-fast-forward actually means#
Each line of push output has the form <flag> <summary> <from> -> <to> (<reason>), as the git push reference describes. The ! flag marks a ref that was rejected or failed. The word rejected on its own, without remote, means Git didn't even try to send the ref: it checked locally that the update wasn't a fast-forward and that you hadn't asked to force it. The reason in parentheses, non-fast-forward, names the rule that tripped.
The rule itself is simple. For a push to a branch under refs/heads/, the destination must be an ancestor of the source commit. Put differently: walking back through the parents of the commit you're pushing must eventually reach the commit the remote branch currently points at. If it does, the remote can move its pointer forward and nothing is lost. If it doesn't, moving the pointer would orphan whatever commits the remote has, so Git stops.
You can see the check in miniature with a few small commit graphs:
# The rule git push applies to a branch: the remote's current tip must be
# an ancestor of the commit you're pushing. Otherwise: non-fast-forward.
def is_ancestor(parents, candidate, tip):
seen, stack = set(), [tip]
while stack:
commit = stack.pop()
if commit == candidate:
return True
if commit in seen:
continue
seen.add(commit)
stack.extend(parents.get(commit, []))
return False
# Each graph maps a commit to its parents. A is the root everywhere.
scenarios = [
('you are ahead', {'B': ['A'], 'C': ['B']}, 'B', 'C'),
('you are behind', {'B': ['A'], 'C': ['B']}, 'C', 'B'),
('diverged', {'B': ['A'], 'C': ['B'], 'D': ['B']}, 'D', 'C'),
('after rebase', {'B': ['A'], 'C': ['B'], 'C2': ['B']}, 'C', 'C2'),
]
for name, parents, remote_tip, local_tip in scenarios:
ok = is_ancestor(parents, remote_tip, local_tip)
verdict = 'fast-forward, push accepted' if ok else 'non-fast-forward, push rejected'
print(f'{name:14} remote={remote_tip:2} local={local_tip:2} -> {verdict}')
The last two scenarios matter most. 'Diverged' is the everyday case: someone pushed while you were working. 'After rebase' is the case where your own commits got new IDs, so even though the content is the same, the old commit on the remote is no longer an ancestor of the new one.
Why the remote has commits you don't#
Before fixing anything, find out what's on the remote that you're missing. Fetch first, because git status and git log only know what your last fetch brought in:
git fetch origin
git status
git log --oneline HEAD..origin/main # commits the remote has, you don't
git log --oneline origin/main..HEAD # commits you have, the remote doesn't
Typical causes:
- A teammate pushed to the same branch. Normal on
mainor any shared integration branch. Pull, then push. - A pull request was merged on GitHub. Squash and rebase merges create new commits on the target branch, so your local
mainis behind. - You pushed from another machine, or edited a file in the web UI and forgot.
- You rewrote history you'd already pushed.
git rebase,git commit --amendandgit resetreplace commits with new ones. The remote still has the old ones. This is the 'after rebase' case, covered below. - A bot or CI job committed a version bump or generated files.
If HEAD..origin/main lists commits you recognize as other people's work, you're in the normal case. If it lists your own commits with the same messages as commits in origin/main..HEAD, you rewrote history.
Pull with rebase, then push again#
This is the recommended fix on a shared branch. It fetches the remote commits, then replays your local commits on top of them, so the result is a straight line with the remote tip as an ancestor of yours:
git pull --rebase origin main
git push origin main
If the rebase hits a conflict, Git pauses. Fix the file, stage it, and continue:
git add path/to/conflicted-file
git rebase --continue
Repeat until the rebase finishes, then push. To bail out and return to where you started, run git rebase --abort.
Confirm it worked: the push output should show a space in the flag column and a summary like 3f2a1c0..9b8d7e6 main -> main. git status should report your branch as up to date with origin/main, and git log --oneline -5 should show the remote's commits directly beneath yours.
Rebasing rewrites your unpushed local commits, which is fine precisely because nobody else has them yet. If you want the reasoning behind that trade-off, rebase vs merge in Git walks through it with examples.
Pull with merge instead#
Merging keeps every commit exactly as it was and adds one merge commit tying the two lines together:
git pull --no-rebase origin main
git push origin main
Conflicts are handled the same way, except you finish with git commit rather than git rebase --continue. After the push, git log --oneline --graph -10 will show the merge commit at the top with both parents below it.
Which should you use? For a short-lived local branch with a few commits, rebase gives a cleaner history and is what most teams prefer on main. Merge is the safer choice when your local commits are already referenced elsewhere, for instance if you cherry-picked them into another branch, or when you're in the middle of a messy conflict and want the simplest possible recovery. The comparison of rebase and merge goes into detail. For the purpose of clearing this error, both work.
If git pull with no flags stops and asks you to choose between rebasing and merging, pass --rebase or --no-rebase explicitly as shown above. The git pull reference covers the remaining options.
Git push rejected non-fast-forward after rebase#
This one confuses people because they didn't pull anything and nobody else touched the branch. The sequence is usually:
- Push
feature/loginwith three commits. - Run
git rebase -iorgit rebase mainto clean up or update the branch. - Push again and get
! [rejected] feature/login -> feature/login (non-fast-forward).
Rebasing created new commits with new IDs. The remote still points at the old ones, and the old tip is not an ancestor of the new tip, so the fast-forward rule fails. Pulling here is wrong: git pull --rebase would replay your new commits on top of your old ones and leave you with duplicates, and git pull --no-rebase would produce a merge of a branch with itself.
The correct fix is a force push, but a guarded one. Read the next section before running it on anything other than a branch you alone work on:
git fetch origin
git log --oneline HEAD..origin/feature/login # should print nothing
git push --force-with-lease origin feature/login
The git log check is there to make sure the only thing you're overwriting is your own old history. If it prints commits, someone else pushed to your branch and you need to rebase onto their work before forcing.
Confirm it worked: the push output shows + in the flag column, the summary uses three dots (a1b2c3d...e4f5a6b, the form Git uses for forced non-fast-forward updates), and a fresh git fetch leaves git status reporting up to date.
When force-with-lease is safe, and when it isn't#
--force tells Git to update the remote branch no matter what it currently points to. --force-with-lease tells Git to update it only if the remote branch still points where your remote-tracking ref (origin/feature/login) says it does. If someone pushed since your last fetch, the remote has moved, the lease fails, and the push is rejected instead of silently deleting their commits. That's the whole difference, and it's why there's rarely a reason to type plain --force.
Two caveats before you rely on it:
- The lease only protects against what you haven't fetched. If your editor fetches in the background,
origin/feature/loginmay already include a teammate's commit that you never looked at. The lease then passes and you overwrite them anyway. Git's--force-if-includesoption, also available as thepush.useForceIfIncludessetting listed in the push configuration section, adds a second check that the remote tip was actually integrated into your local branch. Thegit log HEAD..origin/branchcheck above does the same job by hand. - A lease doesn't make a shared branch safe to rewrite. Even a correctly guarded force push on
mainreplaces history that teammates have based work on. They'll hit this same non-fast-forward error next time they push, and the only clean recovery is for each of them to rebase onto the new history.
My rule of thumb: force-with-lease is for branches with one author, typically a feature branch that's still in review. For anything others pull from, don't rewrite. Add a new commit instead.
The git push documentation shows what a forced non-fast-forward does on the remote. Starting from:
o---o---o---A---B origin/master
\
X---Y---Z dev
running git push origin +dev:master (the leading + on a refspec is another way to force) produces:
A---B (unnamed branch)
/
o---o---o---X---Y---Z master
Commits A and B no longer belong to any branch, and the docs note that git gc on the remote will remove them. That is the data loss the fast-forward rule exists to prevent.
Remote rejected: when the server says no#
A related but different line looks like this:
! [remote rejected] main -> main (pre-receive hook declined)
remote rejected means Git did send the update and the server refused it. The git push docs list the usual causes: a hook on the remote side, or a safety setting on the remote repository such as receive.denyNonFastForwards (which blocks forced updates), receive.denyCurrentBranch, receive.denyDeletes or receive.denyDeleteCurrent. These are documented in the git config reference. On GitHub, a protected branch produces the same kind of refusal.
The important point: forcing won't help. The docs state that no amount of forcing will make the remote accept a push it's configured to deny. If you got remote rejected after --force-with-lease, the branch is protected and the intended path is to not rewrite it. Pull with rebase or merge, push a normal commit, or ask whoever administers the repo to lift the rule for a one-off.
You can tell the two cases apart in a script with git push --porcelain, which prints one tab-separated line per ref:
// Classify lines from `git push --porcelain`.
// Each line: <flag> TAB <from>:<to> TAB <summary> (<reason>)
const lines = [
'!\trefs/heads/main:refs/heads/main\t[rejected] (non-fast-forward)',
'!\trefs/heads/main:refs/heads/main\t[remote rejected] (pre-receive hook declined)',
' \trefs/heads/feature/login:refs/heads/feature/login\t3f2a1c0..9b8d7e6',
'+\trefs/heads/feature/login:refs/heads/feature/login\ta1b2c3d...e4f5a6b',
];
const flags = {
' ': 'fast-forward pushed',
'+': 'forced update succeeded',
'-': 'ref deleted',
'*': 'new ref created',
'!': 'rejected or failed',
'=': 'already up to date',
};
for (const line of lines) {
const [flag, refs, rest] = line.split('\t');
const [from, to] = refs.split(':').map((r) => r.replace('refs/heads/', ''));
let advice = flags[flag];
if (flag === '!' && rest.includes('non-fast-forward')) {
advice += ': fetch, then rebase or merge, then push again';
} else if (flag === '!' && rest.startsWith('[remote rejected]')) {
advice += ': the server refused it, forcing will not help';
}
console.log(`${from} -> ${to} ${advice}`);
}
Common mistakes and gotchas#
- Running
git push --forcebecause an old answer said to. Onmainor any branch others use, this deletes their commits on the remote. If you must force, use--force-with-lease, and only on a branch you own. - Pulling after a rebase. As covered above, this duplicates your commits. If you already did it,
git log --onelinewill show pairs of commits with identical messages. Find the commit from before the pull withgit reflog, check it carefully, thengit reset --hardto it and force-with-lease.reset --harddiscards uncommitted changes, so stash or commit first. - Reading the wrong line. With
push.defaultset tomatching, or with several refspecs, one push can update several branches. The rejection may be about a stale branch you forgot existed. Check thefrom -> topart of the!line. - Pushing to a remote group. When you push to a group of remotes, Git pushes to each in turn, reports any rejection, keeps going, and exits non-zero. A non-fast-forward on one remote doesn't stop the others, so read every line.
- Forgetting to fetch before diagnosing.
git statussaying 'up to date' means up to date with your last fetch. Rungit fetch originfirst. - Pushing
masterwhen the remote branch ismain.git push origin masterfinds a local ref matchingmasterand updates the same name on the remote, creatingmasterrather than updatingmain. Usegit push origin HEAD:mainwhen the local and remote names differ. - IDE clients show the same error. Eclipse, VS Code and others run the same push and get the same rejection. The fix is the same: fetch, rebase or merge, push. If the IDE's pull defaults to merge and you wanted rebase, run the commands in a terminal.
Quick reference#
| Situation | What to run |
|---|---|
| See what the remote has that you don't | git fetch origin && git log --oneline HEAD..origin/main |
| Shared branch, want linear history | git pull --rebase origin main && git push origin main |
| Shared branch, want a merge commit | git pull --no-rebase origin main && git push origin main |
| Conflict during rebase | fix the file, git add <file>, git rebase --continue |
| Abandon a rebase | git rebase --abort |
| Your own feature branch, rewritten on purpose | git fetch origin && git push --force-with-lease origin feature/x |
| Extra guard against background fetches | add --force-if-includes |
[remote rejected] instead of [rejected] |
server hook or branch protection; don't force, push a normal commit |
| Local and remote names differ | git push origin HEAD:main |
Before you push, a short checklist:
git fetch originso your view of the remote is current.git log --oneline HEAD..origin/<branch>to see what you'd be overwriting.- If those are other people's commits, rebase or merge them in. If they're old versions of your own commits on a branch only you touch, use
--force-with-lease. - After pushing, check the flag column: a space means fast-forward,
+means forced,!means it still failed.
Quick answers
Why is git push rejected as non-fast-forward right after a rebase?
Rebasing gave your commits new IDs, so the remote's old tip is no longer an ancestor of your new tip. Don't pull; fetch, confirm nothing new landed on the remote branch, then push with --force-with-lease if the branch is yours alone.
Can I force push to master or main to clear the error?
You can, but it replaces commits on the remote that teammates may have based work on, and Git's docs note those orphaned commits can later be removed by git gc. Pull with rebase or merge instead, and reserve --force-with-lease for single-author feature branches.
What's the difference between [rejected] and [remote rejected]?
[rejected] means your local Git didn't send the ref because the update wasn't a fast-forward and you didn't force. [remote rejected] means the server refused it, usually a hook or a receive.deny* setting, and forcing won't get past that.
I get push rejected non-fast-forward in Eclipse. Is the fix different?
No. Eclipse runs the same push and hits the same rule. Fetch, then pull with rebase or merge from the IDE or a terminal, resolve conflicts, and push again.
GitHub says push rejected non-fast-forward even though I just pulled. Why?
Someone probably pushed between your pull and your push, or your pull hit a different branch than the one you're pushing. Run git fetch origin and git log --oneline HEAD..origin/<branch> to see exactly which commits you're missing.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.
Network
0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
On a computer? Scan the code with your phone wallet, or copy the address.
Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note