Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXXII · Force PushForcePush

`git push --force-with-lease` — the safe force-push

Advanced⏱ ~20 mingit

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

Not yet marked complete on this device.

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-lease confirms 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-lease is 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-lease to detect a teammate’s concurrent push.

The default for any push where the branch is clearly shared:

  • Trunk branches. Use --force-with-lease if 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

  1. 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.
  2. 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.
  3. 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.
  4. 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-lease in 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-lease on a temporary branch to avoid rewriting shared history.

Quiz

Knowledge check · 4 questions

  1. Q1. What does `git push --force-with-lease` check that `git push --force` does not?

  2. Q2. `git push --force-with-lease` catches every kind of force-push mistake, including semantic divergence and stale remote views.

  3. Q3. What is the recommended recovery procedure after a `git push --force-with-lease` is rejected with a 'stale info' message, and why?

  4. 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.