Force-push safely
After rebasing or amending, a normal push is rejected as non-fast-forward. git push --force overwrites the remote and can erase teammates' commits. git push --force-with-lease is safer: it refuses if the remote moved since your last fetch. Never force shared branches like main.
You clean up your feature branch with an interactive rebase, try to push, and git rejects it: (non-fast-forward). Of course — you rewrote the commits, so the branch no longer extends the one on the server. The tempting fix is git push --force. It works, but --force is also the single command most likely to silently delete a teammate’s work. There is a safer twin, --force-with-lease, and knowing the difference is what separates a careful engineer from the one who sends the “sorry, I overwrote main” message.
By the end of this lesson you will be able to push rewritten history without destroying anyone else’s, by reaching for --force-with-lease and knowing which branches must never be forced at all.
After this lesson you can explain why rewritten history needs a force push, use git push --force-with-lease as the safe default, describe exactly how it differs from --force, and state the rule for which branches may be force-pushed.
Rewriting history makes your branch diverge from the remote, so a normal push is rejected. Commands like git rebase, git commit --amend, and git reset do not edit commits in place — they create new commits with new hashes and move your branch to them. The old commits, which the remote still points at, are now gone from your branch’s line:
git commit --amend -m "Better message" # new hash; old commit replaced
git push
# ! [rejected] feature -> feature (non-fast-forward)The remote’s feature still has the original commit, which your rewritten branch no longer contains — so pushing is no longer a fast-forward.
git push --force overwrites the remote branch unconditionally — and that is the danger. Force tells git: “make the remote branch point exactly where mine points, no matter what.” It does not check what is currently on the server:
git push --force origin featureIf, between your last fetch and this push, a teammate pushed a commit you never saw, --force discards it. The commit is not merged, not warned about — it is simply replaced. On a branch only you use, that is fine. On a shared one, it is data loss.
git push --force-with-lease refuses if the remote moved since your last fetch. This is the safe version. Git remembers what it believed the remote branch pointed at (your origin/feature). The push goes through only if the server still matches that — i.e. nobody pushed in the meantime:
git push --force-with-lease origin feature
# Succeeds only if origin/feature is where you last saw it.
# Otherwise: ! [rejected] (stale info) — and your push is refused.A “lease” is like a claim: “I believe the remote is at X; let me overwrite only if that is still true.” If someone else pushed, your belief is stale, the lease is broken, and git stops you before you clobber their commit.
▸Why this works
Why does --force-with-lease actually protect you when --force does not? --force compares nothing — it just imposes your tip. --force-with-lease compares the remote’s current position against your remote-tracking ref (origin/feature) and aborts on mismatch. The subtle trap: a git fetch (or an auto-fetch by your IDE) silently updates origin/feature to the teammate’s new commit, which refreshes your lease and lets a later force-with-lease succeed anyway. So the rule is: never blindly fetch-then-force-with-lease; review what changed first. Even the safe tool needs you to look.
Never force-push a shared branch like main; only your own feature or PR branches. The boundary is whether others build on the branch:
- Safe to force: your personal
feature/xor PR branch, where you are the only author and a rebase/amend is expected before merge. - Never force:
main,develop, release branches — anything teammates pull from and base work on. Rewriting them rewrites history out from under everyone, breaking their clones.
Many teams enforce this with protected branches on GitHub/GitLab that reject any force push to main outright, turning the social rule into a hard guardrail.
The same force push, with and without a lease.
You rebased your feature branch to drop a stray commit; locally it now ends at R. Your origin/feature still shows the old tip O from your last fetch. Unknown to you, a teammate just pushed a review fix T on top of O, so the server’s feature is actually at T.
git push --forceignores the server’s state and setsfeaturetoR. The teammate’s commitTis now unreachable — gone from the branch, no warning. They pushed work and it vanished.git push --force-with-leasecompares the server (T) against your rememberedorigin/feature(O). They differ, so the lease is stale and git aborts: “stale info, refusing to update”. You thengit fetch, seeT, rebase your work on top of it, and push — keeping both your cleanup and their fix.
Same intent, same rewritten branch. The lease is the only thing that turned a silent deletion into a safe stop-and-look.
▸Common mistake
A classic error is treating --force-with-lease as magic that can never lose data, then running git fetch right before it out of habit. The fetch advances origin/feature to the teammate’s commit, so the lease now matches the server and the force succeeds — overwriting exactly the commit the lease was supposed to protect. The discipline: when force-with-lease is rejected, stop and inspect, do not “fix” it by fetching and retrying. The rejection is the protection working.
You rebased your feature branch and the push is rejected as non-fast-forward. A teammate may have pushed to it since you last fetched. Which command is the safe way to push your rewritten history?
Rewriting history with rebase, amend, or reset creates new commit hashes, so your branch no longer fast-forwards the remote and a normal push is rejected as non-fast-forward. git push --force overwrites the remote unconditionally and can silently erase a teammate’s commits. git push --force-with-lease is the safe default: it overwrites only if the remote still matches your last-fetched origin/<branch>, aborting when someone else has pushed — but a blind git fetch right before it refreshes the lease and removes that protection. Force-push only your own feature/PR branches; never main or other shared branches, which protected-branch rules should block outright. Next you will lift this collaboration up to the platform layer — the pull request.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.