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.
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
.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
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.
# Run garbage collection (safe, fast)
cd /path/to/repo
git gc
# Check repo size before and after
du -sh .git
# 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
--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.
.git/objects get compressed into pack filesGit 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
git reset --hard, accidental branch deletions, and bad rebases. Don't expire it too aggressively on repos where you're actively working.
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
# 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
If your repository uses Git LFS for large binary files (images, videos, models, datasets), the LFS cache can grow substantially:
# 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
# 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
# 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
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
git config --global fetch.prune true ensures stale remote branches are automatically cleaned up every time you git fetch.
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
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
| 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% |
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
# 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
git maintenance start in your most-used repositories. It's lightweight and keeps repos optimized automatically without you thinking about it.
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
When you need to free the most space possible and can accept trade-offs:
# 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
# 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
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.
# 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
| 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) |
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.
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.
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.
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.
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.
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.
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 SourceRelated 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.