Open your repository and run git branch -a. How many branches do you see? Ten?
Fifty? A hundred? Now ask yourself: how many of those branches represent active
work? The honest answer is probably "I don't know," and that's the problem.
Branch sprawl creeps up on teams gradually. Each merged feature leaves behind a branch. Experiments get abandoned mid-stream. That quick fix from three months ago still has a branch even though it's long since deployed. Over time, your repository fills with archaeological artifacts that make it harder to find the branches that actually matter.
This isn't just an aesthetic problem. Stale branches confuse developers who can't tell what's active, waste time in branch lists and autocomplete, create ambiguity about what's deployed, and occasionally cause real problems when someone accidentally works on an old branch thinking it's current.
The solution is systematic branch lifecycle management—clear policies about when branches get deleted, automated tools to enforce those policies, and team habits that treat branch cleanup as part of normal workflow rather than periodic housekeeping.
Understanding the Branch Lifecycle
Every branch follows a lifecycle from creation to deletion, but many teams never define what that lifecycle should look like. Without clear expectations, branches accumulate indefinitely.
A healthy branch lifecycle looks like this:
- Creation: Branch created from current main for specific work
- Development: Active commits happen, branch stays current with main
- Review: Pull request opened, feedback incorporated
- Integration: Changes merge to main
- Deletion: Branch deleted locally and remotely
- Archival: Branch name remains in history via merge commit
The critical step that teams skip is deletion. After merging, the branch served
its purpose. Keeping it around provides no value—the commits are now part of
main's history, accessible through git log and the merge commit references the
branch name.
Yet developers hesitate to delete branches, worried they're losing work. This concern misunderstands how Git works. The commits aren't deleted, only the branch pointer. Every commit that was merged is now reachable from main, preserved permanently.
When to Delete Branches
The timing of branch deletion depends on branch type and team workflow, but the general principle is simple: delete branches as soon as they've served their purpose.
Immediately After Merging
For feature and bug fix branches, deletion should happen immediately after the pull request merges. There's no reason to keep the branch once the code is in main.
Most teams automate this. GitHub and GitLab offer options to automatically delete branches after merging. Enable this at the repository level:
On GitHub:
- Settings → General → Pull Requests
- Enable "Automatically delete head branches"
This removes the remote branch the moment you click "Merge pull request." The deleted branch doesn't disappear from history—the PR and merge commit still reference it—but it stops cluttering your branch list.
You still need to clean up local branches manually:
# After a PR merges git checkout main git pull git branch -d feature/completed-feature
The -d flag safely deletes branches that have been merged. If you try to
delete an unmerged branch, Git warns you and refuses. This protects against
accidentally deleting work that hasn't been integrated.
After Deployment for Release Branches
If you use release branches (common in Gitflow), delete them after the release deploys successfully and you've merged back to develop or main.
# Release is deployed successfully git checkout main git branch -d release/v2.1.0 git push origin --delete release/v2.1.0
Keep a release tag to mark the deployment:
git tag -a v2.1.0 -m "Release version 2.1.0" git push origin v2.1.0
Tags preserve the exact commit that was deployed without keeping a branch around.
After a Reasonable Period for Stale Experimental Branches
Experimental or research branches are trickier. You might create a branch to test an approach, set it aside, and intend to return to it later. These branches should have an expiration date.
Establish a team policy: experimental branches older than 30 days without activity get flagged for review. If nobody claims them as active work, delete them. Document this policy so developers know to either keep working on experiments or convert promising ones to tracked work items.
# Find branches without activity in the last 30 days git for-each-ref --sort=-committerdate refs/heads/ \ --format='%(committerdate:short) %(refname:short)' | tail -20
Review this list regularly during team meetings or sprint planning.
Identifying Branches Ready for Deletion
Before deleting branches, you need to know which ones are safe to remove. The key question is: which branches have already been merged?
List All Merged Branches
To see which local branches have been fully merged into main:
git checkout main git pull git branch --merged
This shows every local branch whose commits are already in main. In your scenario with 20 local branches where 17 have been merged, this command would list those 17 branches.
The output includes main itself, which you obviously don't want to delete. Filter it out:
git branch --merged | grep -v "^\*\|main\|develop"
This excludes:
- Your current branch (marked with
*) - The main branch
- The develop branch (if you use Gitflow)
Now you see only the branches that are merged and ready for deletion.
List All Unmerged Branches
To see which branches still have work that hasn't been merged:
git branch --no-merged
These are your 3 active branches (in your example). These branches contain commits that aren't in main yet—either they're still in development, blocked in review, or represent abandoned work you might want to keep.
See Merged Status for Remote Branches
The commands above only check local branches. To check remote branches:
# See all remote branches merged to main git branch -r --merged origin/main | grep -v "main\|develop\|HEAD" # See all remote branches not yet merged git branch -r --no-merged origin/main
A More Detailed View
For a clearer picture showing branch names with their last commit dates:
# Merged branches with dates git branch --merged | grep -v "^\*\|main\|develop" | while read branch; do echo "$(git log -1 --format='%ci' $branch) - $branch" done | sort
This shows when each merged branch last had activity, helping you identify which ones have been sitting around longest.
The Complete Analysis
Here's how to answer "which of my 20 branches can be deleted":
# Total branches echo "Total local branches:" git branch | wc -l # Merged (safe to delete) echo -e "\nMerged branches (safe to delete):" git branch --merged | grep -v "^\*\|main\|develop" # Still active echo -e "\nActive branches (still being worked on):" git branch --no-merged
This gives you the complete picture: how many total branches you have, which ones are merged and deletable, and which ones are still active.
Quick Verification Before Deleting
Before deleting a branch, verify it's actually been merged:
# Check if specific branch is merged git branch --merged | grep feature/user-authentication
If it appears in the output, it's safe to delete. If it doesn't appear, the branch still has unmerged commits.
Batch Delete All Merged Branches
Once you've identified your merged branches and confirmed they're safe to delete, you can remove them all at once:
# Delete all merged local branches except main and develop git branch --merged | grep -v "^\*\|main\|develop" | xargs git branch -d
This pipes the list of merged branches into git branch -d, deleting each one.
The -d flag ensures safety—it will refuse to delete any branch that isn't
actually merged.
In your scenario with 17 merged branches out of 20 total, this single command would delete all 17 merged branches, leaving you with just main and your 3 active feature branches.
Deleting Branches Safely
Git provides multiple ways to delete branches, with different safety characteristics.
Local Branch Deletion
To delete a local branch that's been merged:
git branch -d branch-name
The lowercase -d is "safe delete"—it only works if the branch has been merged
into your current branch or its upstream. If you try to delete unmerged work,
Git refuses:
$ git branch -d feature/unfinished error: The branch 'feature/unfinished' is not fully merged. If you are sure you want to delete it, run 'git branch -D feature/unfinished'.
This safety check prevents losing work. If you genuinely want to delete an
unmerged branch—maybe it was an experiment you're abandoning—use the uppercase
-D:
git branch -D feature/abandoned-experiment
This force deletes without checking merge status. Use it consciously, understanding you're discarding commits that might not be reachable from any other branch.
Remote Branch Deletion
After deleting locally, delete the remote branch:
git push origin --delete feature/branch-name
Or using the older syntax:
git push origin :feature/branch-name
Both commands do the same thing—remove the branch from the remote repository. Other developers will stop seeing it when they fetch, though they'll need to prune their local references to fully clean up.
Cleaning Up Remote Tracking References
When you delete a remote branch, your local Git still has a reference to it in
origin/branch-name. These stale references accumulate over time.
Clean them up with:
git fetch --prune
Or set Git to automatically prune when fetching:
git config --global fetch.prune true
Now every git fetch automatically removes references to deleted remote
branches.
The Complete Cleanup Workflow
Here's the full sequence after merging a PR:
# Verify the PR merged git checkout main git pull origin main # Delete local feature branch git branch -d feature/completed-feature # Delete remote feature branch (if not auto-deleted) git push origin --delete feature/completed-feature # Clean up remote references git fetch --prune
Make this a habit after every merge, and your repository stays clean automatically.
Automated Cleanup Strategies
Manual deletion works, but automation ensures consistency without relying on human memory.
GitHub Actions for Stale Branch Cleanup
Create a GitHub Actions workflow that identifies and deletes stale branches automatically:
name: Clean Stale Branches on: schedule: # Run weekly on Sunday at 2 AM - cron: '0 2 * * 0' workflow_dispatch: # Allow manual trigger jobs: cleanup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history - name: Delete stale branches env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Find branches merged to main git checkout main git pull # Get merged branches merged_branches=$(git branch -r --merged main | grep -v 'main\|develop\|HEAD' | sed 's/origin\///') # Delete merged branches older than 7 days for branch in $merged_branches; do last_commit_date=$(git log -1 --format=%ct origin/$branch) days_old=$(( ($(date +%s) - last_commit_date) / 86400 )) if [ $days_old -gt 7 ]; then echo "Deleting stale branch: $branch (${days_old} days old)" git push origin --delete $branch || true fi done
This workflow runs weekly, finds branches that merged to main over a week ago, and deletes them. The seven-day grace period ensures recently merged branches aren't immediately removed, giving team members time to update their local repositories.
GitLab Auto-Delete on Merge
GitLab provides built-in auto-delete functionality at the project level:
Settings → Merge Requests → Enable "Delete source branch" by default
This automatically deletes the source branch when a merge request merges. Individual MRs can override this setting if needed.
Git Hooks for Local Cleanup
Create a post-merge hook that suggests cleaning up merged branches:
#!/bin/bash # .git/hooks/post-merge # Check if we're on main current_branch=$(git symbolic-ref --short HEAD) if [ "$current_branch" = "main" ]; then # Find local branches that are fully merged merged_branches=$(git branch --merged | grep -v '^\*\|main\|develop') if [ -n "$merged_branches" ]; then echo "The following branches are fully merged into main:" echo "$merged_branches" echo "" echo "Consider deleting them with:" echo " git branch -d <branch-name>" fi fi
This hook runs after every merge to main, reminds developers about merged branches, and suggests cleanup commands. It's non-intrusive—just a helpful reminder rather than automatic deletion.
Using Git Aliases for Quick Cleanup
Create Git aliases that make cleanup easier:
# Add to ~/.gitconfig [alias] # Delete all local branches that have been merged to main cleanup = "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d" # List branches sorted by last commit date recent = "!git for-each-ref --sort=-committerdate refs/heads/ --format='%(committerdate:short) %(refname:short) %(subject)'" # Prune remote references and show what was removed prune-all = "!git fetch --prune && git remote prune origin"
Now you can run:
git cleanup # Delete all merged local branches git recent # See which branches are active git prune-all # Clean up remote references
These aliases turn multi-step cleanup into single commands, removing friction from regular maintenance.
Handling Special Cases
Some branches require careful handling before deletion.
Unmerged Work You Want to Preserve
If you've abandoned a branch but want to preserve the work for future reference, create a tag before deleting:
# Tag the abandoned work git tag archive/experiment-caching feature/experiment-caching # Push the tag git push origin archive/experiment-caching # Now safely delete the branch git branch -D feature/experiment-caching git push origin --delete feature/experiment-caching
The tag preserves the commits, and the archive/ prefix makes it clear this
isn't an active branch or release.
Long-Running Personal Branches
Some developers maintain long-running personal branches for their ongoing work, merging pieces to main via PRs but keeping the branch as a workspace. These branches should be clearly named to distinguish them from feature branches:
# Personal workspace branches jsmith/workspace alice/research # Not to be confused with feature branches feature/add-authentication
Exclude these from cleanup automation using naming patterns.
Branches in Open Pull Requests
Never delete branches with open pull requests—this breaks the PR and makes review impossible. Automation should check for open PRs before deleting:
# Check if branch has open PR before deleting gh pr list --head feature/branch-name --json number --jq length # If result is 0, safe to delete
Most hosted Git platforms (GitHub, GitLab) prevent deleting branches with open merge requests, but local scripts need this protection.
Monitoring Branch Health
Proactive monitoring catches branch sprawl before it becomes a problem.
Regular Branch Audits
Schedule monthly audits to review all open branches:
# Generate branch report git for-each-ref --sort=-committerdate refs/heads/ \ --format='%(committerdate:short) %(authorname) %(refname:short) %(subject)' \ > branch-report.txt
Review this report in team meetings. For each branch older than your defined threshold (e.g., two weeks), ask:
- Is this still active work?
- If not, why hasn't it merged?
- Should it become an epic/project with multiple smaller branches?
- Can we archive or delete it?
This regular attention prevents branches from accumulating indefinitely.
Dashboard Metrics
Track branch metrics over time:
- Total number of branches
- Number of branches older than 30 days
- Average branch age
- Number of stale branches cleaned up per week
When these metrics trend upward, address the root cause. Are features taking too long? Are developers creating too many experimental branches? Is your team forgetting to delete after merging?
Alerting for Stale Branches
Set up alerts when branches exceed age thresholds. A simple approach uses GitHub Actions:
name: Stale Branch Alert on: schedule: - cron: '0 9 * * 1' # Monday mornings jobs: alert: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Find stale branches run: | stale_branches=$(git for-each-ref --sort=-committerdate refs/remotes/ \ --format='%(committerdate:short) %(refname:short)' | \ awk '$1 < "'$(date -d '30 days ago' +%Y-%m-%d)'"') if [ -n "$stale_branches" ]; then echo "Stale branches found:" echo "$stale_branches" # Send to team chat or create issue fi
Connect this to your team's chat system (Slack, Discord, Teams) to notify when stale branches need attention.
For a deeper dive into tracking and visualizing branch activity, see our guide on Git branch visualization and monitoring tools, which covers both command-line and GUI options for keeping tabs on your repository's branch health.
Building a Cleanup Culture
Technical solutions help, but lasting change requires cultural shifts in how teams think about branches.
Normalize Immediate Deletion
Make branch deletion part of your definition of "done." A PR isn't fully complete until the branch is deleted. Include this in your PR checklist:
## Merge Checklist - [ ] All tests pass - [ ] Code reviewed and approved - [ ] Documentation updated - [ ] Branch deleted after merge
This small reminder builds the habit of cleanup immediately after merging. For a more comprehensive checklist covering the entire feature branch lifecycle from creation to deletion, check out our feature branch workflow checklist.
Celebrate Clean Repositories
In team retrospectives or demos, occasionally highlight repository cleanliness. When someone notices the repository looks clean and well-maintained, acknowledge the team's good habits. This positive reinforcement makes cleanup feel like a shared responsibility rather than a chore.
Address Root Causes
If branches aren't getting deleted, ask why. Common reasons include:
Fear of losing work: Education about how Git preserves merged commits helps. Show developers how to find old commits and prove that deletion doesn't mean data loss.
Uncertainty about merge status: Improve visibility into what's deployed. If developers can't easily tell whether their feature is in production, they'll keep branches "just in case."
Lack of automation: Manual cleanup creates friction. Invest in automation that makes deletion easy or automatic.
Long-running features: Large features that take weeks create long-lived branches. Break features into smaller, independently mergeable pieces to keep branches short-lived.
The Connection to Better Development Velocity
Branch sprawl slows teams down in subtle ways. Developers waste seconds every time they scan a long branch list. They waste minutes when they accidentally work on the wrong branch because they couldn't find the right one. They waste hours debugging issues caused by working on stale branches that are far behind main.
Clean repositories make development faster. When your branch list contains only active work, finding the right branch is instant. When stale branches are automatically cleaned up, nobody accidentally works on outdated code. When branches delete after merging, the mental model stays simple: main is current, feature branches are in-progress work.
At Pull Panda, we think about code review in the context of the entire development workflow. Clean branch management contributes to effective code review by ensuring reviewers see current, relevant work rather than wading through stale PRs and abandoned branches. When your repository is well-maintained, everyone—developers, reviewers, and maintainers—works more efficiently.
For more on building effective feature branch workflows, see our complete guide to mastering feature branches. And to learn about naming conventions that help identify branches for cleanup, check out our article on Git branch naming best practices.

