Blog / Coding tips

Git Delete Branch Local and Remote (Tested Commands)

Cover image for Git Delete Branch Local and Remote (Tested Commands)

To delete a Git branch both locally and on the remote, run two commands: git branch -d branch-name deletes your local copy, and git push origin --delete branch-name deletes the branch on the remote (GitHub, GitLab, Bitbucket or your own server). Use -d first: it refuses to delete a branch whose work isn't merged, which protects you from losing commits. If you really want to throw that work away, use -D (capital D) to force it. Deleting a branch doesn't delete any commits that are already merged into another branch, and you can't delete the branch you're currently on, so switch to main first. Your teammates will keep seeing the old remote branch until they run git fetch --prune.

Those are the core commands. The rest of this guide covers what happens around them: the exact error messages and what they mean, deleting a remote branch while keeping your local one, cleaning up stale origin/… branches, deleting every merged branch in one go, and getting a branch back after you've deleted it. Every command was run on 6 October 2026 with Git 2.47.3 in throwaway repositories, using a bare repository on disk as the "remote", so the output below is real. I fixed the author name and dates so the runs are repeatable; your commit hashes will be different.

Delete a local branch: git branch -d vs -D

The test repository has main, a branch called feature/navbar that has been merged into main, and a branch called experiment/dark-mode with a commit that has not been merged. Here's what happens when I try to delete each one, then try to delete the branch I'm standing on:

. "$(dirname "$0")/common.sh"; fresh local
# a branch that has been merged into main
git switch -q -c feature/navbar; commit nav.html '<nav>Menu</nav>' "Add navbar"
git switch -q main; git merge -q --no-edit feature/navbar
# a branch with work that is NOT in main
git switch -q -c experiment/dark-mode; commit dark.css 'body{background:#111}' "Try dark mode"
git switch -q main
run "git branch"
run "git branch -d feature/navbar"
run "git branch -d experiment/dark-mode"
run "git branch -D experiment/dark-mode"
git switch -q -c hotfix
run "git branch -d hotfix"
run "git switch main"
run "git branch -d hotfix"
run "git branch"
$ git branch
  experiment/dark-mode
  feature/navbar
* main

$ git branch -d feature/navbar
Deleted branch feature/navbar (was 9a02e84).

$ git branch -d experiment/dark-mode
error: the branch 'experiment/dark-mode' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D experiment/dark-mode'
hint: Disable this message with "git config advice.forceDeleteBranch false"
(exit code 1)

$ git branch -D experiment/dark-mode
Deleted branch experiment/dark-mode (was 8e7a8fa).

$ git branch -d hotfix
error: cannot delete branch 'hotfix' used by worktree at '/tmp/gd-local/me'
(exit code 1)

$ git switch main
Switched to branch 'main'
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

$ git branch -d hotfix
Deleted branch hotfix (was 9a02e84).

$ git branch
* main

Reading that from the top:

  1. git branch -d feature/navbar worked straight away, because every commit on it is already part of main. Git prints Deleted branch feature/navbar (was 9a02e84). Keep an eye on that short hash, because it's your undo button (see the last section).
  2. git branch -d experiment/dark-mode was refused with error: the branch 'experiment/dark-mode' is not fully merged and exit code 1. This is -d doing its job: the "Try dark mode" commit exists only on that branch, so deleting it would leave that commit unreachable.
  3. git branch -D experiment/dark-mode deleted it anyway. -D is short for --delete --force. Only use it when you've checked the branch and are sure you don't want its commits, or when the work was merged some other way that Git can't detect, such as a squash merge on GitHub.
  4. Deleting the branch you're on fails. In Git 2.47 the message is cannot delete branch 'hotfix' used by worktree at '/tmp/gd-local/me'. Older versions say Cannot delete branch 'hotfix' checked out at …. Same meaning: switch to another branch with git switch main (or git checkout main) first, and then the delete works.

What does "not fully merged" actually check?

-d checks whether the branch's last commit can be reached from its upstream branch (for example origin/feature/navbar), or from your current branch if it has no upstream. That's why you'll sometimes see this warning for a branch that was merged on GitHub: if the pull request was squash-merged or rebased, the commits on main are new commits with new hashes, so Git doesn't recognise your branch's commits as merged. In that case, compare the branch with main using git log main..branch-name or look at the pull request. If you're happy the work landed, -D is safe.

Delete a remote branch with git push --delete

Deleting a branch locally doesn't touch the remote, and deleting it on the remote doesn't touch your local copy. They're separate operations. Here a feature branch has been pushed, merged and pushed again, and now I delete it everywhere:

. "$(dirname "$0")/common.sh"; fresh remote
git switch -q -c feature/contact-form; commit contact.html '<form></form>' "Add contact form"
git push -q -u origin feature/contact-form 2>/dev/null
git switch -q main; git merge -q --no-edit feature/contact-form; git push -q 2>/dev/null
run "git branch -a"
run "git push origin --delete feature/contact-form"
run "git branch -a"
run "git branch -d feature/contact-form"
run "git branch -a"
run "git push origin --delete feature/contact-form"
$ git branch -a
  feature/contact-form
* main
  remotes/origin/feature/contact-form
  remotes/origin/main

$ git push origin --delete feature/contact-form
To /tmp/gd-remote/origin.git
 - [deleted]         feature/contact-form

$ git branch -a
  feature/contact-form
* main
  remotes/origin/main

$ git branch -d feature/contact-form
Deleted branch feature/contact-form (was 091e574).

$ git branch -a
* main
  remotes/origin/main

$ git push origin --delete feature/contact-form
error: unable to delete 'feature/contact-form': remote ref does not exist
error: failed to push some refs to '/tmp/gd-remote/origin.git'
(exit code 1)
  • git push origin --delete feature/contact-form tells the remote called origin to remove that branch. Git confirms with - [deleted] feature/contact-form. You can shorten --delete to -d, as in git push origin -d feature/contact-form.
  • After that, git branch -a no longer lists remotes/origin/feature/contact-form, because the push also removed my local remote-tracking copy of it. But the local branch feature/contact-form is still there.
  • git branch -d feature/contact-form removes the local branch, and the repo is clean.
  • Deleting it again gives error: unable to delete 'feature/contact-form': remote ref does not exist. You'll see this when someone has already deleted the branch (often GitHub, after a merge), or when you've misspelled the name. It's harmless. Run git fetch --prune to bring your list of remote branches up to date.

Unlike git branch -d, remote deletion has no merge check. If you have permission to push, the remote branch is deleted whether or not its work was merged. On GitHub and GitLab, admins can protect important branches so this is refused. If your repo has a main or production branch that deploys a live site, it's worth turning branch protection on.

Delete a remote branch but keep the local one

Sometimes you want the branch gone from GitHub but want to keep working on it locally, for example an experiment you pushed by mistake. Just run the push command and don't delete the local branch:

. "$(dirname "$0")/common.sh"; fresh keeplocal
git switch -q -c feature/search; commit search.js 'search()' "Add search"
git push -q -u origin feature/search 2>/dev/null
run "git push origin --delete feature/search"
run "git branch -vv"
run "git branch --unset-upstream feature/search"
run "git branch -vv"
# old colon syntax: push "nothing" to the remote branch
git push -q -u origin feature/search 2>/dev/null
run "git push origin :feature/search"
# mistake: prefixing the remote name
git push -q -u origin feature/search 2>/dev/null
run "git push origin --delete origin/feature/search"
$ git push origin --delete feature/search
To /tmp/gd-keeplocal/origin.git
 - [deleted]         feature/search

$ git branch -vv
* feature/search 215495e [origin/feature/search: gone] Add search
  main           2a71a81 [origin/main] First commit

$ git branch --unset-upstream feature/search

$ git branch -vv
* feature/search 215495e Add search
  main           2a71a81 [origin/main] First commit

$ git push origin :feature/search
To /tmp/gd-keeplocal/origin.git
 - [deleted]         feature/search

$ git push origin --delete origin/feature/search
error: unable to delete 'origin/feature/search': remote ref does not exist
error: failed to push some refs to '/tmp/gd-keeplocal/origin.git'
(exit code 1)

After the remote delete, git branch -vv shows [origin/feature/search: gone]. The local branch still exists, but its upstream no longer does, so git status will say the upstream is gone and a plain git pull will fail. git branch --unset-upstream feature/search removes the stale link, and the branch is now purely local. When you want to publish it again, git push -u origin feature/search recreates it on the remote.

The last command shows the old colon syntax, git push origin :feature/search. It means "push nothing to feature/search", which deletes it. It does the same as --delete, and you'll still see it in older answers online, but --delete is much easier to read. The final command is a mistake I see a lot: adding the remote name to the branch name. origin/feature/search isn't a branch on the remote, so Git says remote ref does not exist, even though feature/search is right there.

Clean up stale remote branches with git fetch --prune

When you delete a branch on the remote, everyone else's clone still has a remote-tracking branch for it (origin/feature/footer), and it stays there until they prune. A plain git fetch does not remove it. Here's the teammate's view after I deleted feature/footer from my computer:

. "$(dirname "$0")/common.sh"; fresh prune
git switch -q -c feature/footer; commit footer.html '<footer></footer>' "Add footer"
git push -q -u origin feature/footer 2>/dev/null; git switch -q main
# a teammate clones the repo while the branch still exists
cd .. && git clone -q origin.git teammate 2>/dev/null && cd me
git push -q origin --delete feature/footer 2>/dev/null
cd ../teammate
echo "# --- on the teammate's computer ---"
run "git branch -a"
run "git fetch"
run "git branch -a"
run "git remote prune origin --dry-run"
run "git fetch --prune"
run "git branch -a"
run "git config fetch.prune true"
run "git config --get fetch.prune"
# --- on the teammate's computer ---
$ git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/feature/footer
  remotes/origin/main

$ git fetch

$ git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/feature/footer
  remotes/origin/main

$ git remote prune origin --dry-run
Pruning origin
URL: /tmp/gd-prune/origin.git
 * [would prune] origin/feature/footer

$ git fetch --prune
From /tmp/gd-prune/origin
 - [deleted]         (none)     -> origin/feature/footer

$ git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main

$ git config fetch.prune true

$ git config --get fetch.prune
true
  • After a normal git fetch, remotes/origin/feature/footer is still listed, even though it no longer exists on the server. This is why people say "I deleted it but it's still showing".
  • git remote prune origin --dry-run shows what would be cleaned up without changing anything: * [would prune] origin/feature/footer.
  • git fetch --prune fetches and removes remote-tracking branches that no longer exist on the remote. The output line - [deleted] (none) -> origin/feature/footer confirms it.
  • git config fetch.prune true makes every future git fetch and git pull prune automatically in this repo. Add --global to turn it on for all your repos. I'd recommend it: there's no downside unless you rely on seeing deleted branches, which almost nobody does.

Pruning only removes remote-tracking branches (origin/…). It never deletes your own local branches, even ones whose upstream has gone. That's the next section.

Delete local branches after merge, or ones not on the remote

After a few weeks of pull requests, git branch fills up with old feature branches. There are two useful ways to find the ones you can delete. In this test repo, fix/typo and feature/about were merged and deleted on the remote, feature/gallery is pushed but unmerged, and wip/unpushed was never pushed:

. "$(dirname "$0")/common.sh"; fresh bulk
for b in fix/typo feature/about feature/gallery; do
  git switch -q -c "$b"; commit "${b#*/}.txt" "$b" "Work on $b"; git push -q -u origin "$b" 2>/dev/null; git switch -q main
done
git merge -q --no-edit fix/typo feature/about >/dev/null; git push -q 2>/dev/null
git switch -q -c wip/unpushed; commit wip.txt wip "WIP"; git switch -q main
# someone deletes the merged branches on GitHub after the pull requests
git push -q origin --delete fix/typo feature/about 2>/dev/null
run "git branch --merged main"
run "git branch --no-merged main"
run "git branch --merged main | grep -vE '^\*|^\s*(main|master|develop)\$'"
run "git fetch --prune"
run "git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads"
run "git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | awk '\$2 == \"[gone]\" {print \$1}'"
run "git branch --merged main | grep -vE '^\*|^\s*(main|master|develop)\$' | xargs git branch -d"
run "git branch"
$ git branch --merged main
  feature/about
  fix/typo
* main

$ git branch --no-merged main
  feature/gallery
  wip/unpushed

$ git branch --merged main | grep -vE '^\*|^\s*(main|master|develop)$'
  feature/about
  fix/typo

$ git fetch --prune

$ git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads
feature/about [gone]
feature/gallery 
fix/typo [gone]
main 
wip/unpushed 

$ git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | awk '$2 == "[gone]" {print $1}'
feature/about
fix/typo

$ git branch --merged main | grep -vE '^\*|^\s*(main|master|develop)$' | xargs git branch -d
Deleted branch feature/about (was 927dcfd).
Deleted branch fix/typo (was 22f54b0).

$ git branch
  feature/gallery
* main
  wip/unpushed

Option 1: branches merged into main. git branch --merged main lists every local branch whose commits are all in main, including main itself. The grep -vE filter removes the current branch (the line starting with *) and any long-lived branch names you never want to delete. Look at the filtered list first. When it's right, pipe it into xargs git branch -d. Because it uses lower-case -d, Git will still refuse anything that isn't merged, so this is a safe clean-up. git branch --no-merged main shows the branches it will leave alone, and why.

Option 2: branches whose remote has gone. After git fetch --prune, git for-each-ref with %(upstream:track) prints [gone] next to every local branch whose upstream was deleted. These are usually branches whose pull requests were merged and auto-deleted on GitHub. The awk filter prints just their names. I deliberately didn't pipe that list into a delete command, because "gone" doesn't guarantee "merged": a branch could have been deleted on the remote by mistake. Check the list, then delete with -d, and only use -D for squash-merged branches you've checked.

Notice that wip/unpushed appears in neither list. It isn't merged, and it never had an upstream, so it can't be "gone". Bulk clean-ups like these are safe for work you haven't shared yet.

The new git branch --delete-merged (Git 2.56)

Git 2.56, released on 28 September 2026, adds a built-in option for Option 2: git branch --delete-merged 'origin/*' --dry-run lists local branches whose upstream is on origin and whose work is already reachable from that upstream, and running it without --dry-run deletes them. It skips checked-out branches and branches with missing upstreams, and you can protect a branch with branch.<name>.deleteMerged = false. The details are in the official git branch documentation. I couldn't test it for this post, because the machine I used has Git 2.47, so I've stuck to the commands above, which work on any recent version. If you have 2.56 or later, run git --version to check and try the --dry-run form first.

How to recover a deleted Git branch

Deleting a branch only deletes the name. The commits stay in the repository for a while (by default, unreachable commits are kept for at least two weeks before garbage collection can remove them), so a deleted branch is usually easy to get back:

. "$(dirname "$0")/common.sh"; fresh recover
git switch -q -c feature/quiz; commit quiz.js 'quiz()' "Add quiz page"
GIT_COMMITTER_DATE="2026-10-06T10:05:00+06:00" GIT_AUTHOR_DATE="2026-10-06T10:05:00+06:00" commit quiz.css '.quiz{}' "Style quiz page"
git push -q -u origin feature/quiz 2>/dev/null
git switch -q main
run "git branch -D feature/quiz"
run "git push origin --delete feature/quiz"
run "git reflog -n 4"
SHA=$(git rev-parse --short HEAD@{1})
run "git branch feature/quiz $SHA"
run "git log --oneline main..feature/quiz"
run "git push -u origin feature/quiz"
$ git branch -D feature/quiz
Deleted branch feature/quiz (was b96f46f).

$ git push origin --delete feature/quiz
To /tmp/gd-recover/origin.git
 - [deleted]         feature/quiz

$ git reflog -n 4
2a71a81 HEAD@{0}: checkout: moving from feature/quiz to main
b96f46f HEAD@{1}: commit: Style quiz page
c845fb4 HEAD@{2}: commit: Add quiz page
2a71a81 HEAD@{3}: checkout: moving from main to feature/quiz

$ git branch feature/quiz b96f46f

$ git log --oneline main..feature/quiz
b96f46f Style quiz page
c845fb4 Add quiz page

$ git push -u origin feature/quiz
To /tmp/gd-recover/origin.git
 * [new branch]      feature/quiz -> feature/quiz
branch 'feature/quiz' set up to track 'origin/feature/quiz'.
  1. When you delete a branch, Git prints Deleted branch feature/quiz (was b96f46f). That hash is the branch's last commit. If it's still on your screen, git branch feature/quiz b96f46f brings the branch straight back.
  2. If you've lost the message, run git reflog. It records every place HEAD has been. Here, HEAD@{1}: commit: Style quiz page is the last commit made on the deleted branch. Use its hash.
  3. git log --oneline main..feature/quiz confirms both commits are back.
  4. If you'd also deleted it on the remote, git push -u origin feature/quiz recreates it there.

If someone else deleted the remote branch and you never had it locally, ask anyone who had it checked out to push it again. On GitHub, a branch deleted from a merged or closed pull request can usually be restored with the "Restore branch" button on that pull request (I didn't test this in this run, so check the page for your own repo). If a branch ever contained a secret, like an API key, deleting it isn't enough: the commits may still be reachable through forks, pull requests and caches. Rotate the key straight away, then read how I keep an AI API key off the browser so it doesn't end up in the repo in the first place.

Cheat sheet: every command in this guide

Here's everything above in one list, in the order you'd normally need it. Every one of these was run in the test repositories for this post:

  1. See what you've got: git branch -a lists local and remote-tracking branches, and git branch -vv shows which ones have an upstream that's gone.
  2. Move off the branch first: git switch main.
  3. Delete the local branch safely: git branch -d name. If Git says "not fully merged", check with git log main..name before deciding.
  4. Force-delete a local branch: git branch -D name, only when you're sure.
  5. Delete the remote branch: git push origin --delete name, using the plain branch name with no origin/ in front.
  6. Tidy stale remote-tracking branches: git fetch --prune, or preview it with git remote prune origin --dry-run.
  7. Prune automatically from now on: git config --global fetch.prune true.
  8. Find branches you can clean up: git branch --merged main for merged ones, and git for-each-ref with %(upstream:track) for ones marked [gone].
  9. Undo a delete: git branch name <hash>, using the hash from the delete message or from git reflog, then git push -u origin name if the remote copy went too.

If you only ever remember two of them, make it numbers 3 and 5. Lower-case -d locally plus push --delete on the remote covers the everyday "my pull request is merged, tidy up" job, and the safety check on -d means a typo can't cost you unmerged work.

Common mistakes when deleting Git branches

  1. Deleting only one side. git branch -d doesn't touch the remote, and git push --delete doesn't touch your local branch. If you want it gone everywhere, run both.
  2. Reaching for -D every time. -d is a free safety check. Use it by default and only force when you know why Git is refusing.
  3. Expecting teammates' lists to update by themselves. They need git fetch --prune, or fetch.prune set to true.
  4. Typing origin/branch-name in the push command. git push origin --delete origin/feature looks for a branch literally called origin/feature on the remote and fails with "remote ref does not exist". Use the plain branch name.
  5. Deleting main or a deploy branch. Many hosts deploy from a named branch. If your site builds from main, deleting or force-pushing it can take the site down. Protect it on GitHub. If you use Vercel, as in my Gemini API key on Next.js and Vercel guide, check which branch is set as the production branch before tidying up.
  6. Panicking after a delete. The commits are still there. git reflog and the hash from the delete message will get them back, as long as you act within a couple of weeks.

If you've just tidied your branches and want to fix the last commit on one you kept, my guide to undoing the last commit but keeping the changes uses the same throwaway-repo approach. And if you want a small project to practise branching on, a single-page app like my offline study planner is about the right size: one branch per feature, merge, delete, repeat.

FAQ

How do I delete a Git branch locally and remotely?

Run git push origin --delete branch-name to delete it on the remote, then git branch -d branch-name to delete your local copy. Switch to a different branch first, because Git won't delete the branch you're on. Teammates should run git fetch --prune to stop seeing the old remote branch.

What is the difference between git branch -d and -D?

-d (--delete) only deletes the branch if its commits are merged into its upstream or into your current branch, and otherwise stops with "not fully merged". -D is --delete --force: it deletes the branch whatever its merge status, so any unmerged commits become unreachable.

Why does a deleted remote branch still show in git branch -a?

Your clone keeps a remote-tracking branch, such as origin/feature, until you prune it. A normal git fetch doesn't remove it. Run git fetch --prune or git remote prune origin, or set git config --global fetch.prune true so it happens on every fetch.

How do I delete all local branches that are already merged?

List them with git branch --merged main, filter out main and the current branch, check the list, then pipe it to xargs git branch -d. Using lower-case -d means Git still refuses anything that isn't really merged. Git 2.56 and later also offer git branch --delete-merged with a --dry-run option.

Can I recover a branch after deleting it in Git?

Usually, yes. Use the hash Git printed when you deleted it (Deleted branch name (was abc1234)) or find the last commit in git reflog, then run git branch name abc1234. Push it again with git push -u origin name if it was also deleted on the remote.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

UK Phone Number Regex for JavaScript and Python (Tested)
Coding tips

UK Phone Number Regex for JavaScript and Python (Tested)

Strip spaces, turn +44 into 0, then check for 07 plus nine digits. Tested JavaScript and Python code for every UK number type, E.164 storage, display spacing and libphonenumber.

October 6, 2026 · 19 min read · 0 views

CSS Text Overflow Ellipsis: 1 Line, 2 Lines and Flexbox
Coding tips

CSS Text Overflow Ellipsis: 1 Line, 2 Lines and Flexbox

One-line ellipsis needs white-space: nowrap, overflow: hidden and text-overflow: ellipsis. Tested fixes for flexbox, grid and tables, plus 2- and 3-line truncation with line-clamp.

October 6, 2026 · 15 min read · 0 views

JavaScript Sort Array of Objects by Property (Tested)
Coding tips

JavaScript Sort Array of Objects by Property (Tested)

Pass sort() a compare function: a.price - b.price for numbers, a.name.localeCompare(b.name) for strings. Dates, multiple properties, toSorted() and the mistakes to avoid, all tested.

October 6, 2026 · 20 min read · 0 views