Skip to content

Push safety quick reference

Client-side safe force-push

Command What it protects
git push --force-with-lease origin feat/x Refuses if remote ref moved since last fetch
git push --force-with-lease=feat/x:<sha> Explicit lease with expected old SHA
git push --force-if-includes origin feat/x Also checks remote tip is reachable locally
git push --atomic origin main v1.0 All or nothing — no partial push
git push --signed origin main Signs the push certificate for audit

Default config (one-time setup)

git config --global push.useForceIfIncludes true
git config --global push.default simple

Server-side deny rules

Set on the receiving (bare) repository:

git config receive.denyNonFastForwards true   # blocks force-push
git config receive.denyDeletes true            # blocks ref deletion
git config receive.fsckObjects true            # validates every object
git config receive.certNonceSecret "$(openssl rand -hex 32)"  # requires signed pushes
git config transfer.fsckObjects true           # validates clones/fetches
git config receive.maxObjectSize 10485760      # reject objects > 10 MB

CI pipeline push patterns

# Squash-and-push for feature branches (safe force-push)
git fetch origin main
git rebase origin/main
git push --force-with-lease --force-if-includes origin "$CI_BRANCH"

# Release push (atomic — commit + tag must land together)
git push --atomic origin main "v$VERSION"

# Merge PR into protected main (no force required)
git merge --no-ff "$MERGE_SOURCE"
git push origin HEAD:main

Pre-receive hook: enforce signing on main

while read old new ref; do
  case "$ref" in refs/heads/main)
    for sha in $(git rev-list "$old..$new"); do
      git verify-commit "$sha" &>/dev/null || { echo "Rejected: unsigned commit"; exit 1; }
    done
  esac
done
exit 0

Audit post-receive hook

while read old new ref; do
  echo "$(date -Iseconds) | $ref | $old..$new | ${GIT_PUSH_OPTION_0:-unknown}" >> /var/log/git-push-audit.log
done

Inspect push safety config

```bash git config --local --list | grep -E '^receive.' git config --global --list | grep -E '^push.' git config --global --list | grep -E '^transfer.' git ls-remote origin # confirm remote refs before push