git push -f tells the remote to accept your branch even though it is not a fast-forward — overwriting whatever was there. On your own feature branch after a rebase, that is routine and correct. On a shared branch, it can delete a colleague’s commits with no warning and no record on the remote.
Force pushing has a reputation somewhere between routine and forbidden, and both views are half right. The useful framing is not is force push dangerous but whose commits am I about to overwrite, and how would I know. There is a flag that answers the second question for you, and almost nobody uses it.
Table of contents
- What force push actually does
- Use —force-with-lease instead
- When force pushing is genuinely wrong
- Recovering when it has already happened
- Rebase and force push as a normal workflow
- How this fits the rest of the stack
- FAQ
What force push actually does
Normally a push is refused unless it is a fast-forward — the remote branch’s tip must be an ancestor of what you are pushing. That check is what stops you discarding commits you have not seen.
$ git push
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
--force removes the check. The remote branch pointer moves to your commit regardless of what it was pointing at.
The commits are not immediately deleted — they become unreferenced and are removed later by garbage collection. That gap is what makes recovery possible, and it is not indefinite.
Legitimate reasons to force push:
- You rebased your feature branch onto an updated main. The history genuinely changed.
- You amended a commit to fix a message or add a forgotten file.
- You squashed several commits before review.
- You removed a secret or a large file from history.
All four are normal on a branch you own. None are normal on main.
Use —force-with-lease instead
This is the practical takeaway. --force-with-lease force-pushes only if the remote is where you last saw it. If someone else pushed in the meantime, it refuses.
# Refuses if the remote moved since your last fetch
git push --force-with-lease
# Stricter: also requires the remote ref to be exactly this commit
git push --force-with-lease=main:abc123f
# Newer, and stricter still: also fails if you have stale remote-tracking
# refs, which closes the background-fetch hole below
git push --force-if-includes --force-with-lease
It gives you exactly what you wanted from --force — replace remote history — with the safety check you did not know you were skipping.
One trap worth knowing. --force-with-lease compares against your remote-tracking ref, so anything that silently updates it undermines the check. An editor or IDE running git fetch in the background can update the ref without you seeing the new commits, and the lease then passes on work you have never looked at.
# Make the safer form your default
git config --global alias.pushf 'push --force-with-lease --force-if-includes'
# Then use it everywhere
git pushf
When force pushing is genuinely wrong
Shared branches. main, develop, release branches — anything more than one person builds on. Everyone with the old history now has a divergent local branch, and the recovery advice they find online is git reset --hard origin/main, which throws away their own work too.
Branches with open pull requests you do not own. Reviewers lose their place, and inline comments detach from the lines they referenced.
As a way of resolving a conflict. Force pushing to make a rejection go away discards whatever caused the rejection. That rejection was information.
The structural fix is to make it impossible rather than to rely on discipline:
# Server-side: reject all force pushes to this branch
git config --system receive.denyNonFastForwards true
On any hosted platform, branch protection does this properly — block force pushes on main, require pull requests, require passing checks. This is the single highest-value repository setting, and it converts a class of incident into an error message.
Recovering when it has already happened
Someone force-pushed over work. The commits are unreferenced, not gone, and the reflog is the way back.
# On any machine that had the commits, including the author's
git reflog show origin/main
# abc123f refs/remotes/origin/main@{0}: update by push
# def456a refs/remotes/origin/main@{1}: update by push <- before the force
# Inspect before acting
git log def456a --oneline -10
# Restore
git push --force-with-lease origin def456a:main
If nobody has the commits locally, hosted platforms keep their own record. GitHub’s activity view and events API list every ref update with the SHA, and the object usually survives on the server long enough to be recovered by pushing it from a fetch.
# Recover a specific object that is no longer referenced
git fetch origin abc123f
git branch recovered abc123f
# Locally, find anything orphaned
git fsck --lost-found
Act quickly. Garbage collection eventually removes unreferenced objects, and the window is finite. Recovery an hour later is routine; a month later it may be impossible.
The best recovery is the one you do not need. git reflog on your own machine records every position HEAD has held for ninety days by default, which means almost nothing you do locally is truly irreversible.
Rebase and force push as a normal workflow
For teams that keep a linear history, force pushing feature branches is not an incident, it is the routine.
git checkout feature/checkout
git fetch origin
git rebase origin/main
# Rebasing rewrote your commits -- a force push is now required
git push --force-with-lease
This is fine because the branch is yours. The rules that keep it fine:
- One owner per branch. If two people are pushing to the same feature branch, rebasing it is antisocial.
--force-with-lease, always. It costs nothing and catches the case where someone did push.- Never rebase a branch someone else has already based work on.
- Tell reviewers when you rebase mid-review, since their comments may detach.
git pull --rebase as a default avoids a related mess — merge commits generated by routine pulls, which clutter history and produce the divergence that tempts people to force push in the first place:
git config --global pull.rebase true
git config --global rebase.autoStash true
One consequence worth planning for: a force-pushed branch changes every commit SHA. If your deployment is wired to commit hashes, that matters — on RunxBuild each deploy maps to a commit with rollback to the previous deploy, so rewriting a branch’s history after deploying from it means the rollback target refers to a commit no longer on the branch. Not a problem, as long as nobody assumes otherwise.
How this fits the rest of the stack
Force pushing is fine on a branch you own and a bad idea on one you share. Make --force-with-lease your habit so Git checks for you, turn on branch protection so main cannot be overwritten at all, and remember the reflog when it does go wrong. If you are setting up deploys where each commit is a rollback point, the RunxBuild hosting calculator shows the build and runtime costs separately.
Useful related references:
- MongoDB push: Append to Arrays Without Growing Documents Forever
- How to Rename a File in Linux: mv, rename, and git mv
- Undoing a Merge in Git: Revert, Reset, and the Trap That Follows
- Services on RunxBuild
FAQ
What does git push -f do?
It pushes your branch to the remote even when the update is not a fast-forward, moving the remote branch pointer to your commit and leaving whatever was there unreferenced. Those commits survive until garbage collection removes them.
What is the difference between —force and —force-with-lease?
—force overwrites unconditionally. —force-with-lease overwrites only if the remote branch is still where you last saw it, so it refuses when someone else has pushed in the meantime.
Is it ever safe to force push?
Yes, on a branch only you work on — after a rebase, an amend, or a squash. It is not safe on shared branches like main, where it can discard commits other people are building on.
How do I recover commits after someone force pushed over them?
Use git reflog show origin/main on a machine that had the old history to find the previous SHA, then push it back with —force-with-lease. Act quickly, since unreferenced objects are eventually garbage collected.
How do I stop force pushes to main?
Enable branch protection on your hosting platform to block force pushes and require pull requests. On a self-hosted remote, set receive.denyNonFastForwards to true.