Delete a Git branch, locally and remotely
To delete a git branch you need two commands, because local and remote are separate. And everyone else keeps a dead copy until they prune.
Deleting a branch takes two commands, because a local branch and a remote branch are two different things:
git branch -d feature # delete it locally
git push origin --delete feature # delete it on the remote
Running only the first leaves the branch on the server. Running only the second leaves it on your machine. Neither one tells you about the other.
There is also a third piece that almost nothing mentions: everyone else's clone keeps a copy of the dead branch until they prune it. All output here came from real repositories on git 2.50.0.
What you are actually deleting
Everything about branch deletion makes more sense once you know what a branch is, and it is much less than most people assume.
A branch is a file containing a commit hash. That is the whole thing:
cat .git/refs/heads/feature
1c7d603920a8553b2e7392b37487bbd9bf45b7fd
Forty-one bytes: forty hexadecimal characters and a newline. There is no copy of your files, no list of commits, no history. Just a pointer at one commit, and that commit points at its parent, and so on backwards.
HEAD is a file too, holding which branch you are on:
cat .git/HEAD
ref: refs/heads/feature
So deleting a branch deletes a pointer. Confirmed directly: after git branch -D feature, the commit it pointed at is still in the repository and still readable.
git cat-file -t 1c7d603 # commit
git log -1 --pretty=%s 1c7d603 # second
The work is not gone. It has simply become unreachable, because nothing points at it any more, and unreachable objects are what -d is protecting you from creating by accident.
-d and -D are not the same
git branch -d feature
error: the branch 'feature' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feature'
-d is the safe version. It refuses when the branch contains commits that are not reachable from anywhere else, because deleting it would leave that work with nothing pointing at it.
-D is the same command with the safety off:
git branch -D feature
Deleted branch feature (was fd5a9f3).
Note what it prints. (was fd5a9f3) is the commit the branch pointed at, and it is the thing you need if you want the branch back. Copy that hash before you close the terminal.
On a branch that has been merged, -d succeeds without complaint, because the commits are reachable from wherever you merged them into. So the rule is simple: if -d works, the work is safe somewhere. If it refuses, stop and check before reaching for -D.
You cannot delete the branch you are on
git switch feature
git branch -d feature
error: cannot delete branch 'feature' used by worktree at '/path/to/repo'
Git will not remove the branch you are standing on, because HEAD would have nothing to point at. Switch away first:
git switch main
git branch -d feature
This is the most common reason the command "does not work", and the message says so plainly once you know that a worktree is just your working copy.
Deleting the remote branch is a push
The command reads oddly the first time:
git push origin --delete feature
To /path/to/remote
- [deleted] feature
You are pushing a deletion. There is no git delete-remote-branch, because from git's point of view you are telling the remote what its branches should look like, and one of them should no longer exist.
The older syntax does the same thing and explains the idea better:
git push origin :feature
That reads as "push nothing into the branch called feature", which is what a deletion is. The --delete form is clearer to read and is what you should write.
The stale copy everyone else keeps
This is the part that causes confusion on teams, and it is worth seeing measured.
A teammate clones while the branch exists:
origin/HEAD -> origin/main
origin/main
origin/topic
You delete topic from the remote. Your teammate runs a plain fetch:
git fetch
origin/HEAD -> origin/main
origin/main
origin/topic
Still there. A plain git fetch adds and updates remote-tracking branches, and does not remove ones that have disappeared. Their git branch -a will keep listing a branch that no longer exists on the server, potentially for months.
The fix is to prune:
git fetch --prune
origin/HEAD -> origin/main
origin/main
Now it is gone. This is why long-lived clones accumulate dozens of dead origin/* entries, and why a branch you deleted last week still turns up in a colleague's autocomplete.
Make it automatic once and forget about it:
git config --global fetch.prune true
Every fetch and pull will then clean up as it goes. There is no real downside: pruning only removes local references to branches the remote has already deleted, never any of your own work.
Cleaning up in bulk
After a few months, most of your local branches are merged and dead. To see which:
git branch --merged main
Everything listed there is already contained in main, so deleting it loses nothing. To delete them all except main itself:
git branch --merged main | grep -v "main" | xargs git branch -d
Run the --merged command on its own first and read the list. xargs will happily act on whatever it is given, and this is a command worth understanding before you run it rather than after.
Note that a branch merged with a squash merge will not appear in --merged, because squashing produces a new commit rather than bringing the originals across. That is covered in merge versus rebase, and it is why some obviously-finished branches still need -D.
Getting a deleted branch back
Deleting a branch does not delete its commits straight away. It removes the label, and the commits stay in the repository until git garbage-collects unreachable objects, which is typically weeks later.
If you have the hash from the (was fd5a9f3) message:
git branch feature fd5a9f3
If you do not, the reflog has it:
git reflog
That lists everywhere HEAD has been, including the commits you made on the branch. Find the last one and create a branch pointing at it. The same technique recovers work from a detached HEAD, and it is the closest thing git has to a universal undo.
The one case where this fails is a branch you never checked out on this machine, since your reflog has no record of it. There, the copy on the remote or on a teammate's machine is the only route back, which is a reason not to rush the remote deletion.
When the remote refuses
Two rejections come up on real projects, and neither is a git problem.
A protected branch. GitHub, GitLab and Bitbucket can mark branches as protected, and the remote will refuse the deletion regardless of your permissions locally. The message comes from the host rather than from git, and the fix is in the repository settings, not in your terminal.
The default branch. A remote cannot delete the branch its HEAD points at, which is normally main. If you genuinely need to remove it, change the default branch in the host's settings first.
Both are worth recognising, because the failure looks like a permissions problem and is usually a policy that exists on purpose.
A safe order of operations
When you are finished with a branch, this sequence avoids every trap above:
- Merge or confirm it is merged.
git branch --merged mainshould list it. - Switch away.
git switch main, since you cannot delete what you are standing on. - Delete locally with
-d. If it refuses, the work is not safely stored anywhere else, so investigate rather than forcing. - Note the hash from the
(was ...)output. - Delete the remote.
git push origin --delete feature. - Tell anyone with a clone to fetch with
--prune, or have them turn it on permanently.
Step 3 is the important one. Reaching for -D because -d complained is the single way this operation loses work, and the complaint is the only warning you get.
Finding what is safe to remove
Two commands make the cleanup decision for you rather than leaving it to memory.
git branch -vv
This lists every local branch with its upstream and its state. A branch whose upstream shows [origin/thing: gone] is one whose remote counterpart has been deleted, which usually means it was merged and cleaned up on the server. Those are the safe candidates.
git branch --sort=-committerdate -v
Sorted by most recent commit, so the branches you have not touched in months sink to the bottom. Between the two, a monthly tidy takes about a minute and stops the branch list becoming something you scroll past.
Quick reference
git branch -d feature # safe local delete
git branch -D feature # force, even if unmerged
git push origin --delete feature # delete on the remote
git fetch --prune # drop dead remote-tracking refs
git branch --merged main # what is safe to delete
git branch feature <hash> # bring one back
git config --global fetch.prune true # prune automatically
Want to create and delete branches somewhere mistakes cost nothing? Work through Git basics in a real repository in the browser.
More from the blog
Objects are not valid as a React child: what to render
Objects are not valid as a React child, and React tells you which one by listing its keys. In React 19 the promise version throws a different error.
Read morenpm install errors: read the code, not the stack
Every npm ERR code names one cause. ERESOLVE, E404, EJSONPARSE and the rest, with what each one means, measured against real installs.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.