Git, CI/CD & GitOpsXXII · Force PushForcePush
`git push --force-with-lease` — the safe force-push
What you'll learn
- Explain the tip-divergence check `--force-with-lease` performs before accepting a force-push
- Read the `stale info` rejection message and identify what it implies about the remote state
- Use `git push --force-with-lease=$REFNAME:$EXPECT` to check a specific ref against a specific expected OID
- Distinguish the protection `--force-with-lease` provides (tip divergence) from what it does not (semantic divergence)
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
git push --force-with-lease is the safe variant of --force. The
two operations are identical except for one extra check: before
overwriting the remote’s branch ref, --force-with-lease verifies
that the remote’s current tip is still the OID the local
remote-tracking ref expects. If the remote has moved since the
last fetch, the push is refused. The check costs one SHA
comparison on the server; the value is that the engineer cannot
silently clobber a teammate’s commit that arrived between their
last fetch and their push.
What the lease checks
The local remote-tracking ref (origin/feature/iam-rotation) is
the engineer’s recorded view of the remote’s branch tip, last
updated by git fetch. When --force-with-lease runs, it sends
the expected tip (the OID in the remote-tracking ref) to the
remote as part of the push operation. The remote compares the
expected tip against its current tip; if they match, the push
proceeds; if they differ, the push is refused.
git fetch origin # local view: origin/feature/iam-rotation = A1
# (teammate pushes commit C1; remote tip is now A1+C1)
git rebase origin/main # local branch rewritten to A2
git push --force-with-lease origin feature/iam-rotation
# To git@github.com:acme/iac.git
# ! [rejected] feature/iam-rotation -> feature/iam-rotation (stale info)
# error: failed to push some refs to 'git@github.com:acme/iac.git'
The stale info rejection is the safety net. It means the
remote’s tip is no longer what the local view expected. The most
common cause is that a teammate has pushed a commit to the same
branch since the last fetch; the force-push would have clobbered
that commit, and the lease caught it.
flowchart LR
subgraph LOCAL["local"]
LE["local feature = A2 (rewritten)"]
LR["origin/feature = A1 (stale)"]
end
subgraph REMOTE["remote"]
RR["feature = A1 + C1 (teammate)"]
end
LOCAL -->|"git push --force-with-lease"| CHECK{"remote tip == origin/feature?"}
CHECK -->|"yes (A1 == A1)"| PUSH["push accepted"]
CHECK -->|"no (A1+C1 != A1)"| REJECT["stale info rejection"]
The check is a single SHA comparison on the server side. The cost is negligible; the value is that the engineer cannot push to a remote tip they have not seen.
Recovering from a lease rejection
The right recovery from a --force-with-lease rejection is to
fetch, rebase (or merge) onto the new remote tip, and try again.
The engineer never clobbers the teammate’s commit, because the
lease caught it.
# Refused: teammate pushed C1 between fetch and push
git fetch origin # updates origin/feature/iam-rotation to A1+C1
git rebase origin/feature/iam-rotation # rebases local commits onto teammate's
git push --force-with-lease origin feature/iam-rotation
# (succeeds: local is now based on teammate's tip)
The recovery is the same procedure the engineer should have used in the first place: fetch, look at what is on the remote, rebase or merge if necessary, push. The lease makes that procedure mandatory; without the lease, the engineer could have skipped the fetch and the merge and silently clobbered the teammate’s commit.
The explicit form: --force-with-lease=$REFNAME:$EXPECT
The default form checks every ref being pushed against the local remote-tracking ref. The explicit form checks a specific ref against a specific expected OID:
git push --force-with-lease=refs/heads/feature/iam-rotation:0a1b2c3d4e5f origin feature/iam-rotation
This form is useful in scripts and CI steps where the local
remote-tracking ref might be stale or unavailable. The $EXPECT
is the OID the engineer expects the remote to be at; if the remote
is at any other OID, the push is refused.
When --force-with-lease is the right tool
The default for any force-push on a feature branch you believe you own:
- Solo work on a feature branch. The branch has been pushed
but no teammate has pulled it.
--force-with-leaseconfirms this is still true at push time. - Rebase-and-republish workflows. After
git rebase origin/main, the local branch is no longer a fast-forward of the remote;--force-with-leaseis the way to publish the rebased branch without clobbering a teammate. - CI-driven squash merges. A CI job that squashes a feature
branch into a single commit and force-pushes the squashed
branch back to the source branch should use
--force-with-leaseto detect a teammate’s concurrent push.
The default for any push where the branch is clearly shared:
- Trunk branches. Use
--force-with-leaseif you must push (and you almost certainly should not be pushing at all to a trunk; the right tool is a merge queue or a PR merge). - Release branches. Same as trunk; the release branch is shared by definition.
- Branches with signed tags. The tag points at a specific OID; the lease protects against rewriting the OID out from under the tag.
Production discipline
- Default to
--force-with-lease, never--force. The change is one word (--force->--force-with-lease); the benefit is the most common clobber case caught automatically. There is no operational reason to use plain--force. - Fetch immediately before force-pushing. The lease is only as fresh as the last fetch; a force-push that follows a fresh fetch is the strongest single-command safety net Git provides.
- Treat a lease rejection as information, not failure. The rejection tells you a teammate has pushed since your last fetch; the right action is to fetch, look at what they pushed, and rebase or merge accordingly.
- Document the policy in CONTRIBUTING. A team’s policy on which branches can be force-pushed and which flag is required belongs in the repository’s contributing guide, not in individual engineers’ heads.
Cross-course references
- Git, CI/CD & GitOps - Part XI (Rebasing) lesson 06
introduces
--force-with-leasein the context of the shared-history risks of rebasing; this lesson is the detailed treatment of the flag itself. - Git, CI/CD & GitOps - Part XX (Remotes) lesson 03 covers remote-tracking branches, which are the source of the expected OID that the lease checks against.
- CI/CD Pipeline Patterns - Part V (MergeQueues) shows
how merge queues use
--force-with-leaseon a temporary branch to avoid rewriting shared history.
Quiz
Knowledge check · 4 questions
Q1. What does `git push --force-with-lease` check that `git push --force` does not?
Q2. `git push --force-with-lease` catches every kind of force-push mistake, including semantic divergence and stale remote views.
Q3. What is the recommended recovery procedure after a `git push --force-with-lease` is rejected with a 'stale info' message, and why?
Q4. Decide whether `--force-with-lease` is the right tool for the proposed workflow and recommend the correct sequence of commands.
An engineer is the sole contributor on `feature/iam-rotation`. They rebase the branch onto the latest `origin/main`, run their tests locally, and want to publish the rebased branch. They intend to run `git push --force-with-lease origin feature/iam-rotation`. They have not run `git fetch` since starting the rebase. There is a CI pipeline that builds every push to feature branches. There is no branch protection on `feature/*` branches.
Passing score: 75%. Answers are checked in this browser.