ClearDisk -- Free macOS Developer Cache Cleaner

Git Repository Cleanup on Mac — Reclaim 2-20GB+ from .git Directories

Updated March 2025 -- Covers git gc, git prune, Git LFS, shallow clones, git maintenance, and bulk repo cleanup -- 11 min read

Every Git repository on your Mac contains a hidden .git directory that stores the entire project history — every commit, every branch, every version of every file. For active developers with dozens of cloned repositories, these .git directories can collectively consume 2-20GB+ of disk space, often more than the actual project files themselves.

This guide covers every aspect of Git disk usage on macOS: how to audit it, how to safely reclaim space, and how to prevent bloat from accumulating.

Table of Contents 1. Quick Audit — Find Your Largest .git Directories 2. git gc — The Built-In Garbage Collector 3. Reflog Cleanup — Expire Old References 4. Pruning Unreachable Objects 5. Git LFS — Large File Storage Cleanup 6. Stale Branches & Remote Tracking 7. Git Worktrees — Orphaned Checkouts 8. Shallow Clones — Reduce History on Disk 9. git maintenance — Automated Optimization 10. Bulk Cleanup — All Repos at Once 11. Nuclear Options — When You Need Maximum Space 12. Git Cleanup Cheatsheet 13. FAQ

1. Quick Audit — Find Your Largest .git Directories

Before you clean anything, find out where your disk space is actually going:

#!/bin/bash
echo "=== Git Repository Disk Usage Audit ==="
echo ""

# Find all .git directories under common project locations
echo "Scanning for Git repositories..."
echo ""

for base_dir in ~/Developer ~/Projects ~/repos ~/code ~/workspace ~/Documents/GitHub ~/src; do
    if [[ -d "$base_dir" ]]; then
        echo "--- $base_dir ---"
        find "$base_dir" -name ".git" -type d -maxdepth 4 2>/dev/null | while read gitdir; do
            repo_dir=$(dirname "$gitdir")
            git_size=$(du -sk "$gitdir" 2>/dev/null | awk '{print $1}')
            repo_name=$(basename "$repo_dir")
            echo "  ${git_size}K  .git  ${repo_name}"
        done | sort -hr | head -10
        echo ""
    fi
done

echo "--- Global Git data ---"
du -sh ~/.gitconfig 2>/dev/null
du -sh ~/Library/Caches/git 2>/dev/null || echo "  No global Git cache found"

# Git LFS cache
du -sh ~/.cache/lfs 2>/dev/null || echo "  No LFS cache found"

echo ""
echo "=== Top 15 Largest .git Directories ==="
find ~ -name ".git" -type d -maxdepth 6 -not -path "*/node_modules/*" -not -path "*/.cache/*" 2>/dev/null | while read gitdir; do
    du -sk "$gitdir" 2>/dev/null
done | sort -hr | head -15 | while read size dir; do
    repo=$(dirname "$dir" | sed "s|$HOME|~|")
    echo "  $((size/1024))MB  $repo"
done
Quick one-liner: To find git repos using more than 100MB for their .git directory:
find ~/Developer -name ".git" -type d -maxdepth 4 -exec du -sk {} \; 2>/dev/null | awk '$1 > 102400 {printf "%dMB %s\n", $1/1024, $2}' | sort -hr

2. git gc — The Built-In Garbage Collector

Git's garbage collector (git gc) is the primary tool for reclaiming space. It compresses file revisions, removes unreachable objects, and packs loose objects into efficient pack files.

2.1 Basic Garbage Collection

# Run garbage collection (safe, fast)
cd /path/to/repo
git gc

# Check repo size before and after
du -sh .git

2.2 Aggressive Garbage Collection Saves 10-50%

# More thorough compression (slower, better results)
git gc --aggressive --prune=now

# This rewrites pack files with better delta compression
# Can take several minutes on large repos
When to use --aggressive: Only use --aggressive occasionally (e.g., after importing a repo or removing large files from history). It's CPU-intensive and the regular git gc is sufficient for routine cleanup.

2.3 Understanding What git gc Does

3. Reflog Cleanup — Expire Old References

Git keeps a reflog — a history of where your branch tips have been — for safety. This prevents accidental loss but consumes space:

# See reflog size
git reflog | wc -l

# Show reflog entries with dates
git reflog --date=relative | head -20

# Expire reflog entries older than 30 days
git reflog expire --expire=30.days --all

# Expire immediately (aggressive — only if you're sure)
git reflog expire --expire=now --all

# Always run gc after expiring reflog
git gc --prune=now
Tip: The reflog is your safety net for recovering from git reset --hard, accidental branch deletions, and bad rebases. Don't expire it too aggressively on repos where you're actively working.

4. Pruning Unreachable Objects

After rebasing, amending commits, or force-pushing, Git leaves behind "dangling" objects that are no longer reachable from any ref:

# Count dangling objects
git fsck --unreachable --no-reflogs 2>/dev/null | wc -l

# Show dangling objects
git fsck --dangling 2>/dev/null | head -20

# Prune all unreachable objects
git prune --expire=now

# Combined approach: expire reflog + prune + gc
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Check Pack File Sizes

# List pack files (these contain your compressed history)
ls -lh .git/objects/pack/

# Count pack files (ideally just 1-2)
ls .git/objects/pack/*.pack 2>/dev/null | wc -l

# Repack into a single pack file
git repack -a -d -f --depth=250 --window=250

5. Git LFS — Large File Storage Cleanup

If your repository uses Git LFS for large binary files (images, videos, models, datasets), the LFS cache can grow substantially:

5.1 Audit LFS Usage

# Check if LFS is used in this repo
git lfs ls-files

# Check LFS disk usage
git lfs env | grep -i storage
du -sh .git/lfs

# Check the global LFS cache
du -sh ~/.cache/lfs 2>/dev/null

# List LFS objects by size
git lfs ls-files -s | sort -k3 -hr | head -20

5.2 Clean LFS Cache

# Remove old and unused LFS objects
git lfs prune

# More aggressive: remove all local LFS objects not on current HEAD
git lfs prune --verify-remote --verify-unreachable

# Clean the global LFS cache
rm -rf ~/.cache/lfs/*

# Dry run first to see what would be removed
git lfs prune --dry-run

5.3 Deduplication Check

# If you have multiple clones of the same repo, LFS objects may be duplicated
# Check for duplicate LFS objects across repos
find ~/Developer -name "lfs" -path "*/.git/lfs" -type d 2>/dev/null -exec du -sh {} \; | sort -hr

6. Stale Branches & Remote Tracking

Over time, repos accumulate remote tracking branches for branches that have been merged or deleted on the remote:

# List all branches (local and remote)
git branch -a | wc -l

# List remote tracking branches that no longer exist on the remote
git remote prune origin --dry-run

# Actually prune them
git remote prune origin

# Or fetch with prune (recommended — do this regularly)
git fetch --prune

# Set auto-pruning globally
git config --global fetch.prune true

# Delete local branches that have been merged into main
git branch --merged main | grep -v "main" | xargs git branch -d

# Find and display local branches with no upstream
git branch -vv | grep ': gone]'

# Delete them
git branch -vv | grep ': gone]' | awk '{print $1}' | xargs git branch -d
Set this once: Running git config --global fetch.prune true ensures stale remote branches are automatically cleaned up every time you git fetch.

7. Git Worktrees — Orphaned Checkouts

Git worktrees let you check out multiple branches simultaneously, but forgotten worktrees waste disk space:

# List all worktrees for a repository
git worktree list

# Check for orphaned worktrees (paths that no longer exist)
git worktree list --porcelain | grep "worktree" | while read _ path; do
    if [[ ! -d "$path" ]]; then
        echo "ORPHANED: $path"
    fi
done

# Remove orphaned worktrees
git worktree prune

# Verbose mode to see what's being pruned
git worktree prune --verbose

8. Shallow Clones — Reduce History on Disk

For repositories you only need for reference (not active development), shallow clones dramatically reduce disk usage:

# Clone with only the last 1 commit (saves 50-95% space)
git clone --depth 1 https://github.com/user/repo.git

# Clone with last 10 commits
git clone --depth 10 https://github.com/user/repo.git

# Clone only a single branch
git clone --single-branch --branch main https://github.com/user/repo.git

# Convert an existing full clone to shallow
git fetch --depth 1
git reflog expire --expire=now --all
git gc --prune=now

# Check if a repo is shallow
git rev-parse --is-shallow-repository

# Unshallow if you need full history later
git fetch --unshallow

Comparison: Full vs. Shallow Clone Sizes

Repository Full Clone Shallow (depth=1) Savings
linux kernel 4.5GB 260MB 94%
chromium 12GB 850MB 93%
react 350MB 45MB 87%
swift 1.8GB 180MB 90%
tensorflow 3.2GB 280MB 91%

9. git maintenance — Automated Optimization

Git 2.29+ includes a built-in git maintenance command that automates repository optimization in the background:

# Start background maintenance for a repository
cd /path/to/repo
git maintenance start

# This registers the repo and runs tasks on a schedule:
# - Hourly: commit-graph, prefetch
# - Daily: loose-objects, incremental-repack
# - Weekly: pack-refs

# Check which repos are registered for maintenance
git config --global --get-all maintenance.repo

# Run maintenance manually
git maintenance run

# Run specific maintenance tasks
git maintenance run --task=gc
git maintenance run --task=loose-objects
git maintenance run --task=incremental-repack
git maintenance run --task=pack-refs

# Stop background maintenance
git maintenance unregister

Configure Maintenance Schedule

# Use launchd on macOS (git maintenance uses this by default)
git maintenance start --scheduler=launchd

# Check the launchd configuration
ls ~/Library/LaunchAgents/org.git-scm.git.*

# Verify maintenance is running
launchctl list | grep git-maintenance
Recommendation: Run git maintenance start in your most-used repositories. It's lightweight and keeps repos optimized automatically without you thinking about it.

10. Bulk Cleanup — All Repos at Once

If you have many repositories, clean them all at once:

#!/bin/bash
# bulk-git-cleanup.sh — Clean all Git repos under a directory

BASE_DIR="${1:-$HOME/Developer}"
total_before=0
total_after=0

echo "=== Bulk Git Repository Cleanup ==="
echo "Scanning: $BASE_DIR"
echo ""

find "$BASE_DIR" -name ".git" -type d -maxdepth 5 2>/dev/null | while read gitdir; do
    repo_dir=$(dirname "$gitdir")
    repo_name=$(basename "$repo_dir")
    
    before=$(du -sk "$gitdir" 2>/dev/null | awk '{print $1}')
    
    # Change to repo directory
    cd "$repo_dir" 2>/dev/null || continue
    
    # Run cleanup
    git remote prune origin 2>/dev/null
    git reflog expire --expire=30.days --all 2>/dev/null
    git gc --prune=now 2>/dev/null
    git worktree prune 2>/dev/null
    git lfs prune 2>/dev/null
    
    after=$(du -sk "$gitdir" 2>/dev/null | awk '{print $1}')
    saved=$((before - after))
    
    if [[ $saved -gt 1024 ]]; then
        echo "  $repo_name: saved $((saved/1024))MB ($((before/1024))MB → $((after/1024))MB)"
    fi
done

echo ""
echo "Done! Run the audit script to see updated totals."

Make executable and run:

chmod +x bulk-git-cleanup.sh

# Clean all repos under ~/Developer
./bulk-git-cleanup.sh ~/Developer

# Clean all repos under a specific directory
./bulk-git-cleanup.sh ~/Projects

11. Nuclear Options — When You Need Maximum Space

When you need to free the most space possible and can accept trade-offs:

11.1 Re-clone as Shallow

# If you don't need full history, delete and re-clone shallow
rm -rf my-project
git clone --depth 1 https://github.com/user/my-project.git

11.2 Remove Large Files from History

# Find the largest files in git history
git rev-list --objects --all | \
    git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
    awk '/^blob/ {printf "%s\t%s\t%s\n", $3, $2, $4}' | \
    sort -hr | head -20 | \
    numfmt --to=iec --field=1

# Use git filter-repo to remove large files (install first)
# brew install git-filter-repo
git filter-repo --strip-blobs-bigger-than 10M
Caution: Rewriting history with git filter-repo changes all commit hashes. Only do this on repositories you own, and coordinate with your team. All collaborators will need to re-clone.

11.3 Delete Repos You Don't Need Locally

# Find repos you haven't touched in over 6 months
find ~/Developer -name ".git" -type d -maxdepth 4 2>/dev/null | while read gitdir; do
    repo_dir=$(dirname "$gitdir")
    last_commit=$(git -C "$repo_dir" log -1 --format="%ar" 2>/dev/null)
    repo_size=$(du -sh "$repo_dir" 2>/dev/null | awk '{print $1}')
    echo "$repo_size  $last_commit  $(basename $repo_dir)"
done | sort -hr | head -20

# If it's on GitHub/GitLab, you can always re-clone later
# rm -rf ~/Developer/old-unused-project

12. Git Cleanup Cheatsheet

Command What It Does Space Saved Risk
git gc Compress objects, basic cleanup 5-20% None
git gc --aggressive Deep compression, rewrite packs 10-50% None (slow)
git remote prune origin Remove stale remote branches Minimal None
git reflog expire Expire old reflog entries 5-15% Low (recovery limited)
git prune --expire=now Remove unreachable objects 5-30% Low
git lfs prune Remove unused LFS objects Variable (large) None
git worktree prune Remove orphaned worktrees Variable None
git fetch --prune Fetch + remove stale remotes Minimal None
git clone --depth 1 Shallow clone (no history) 80-95% No git blame/log
git maintenance start Background auto-optimization Continuous None
git filter-repo Rewrite history (remove large files) Massive High (rewrites history)

13. FAQ

How often should I run git gc?

Git runs git gc --auto automatically when loose objects exceed a threshold (typically ~6700). For most developers, this is sufficient. If you want proactive maintenance, use git maintenance start to schedule background optimization. Running git gc --aggressive monthly on large repos is a good practice if disk space is a concern.

Is it safe to delete the .git directory?

Deleting .git removes all version history, branches, tags, and stashes from the local copy. Your working directory files remain intact. If the repo is pushed to a remote (GitHub, GitLab, etc.), you can always re-clone. But if you have local-only branches or stashes, they'll be permanently lost. Only delete .git if you just need the current files or plan to re-clone.

Why is my .git directory larger than the actual code?

The .git directory stores the entire history of every file. If a 5MB file was modified 100 times, Git stores delta-compressed versions of all 100 states. Binary files (images, compiled assets) that were committed and later deleted are still in the history. Use git rev-list (shown in Section 11.2) to find the largest objects in your history.

Will shallow cloning break anything?

Shallow clones work fine for reading code, building, and even pushing commits. However, you can't git blame past the shallow boundary, git log is truncated, and some merge strategies may not work. For development repos where you need full history, keep a full clone. For reference repos, CI builds, and read-only checkouts, shallow clones are ideal.

What's the difference between git prune and git gc?

git prune only removes unreachable loose objects. git gc does everything: it runs git prune, compresses loose objects into pack files, cleans up reflogs, repacks pack files, and updates the commit graph. Always prefer git gc — it's the comprehensive cleanup tool that includes pruning.

Can ClearDisk help with Git repository cleanup?

Yes! ClearDisk is a free macOS menu bar app that monitors 44+ developer cache locations in real time, giving you instant visibility into disk usage by developer tools including Git repositories, Xcode DerivedData, Docker, node_modules, and more. While Git-specific operations like gc and prune require the CLI, ClearDisk helps you identify which repos and caches are consuming the most space so you know where to focus your cleanup efforts.

Monitor All Your Developer Disk Usage

ClearDisk lives in your menu bar and monitors 44+ cache paths — Git repos, Xcode, Docker, node_modules, IDE caches, and more — so you always know where your disk space is going.

Get ClearDisk — Free & Open Source

Related guides:

About ClearDisk: A free, open-source macOS menu bar utility that monitors developer cache disk usage in real time. Supports 44+ cache paths including Xcode, node_modules, CocoaPods, Gradle, Docker, pip, Cargo, Homebrew, and more. View on GitHub.