macOS self-hosted GitHub Actions runners are expensive machines — and they fill up fast. Between Xcode DerivedData, Docker images, Homebrew caches, npm/yarn packages, Gradle artifacts, and runner update leftovers, a 256GB SSD can hit 90% capacity within weeks of active CI usage.
This guide covers every major cache path that accumulates on macOS CI runners, how to audit them, and how to set up automated cleanup workflows to keep your runners healthy.
Before cleaning anything, understand what's consuming space. Run this comprehensive audit on your macOS runner:
# Overall disk usage
df -h /
# Top-level directory sizes
sudo du -sh /* 2>/dev/null | sort -hr | head -20
# User-level breakdown (where most dev caches live)
du -sh ~/Library/Developer/* 2>/dev/null | sort -hr
du -sh ~/Library/Caches/* 2>/dev/null | sort -hr
du -sh ~/.* 2>/dev/null | sort -hr | head -20
| Path | Typical Size | Growth Rate |
|---|---|---|
~/Library/Developer/Xcode/DerivedData/ | 5-30 GB | Fast (every build) |
~/Library/Developer/CoreSimulator/ | 5-20 GB | Per simulator version |
~/Library/Developer/Xcode/iOS DeviceSupport/ | 2-10 GB | Per iOS version |
~/Library/Caches/org.swift.swiftpm/ | 1-5 GB | Per dependency |
~/.docker/ | 5-50 GB | Per image/layer |
$(brew --cache) | 2-10 GB | Per install/upgrade |
~/.npm/ | 1-5 GB | Per package version |
~/.gradle/caches/ | 2-15 GB | Per dependency version |
~/actions-runner/_work/ | 5-30 GB | Per workflow run |
The runner itself accumulates bloat over time, especially with automatic updates:
# Check work directory size
du -sh ~/actions-runner/_work/ 2>/dev/null
# Check individual repo work dirs
du -sh ~/actions-runner/_work/*/ 2>/dev/null | sort -hr | head -10
# Tool cache (setup-node, setup-python, etc.)
du -sh ~/actions-runner/_work/_tool/ 2>/dev/null
# Old runner binaries from auto-updates
ls -la ~/actions-runner/bin.* ~/actions-runner/externals.* 2>/dev/null
# Clean old runner update artifacts
rm -rf ~/actions-runner/bin.* ~/actions-runner/externals.* 2>/dev/null
rm -rf ~/actions-runner/_work/_update/externals 2>/dev/null
~/actions-runner/_work/ while workflows are running. Schedule cleanup during idle periods.
# Remove work dirs older than 7 days (safe for most CI setups)
find ~/actions-runner/_work/ -maxdepth 1 -mindepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null
# Or remove all (runner recreates on next run)
rm -rf ~/actions-runner/_work/*/
Xcode is the #1 disk space consumer on macOS CI runners. iOS/macOS builds generate massive DerivedData directories.
# Check size
du -sh ~/Library/Developer/Xcode/DerivedData/ 2>/dev/null
# Clean all DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# Alternative: clean only builds older than 3 days
find ~/Library/Developer/Xcode/DerivedData/ -maxdepth 1 -mindepth 1 -mtime +3 -exec rm -rf {} + 2>/dev/null
# List all simulators
xcrun simctl list devices
# Delete unavailable simulators
xcrun simctl delete unavailable
# Reset all simulator content
xcrun simctl erase all
# Nuclear: delete all simulator runtimes (re-downloads as needed)
xcrun simctl runtime delete all 2>/dev/null
# Check size
du -sh ~/Library/Developer/Xcode/iOS\ DeviceSupport/ 2>/dev/null
# Remove old device support files (keep only latest)
ls -t ~/Library/Developer/Xcode/iOS\ DeviceSupport/ | tail -n +3 | while read d; do
rm -rf "$HOME/Library/Developer/Xcode/iOS DeviceSupport/$d"
done
# Xcode Archives
du -sh ~/Library/Developer/Xcode/Archives/ 2>/dev/null
rm -rf ~/Library/Developer/Xcode/Archives/*
# Swift Package Manager cache
du -sh ~/Library/Caches/org.swift.swiftpm/ 2>/dev/null
rm -rf ~/Library/Caches/org.swift.swiftpm/*
# Xcode module cache
rm -rf ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/*
If your CI uses Docker (via Docker Desktop, Colima, or OrbStack), images and build caches accumulate quickly:
# Check Docker disk usage
docker system df
# Standard cleanup (removes stopped containers, unused networks, dangling images)
docker system prune -f
# Aggressive cleanup (removes ALL unused images, not just dangling)
docker system prune -af
# Also clean volumes
docker system prune -af --volumes
# If using BuildKit, clean build cache separately
docker builder prune -af
docker system prune -af --volumes is usually safe since you rebuild everything from scratch anyway.
# Check cache size
du -sh $(brew --cache) 2>/dev/null
# Clean downloads and old versions
brew cleanup --prune=0
# Remove all cached downloads
rm -rf $(brew --cache)/*
# npm cache
du -sh ~/.npm/ 2>/dev/null
npm cache clean --force
# Yarn Classic cache
du -sh ~/Library/Caches/Yarn/ 2>/dev/null
yarn cache clean
# Yarn Berry (v2+)
du -sh ~/.yarn/berry/cache/ 2>/dev/null
# pnpm store
du -sh ~/Library/pnpm/store/ 2>/dev/null
pnpm store prune
# pip cache
du -sh ~/Library/Caches/pip/ 2>/dev/null
pip cache purge
# Poetry cache
du -sh ~/Library/Caches/pypoetry/ 2>/dev/null
# CocoaPods cache
du -sh ~/Library/Caches/CocoaPods/ 2>/dev/null
pod cache clean --all
# CocoaPods repos
du -sh ~/.cocoapods/repos/ 2>/dev/null
# Gradle caches
du -sh ~/.gradle/caches/ 2>/dev/null
du -sh ~/.gradle/wrapper/ 2>/dev/null
# Clean Gradle cache
rm -rf ~/.gradle/caches/transforms-*
rm -rf ~/.gradle/caches/build-cache-*
rm -rf ~/.gradle/caches/journal-*
# Or nuclear: remove all caches (slower first build)
rm -rf ~/.gradle/caches/*
# Cargo registry and git
du -sh ~/.cargo/registry/ 2>/dev/null
du -sh ~/.cargo/git/ 2>/dev/null
# Clean registry
rm -rf ~/.cargo/registry/cache/*
rm -rf ~/.cargo/registry/src/*
# Go module cache
du -sh ~/go/pkg/mod/ 2>/dev/null
# Clean all Go caches
go clean -cache -modcache -testcache
# macOS system caches
du -sh ~/Library/Caches/ 2>/dev/null
# Log files
du -sh ~/Library/Logs/ 2>/dev/null
sudo du -sh /var/log/ 2>/dev/null
# Spotlight index (can be large, rebuilds automatically)
sudo mdutil -E / 2>/dev/null
# Trash
rm -rf ~/.Trash/*
Create a GitHub Actions workflow that runs weekly to clean your macOS self-hosted runner:
# .github/workflows/runner-cleanup.yml
name: macOS Runner Cleanup
on:
schedule:
- cron: '0 3 * * 0' # Every Sunday at 3 AM
workflow_dispatch: # Allow manual trigger
jobs:
cleanup:
runs-on: [self-hosted, macOS]
steps:
- name: Pre-cleanup disk usage
run: df -h /
- name: Clean Xcode caches
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf ~/Library/Developer/Xcode/Archives/*
xcrun simctl delete unavailable 2>/dev/null || true
rm -rf ~/Library/Developer/CoreSimulator/Caches/*
- name: Clean package managers
run: |
brew cleanup --prune=0 2>/dev/null || true
npm cache clean --force 2>/dev/null || true
pip cache purge 2>/dev/null || true
pod cache clean --all 2>/dev/null || true
- name: Clean build tools
run: |
rm -rf ~/.gradle/caches/transforms-*
rm -rf ~/.gradle/caches/build-cache-*
go clean -cache 2>/dev/null || true
- name: Clean Docker
run: |
docker system prune -af --volumes 2>/dev/null || true
docker builder prune -af 2>/dev/null || true
- name: Clean runner artifacts
run: |
rm -rf ~/actions-runner/bin.* ~/actions-runner/externals.* 2>/dev/null || true
find ~/actions-runner/_work/ -maxdepth 1 -mindepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true
- name: Clean system caches
run: |
rm -rf ~/Library/Logs/*
rm -rf ~/.Trash/*
- name: Post-cleanup disk usage
run: df -h /
workflow_dispatch to allow manual trigger when disk is critically low.
Set up proactive monitoring so you catch disk issues before builds start failing:
# Add this as the FIRST step in your CI workflows
- name: Check disk space
run: |
USAGE=$(df -h / | tail -1 | awk '{print $5}' | sed 's/%//')
echo "Disk usage: ${USAGE}%"
if [ "$USAGE" -gt 85 ]; then
echo "⚠️ WARNING: Disk usage is above 85%!"
echo "Running emergency cleanup..."
rm -rf ~/Library/Developer/Xcode/DerivedData/*
brew cleanup --prune=0 2>/dev/null || true
docker system prune -af 2>/dev/null || true
echo "Post-cleanup:"
df -h /
fi
If your macOS runners have screen access (e.g., Mac minis), ClearDisk provides a real-time menu bar indicator showing total developer cache usage across 44+ paths. One glance tells you if the runner needs cleanup.
# Install ClearDisk on macOS runners
brew tap bysiber/cleardisk
brew install --cask cleardisk
#!/bin/bash
# macos-runner-cleanup.sh — Complete CI runner cleanup script
# Usage: ./macos-runner-cleanup.sh [--aggressive]
echo "=== macOS CI Runner Cleanup ==="
echo "Before: $(df -h / | tail -1 | awk '{print $4}') free"
# Xcode (always safe on CI)
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf ~/Library/Developer/Xcode/Archives/*
rm -rf ~/Library/Developer/CoreSimulator/Caches/*
xcrun simctl delete unavailable 2>/dev/null
# Package managers
brew cleanup --prune=0 2>/dev/null
npm cache clean --force 2>/dev/null
pip cache purge 2>/dev/null
pod cache clean --all 2>/dev/null
# Build tools
rm -rf ~/.gradle/caches/transforms-*
rm -rf ~/.gradle/caches/build-cache-*
go clean -cache 2>/dev/null
# Docker
docker system prune -f 2>/dev/null
# Runner artifacts
rm -rf ~/actions-runner/bin.* ~/actions-runner/externals.* 2>/dev/null
# System
rm -rf ~/Library/Logs/* ~/.Trash/*
if [[ "$1" == "--aggressive" ]]; then
echo "=== Aggressive mode ==="
docker system prune -af --volumes 2>/dev/null
rm -rf ~/.gradle/caches/*
rm -rf ~/.npm/*
rm -rf ~/Library/Caches/pip/*
rm -rf ~/.cargo/registry/cache/* ~/.cargo/registry/src/*
go clean -cache -modcache 2>/dev/null
rm -rf ~/Library/Caches/CocoaPods/*
rm -rf ~/Library/Caches/org.swift.swiftpm/*
fi
echo "After: $(df -h / | tail -1 | awk '{print $4}') free"
ClearDisk is a free, open-source macOS menu bar utility that monitors 44+ developer cache paths in real-time — perfect for keeping an eye on your CI runner disk usage.
brew tap bysiber/cleardisk && brew install --cask cleardisk
Weekly cleanup is sufficient for most teams. If you run 50+ builds per day or build large Xcode projects, consider daily cleanup of DerivedData and Docker caches. Set up the automated workflow in section 8.
Yes. DerivedData contains build intermediates that are regenerated on the next build. On CI, builds typically start fresh anyway. The only downside is a slightly slower first build after cleanup.
On an active macOS CI runner that hasn't been cleaned in a month, expect to recover 20-60 GB. The biggest contributors are usually DerivedData (5-30 GB), Docker images (5-50 GB), and Homebrew cache (2-10 GB).
GitHub-hosted runners are ephemeral — they're destroyed after each workflow run. Cleanup is only needed for self-hosted runners that persist between jobs.
Package manager caches (npm, pip, brew, etc.) are just download caches. Deleting them means packages will be re-downloaded on the next install, which takes a few extra minutes but won't break anything.