Git, CI/CD & GitOpsXX · RemotesRemotes
Remote pruning and cleanup — keeping the local cache honest
What you'll learn
- Explain what git fetch --prune does, which refs it removes, and which it leaves alone
- Run git remote prune to remove stale refs without performing a full fetch
- Inspect a ref with git for-each-ref before pruning to confirm it is stale and not a local branch
- Configure fetch.prune = true so every fetch in the clone prunes by default
- Diagnose "phantom branch" errors caused by stale remote-tracking refs that survived a deletion on the remote
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
A Git clone accumulates stale data over time: branches that
existed on the remote six months ago but have since been deleted
will still be represented in the local remote-tracking refs
until something explicitly cleans them up. The cleanup operation
is pruning, and it is one of the safest operations in Git:
it only touches refs under refs/remotes/<name>/, it never
deletes a local branch, and it never deletes a commit or an
object. A pruned ref is a ref that has been removed because the
remote no longer reports it; nothing more.
What gets stale, and why
Every git fetch <remote> updates the remote-tracking refs
under refs/remotes/<remote>/ to reflect the current state of
the remote. But the default fetch is additive: it adds new
refs and updates existing ones, but it does not remove refs
whose remote counterparts have disappeared.
# A branch is deleted on the remote
# (someone ran `git push origin --delete feature/old-thing`)
# The next git fetch does NOT remove the local mirror
git fetch origin
# (no output for the deleted branch — fetch is silent on deletions)
git branch -r
# origin/main
# origin/feature/old-thing <-- still here, but the remote doesn't have it
The reason for the additive default is safety. A branch that
existed on the remote yesterday should not be silently removed
from the local cache today just because the network call
succeeded — there might be a network or DNS issue that made the
remote’s advertisement incomplete, and silently deleting a ref
that turns out to be valid is worse than keeping it. The
opt-in to remove stale refs is --prune.
flowchart LR
A["remote has: main, feature/x"] --> B["local after fetch:\nrefs/remotes/origin/main\nrefs/remotes/origin/feature/x"]
C["feature/x deleted on remote"] --> D["local after additive fetch:\norigin/main (updated)\norigin/feature/x (stale)"]
C --> E["local after prune:\norigin/main (updated)\norigin/feature/x removed"]
The additive behaviour is defensive: it assumes the network
view might be incomplete and prefers to keep a stale ref over
deleting a valid one. The cost is that the local cache
accumulates phantom refs over time; the cleanup is --prune.
git fetch —prune
git fetch --prune (or git fetch -p) is the standard way to
refresh the local cache and remove refs that the remote no
longer advertises. The operation has two phases:
- Fetch phase — the same as a normal
git fetch: contact the remote, transfer objects, update existing remote-tracking refs to their new OIDs, add new remote-tracking refs for any branches the remote advertises but the local clone does not have. - Prune phase — for every ref currently under
refs/remotes/<name>/, check whether the remote advertises it. If the remote does not advertise the ref, delete it from the local namespace.
# Pruning the origin remote
git fetch --prune origin
# remote: Enumerating objects: 12, done.
# From github.com:acme/infra
# 8a3f9d2..4d2c8e0 main -> origin/main
# * [new branch] feature/oidc -> origin/feature/oidc
# - [deleted] (none) -> origin/feature/old-thing
# Verify the deletion
git branch -r
# origin/main
# origin/feature/oidc
# (origin/feature/old-thing is gone)
The - [deleted] line in the fetch output is the only
indication that pruning occurred; it is the visible record of
the removal.
Pruning is safe because:
- It only touches refs under
refs/remotes/<name>/. Local branches underrefs/heads/are not affected. - It only deletes a ref that the remote did not advertise in this fetch’s response. A network or DNS failure that made the remote’s advertisement incomplete would result in pruning false positives (refs being deleted that are still valid on the remote), but in that case the next successful fetch would add them back.
- The commits and objects those refs pointed at are not
deleted. Pruning removes the ref; the underlying commits
remain in the object database and will be cleaned up later by
git gcif nothing else references them.
git remote prune
git remote prune <name> runs the prune phase only, without
performing a full fetch. It contacts the remote to learn its
current ref advertisement, then deletes local remote-tracking
refs that the remote no longer advertises.
# Prune without fetching
git remote prune origin
# From github.com:acme/infra
# - [deleted] (none) -> origin/feature/old-thing
The difference from git fetch --prune is that remote prune
does not transfer new objects. It is useful in three situations:
- The local cache is known to be current (a recent fetch ran), and only the deletion needs to be applied.
- The network is slow and a full fetch is too expensive; a prune-only operation transfers no objects, only the ref advertisement.
- The operation needs to be scripted as a periodic cleanup step, separate from the regular fetch cadence.
flowchart LR
A["git remote prune origin"] --> B["contact remote\nadvertise refs only"]
B --> C["for each refs/remotes/origin/*\nnot in remote advertisement:\ndelete ref"]
C --> D["no object transfer\nno local branch touched"]
In production, git fetch --prune is the right command for a
periodic refresh-and-clean step; git remote prune is the
right command when the cleanup needs to be independent of the
fetch cadence (e.g., a nightly job that just cleans stale
refs without re-pulling everything).
What pruning does NOT do
Three things that look like pruning but are not:
- It does not delete local branches. A local branch under
refs/heads/is unaffected by any prune operation. A localfeature/old-thingbranch is independent of theorigin/feature/old-thingremote-tracking ref. To delete a local branch, usegit branch -d <name>orgit branch -D <name>(the-dis for merged branches;-Dis forced). - It does not delete commits or objects. Pruning deletes a
ref (a name that points at an object). The object the ref
pointed at remains in the object database until
git gccollects it as unreachable. If the deleted ref was the only handle to a chain of commits, those commits will become unreachable and will eventually be garbage-collected. - It does not touch upstream configurations. A local branch
whose upstream was
origin/feature/old-thingdoes not have its upstream configuration cleared by a prune. The configuration still names the (now-pruned) ref;git pullwill fail with “couldn’t find remote ref” until the upstream is reset withgit branch --unset-upstreamor--set-upstream-to.
The third point is important: after a prune, every local branch whose upstream was pointing at a pruned ref has become orphaned. The fix is to repair the upstream configuration per branch:
# Find orphaned branches after a prune
git for-each-ref --format='%(refname:short) %(upstream:short)' refs/heads | grep -v ' $'
# feature/old-thing (no upstream — good)
# feature/iam-rotation origin/feature/old-thing (orphaned)
# Repair the orphaned branch
git branch --set-upstream-to=origin/feature/iam-rotation feature/iam-rotation
Inspecting before pruning
For high-stakes remotes, the right workflow is to inspect the
refs that would be pruned before pruning them. git for-each-ref with a format string can list every remote-tracking
ref under a remote, and the remote’s live advertisement can be
read with git ls-remote. The diff between the two is the set
of refs that --prune would remove.
# What the local clone currently has
git for-each-ref --format='%(refname:short)' refs/remotes/origin
# origin/HEAD
# origin/main
# origin/feature/old-thing
# What the remote currently advertises
git ls-remote origin | awk '{print $2}' | sed 's|^refs/heads/|origin/|'
# origin/HEAD
# origin/main
# The diff is the prune set
diff <(git for-each-ref --format='%(refname:short)' refs/remotes/origin | sort) \
<(git ls-remote origin | awk '{print $2}' | sed 's|^refs/heads/|origin/|' | sort)
# < origin/feature/old-thing
The diff shows origin/feature/old-thing is on the local side
but not on the remote side; that is the ref --prune will
remove. Once the inspection confirms the prune set is correct,
git fetch --prune (or git remote prune) can be run.
Configuring prune-by-default
For clones that fetch regularly, the additive behaviour is more
work than it is worth: every fetch leaves phantom refs that have
to be cleaned by a subsequent --prune. Configuring
fetch.prune = true in the clone’s config (or the user’s
global config) makes every fetch prune by default.
# Configure for one clone (in .git/config)
git config fetch.prune true
# Configure globally for all clones (in ~/.gitconfig)
git config --global fetch.prune true
# Confirm
git config --get fetch.prune
# true
With fetch.prune = true, every git fetch (and git pull)
removes stale remote-tracking refs as part of the operation;
the additive behaviour is opt-out per-clone by
fetch.prune = false. In production, fetch.prune = true is
the right default for any clone that depends on remote state
staying current; the safety property (only
refs/remotes/<name>/* is touched) makes the default safe to
leave on.
Production discipline
- Configure
fetch.prune = truein clone bootstrap. A periodic fetch that leaves phantom refs is a periodic fetch that is doing half its job. The safety property ofrefs/remotes/<name>/*being the only namespace touched makes the opt-in safe by default. - Inspect before pruning high-stakes remotes. The diff
between
git for-each-ref refs/remotes/<remote>andgit ls-remote <remote>is the exact prune set; review it before running the prune for any remote whose deletions would be costly to recover from. - Prune does not fix orphaned upstream configurations.
After a prune, repair every local branch whose upstream was
pruned, with
git branch --unset-upstreamor--set-upstream-to. - Treat a pruned ref as a deletion of the local cache, not of the remote branch. The remote branch was deleted by the upstream; the prune simply updated the local mirror. Confusing the two leads to engineers thinking Git deleted a branch they cared about.
- Run
git fetch --prune(orfetch.prune = true) as part of every CI pipeline. A CI job that depends on the remote-tracking namespace should not accumulate phantom refs across runs; the cleanup should happen on every fetch.
Cross-course references
- GitOps with Argo CD - Part VII (PrunePolicy) uses the same idea of distinguishing “delete on the remote” from “remove the local handle”. Argo CD’s prune policy mirrors the Git prune operation: it removes the local Kubernetes resource when the corresponding remote manifest disappears, but only if the prune policy is enabled.
- Ansible for Production Sysadmins - Part XLI (MirrorSync)
uses
git fetch --pruneon a mirror clone to keep the mirror’s remote-tracking refs aligned with the primary repository’s current branch set. - Terraform for Production Sysadmins - Part XVII (ModCache)
uses
git fetch --prunein the module cache to invalidate stale Terraform module references; a stale ref is a phantom that the nextterraform initwill resolve to a missing branch.
Quiz
Knowledge check · 4 questions
Q1. What does `git fetch --prune origin` remove from the local clone?
Q2. After `git fetch --prune origin` removes a remote-tracking ref, any local branch whose upstream configuration referenced that ref will continue to work normally with `git pull`.
Q3. Explain the difference between `git fetch --prune origin` and `git remote prune origin`. When would you use each?
Q4. After a teammate deletes a long-lived feature branch on the remote and an engineer runs `git fetch --prune`, several local branches on the engineer's clone start failing. Diagnose and recommend a fix.
An engineer has been working on `feature/iam-rotation` which tracked `origin/feature/iam-rotation`. A teammate deleted `feature/iam-rotation` on the remote (perhaps after a squash-merge). The engineer runs `git fetch --prune origin` to clean up. Now their `git status` reports `Your branch is based on 'origin/feature/iam-rotation', but the upstream is gone.` and `git pull` fails.
Passing score: 75%. Answers are checked in this browser.