The proximate cause is a git push --force without --force-with-lease,
but the root cause is the policy that allowed it. The default branch should
never accept a force push; if it ever did, it is a policy gap to be closed,
not a habit to be trained.
← All break/fix scenarios in Git, CI/CD & GitOps
Unsafe force push destroyed teammate's commit
Reported symptoms
- ●`origin/main` advanced from SHA `aaa` to `ccc` but `bbb` (pushed by a teammate ten minutes ago) is not in the new history
- ●The teammate's last commit, on their own local branch, shows "Your branch is behind `origin/main` by 3 commits" and they cannot understand how — they had merged it ten minutes ago
- ●The engineer who pushed remembers an interactive rebase and a `git push --force`
- ●The repo's audit log shows the ref-update event for `refs/heads/main` with the old SHA `aaa` and the new SHA `ccc`
- ●No PR exists between `aaa` and `ccc`; the force push was a direct push to the default branch
- ●CI for the teammate's feature branch is failing on a commit they did not write
- ●`git fsck --unreachable` on the engineer's clone shows the orphaned `bbb` and its parents as dangling
Evidence
- · `git reflog show origin/main` on the engineer's clone shows `aaa → ccc (forced update)` with the timestamp of the incident
- · `git log --oneline origin/main` shows the post-rebase history with `bbb` and its parents absent
- · `git log --oneline <bbb-sha>` returns the commit message and diff, but `<bbb-sha>` is not reachable from any ref
- · The teammate's `git log --oneline feature/x` ends at a commit whose tree matches the tree of `<bbb-sha>` — same content, different SHA
- · `git reflog show feature/x` on the teammate's clone shows `bbb → origin/main` and then `merge feature/x into main` followed by `rebase -i (pick)` and `rebase -i (finish)`
- · The GitHub/GitLab audit log entry for the force push has actor `<engineer>` and event `push.force`
- · `git config --get-regexp ^receive` on a self-hosted equivalent returns `receive.denyNonFastForwards true` — the push should not have succeeded under policy
Diagnosis and resolutionclick to reveal
Root cause
`git push --force` (without `--force-with-lease`) is an unconditional ref-update: it overwrites the remote ref with whatever the local ref points at, regardless of what has been pushed to the remote in the meantime. The `--force-with-lease` form compares the local view of the remote ref against the actual remote ref and refuses to overwrite if they differ — which is exactly the protection that would have caught this incident, because the teammate's ten-minute-old push would have advanced the remote ref between the engineer's fetch and their push. The underlying structural failure is that the default branch accepted force pushes at all: branch protection with "Block force pushes" disabled, or the equivalent `receive.denyNonFastForwards` set to false on a self-hosted Git server. The engineer is the proximate cause; the policy is the root cause.
Remediation
Restore the orphaned commits before they expire. The previous remote SHA is recoverable from `git reflog show origin/main@{1.hour.ago}` (or the reflog of a colleague's clone that still has `origin/main` pointing at `aaa`) and from the platform's ref-update audit log. Push that SHA back to `origin/main` with `git push --force-with-lease origin <aaa-sha>:main` — using `--force-with-lease` even in the recovery, so the recovery itself cannot create a second orphaning. Notify the teammate immediately so they can rebase their feature branch onto the restored `aaa`. Audit the diff between `aaa` and the engineer's `ccc` to confirm the engineer's work was preserved on the restored history (it should have been, because the engineer rebased onto a commit that was already on `aaa`). Disable force pushes on `main` and every protected branch before any further work proceeds.
Verification
`origin/main` includes both the engineer's rebased chain and the teammate's commits in the expected order. The teammate's local branch rebases cleanly onto the restored `main` (no missing commits, no unexpected conflicts). CI is green on both branches. A second force-push attempt against `main` is refused by the remote with the platform's expected error message (`Remote rejected (protected branch hook declined)` or equivalent). The team's `git push` alias no longer accepts a bare `--force` — only `--force-with-lease` or a deliberately-empty `--force` bypass.
Prevention
Make the destructive operation impossible at the policy layer. Enable "Block force pushes" on every protected branch in GitHub/GitLab and set `receive.denyNonFastForwards true` on every self-hosted Git server. Force `--force-with-lease` on every developer by aliasing `git push` to `git push --force-with-lease` in the team's `~/.gitconfig` template, and add a `pre-push` hook that detects any non-fast-forward update and asks for an explicit `--allow-orphan` flag. For shared branches where rewriting is genuinely necessary (release tags, changelog files), use a separate ref namespace (`refs/tags/` versus `refs/heads/`) with its own protection rule, so that the policy that protects code branches does not have to be loosened to accommodate tag maintenance. The structural control is the policy: a force push that requires a `--force-with-lease` flag is a force push that the author has to think about, every time.