macOS CI Runner Disk Space Cleanup — GitHub Actions Self-Hosted Runner Guide

Updated March 2025 · 12 min read · By bysiber

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.

Table of Contents

  1. Full Disk Audit for macOS Runners
  2. GitHub Actions Runner Artifacts
  3. Xcode Caches on CI
  4. Docker Caches
  5. Package Manager Caches
  6. Build Tool Caches
  7. System-Level Caches
  8. Automated Weekly Cleanup Workflow
  9. Monitoring Disk Usage
  10. Quick Reference Cheatsheet
  11. FAQ

1. Full Disk Audit for macOS Runners

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

Common macOS Runner Disk Usage Breakdown

PathTypical SizeGrowth Rate
~/Library/Developer/Xcode/DerivedData/5-30 GBFast (every build)
~/Library/Developer/CoreSimulator/5-20 GBPer simulator version
~/Library/Developer/Xcode/iOS DeviceSupport/2-10 GBPer iOS version
~/Library/Caches/org.swift.swiftpm/1-5 GBPer dependency
~/.docker/5-50 GBPer image/layer
$(brew --cache)2-10 GBPer install/upgrade
~/.npm/1-5 GBPer package version
~/.gradle/caches/2-15 GBPer dependency version
~/actions-runner/_work/5-30 GBPer workflow run

2. GitHub Actions Runner Artifacts

The runner itself accumulates bloat over time, especially with automatic updates:

Runner work directory

# 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

Runner update leftovers

# 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
⚠️ Warning: Don't delete ~/actions-runner/_work/ while workflows are running. Schedule cleanup during idle periods.

Clean stale work directories

# 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/*/

3. Xcode Caches on CI

Xcode is the #1 disk space consumer on macOS CI runners. iOS/macOS builds generate massive DerivedData directories.

DerivedData (biggest offender)

# 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

iOS Simulators

# 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

iOS DeviceSupport

# 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

Archives and build caches

# 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/*

4. Docker Caches

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
💡 Tip: On CI runners, docker system prune -af --volumes is usually safe since you rebuild everything from scratch anyway.

5. Package Manager Caches

Homebrew

# 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 / Yarn / pnpm

# 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 / Poetry

# pip cache
du -sh ~/Library/Caches/pip/ 2>/dev/null
pip cache purge

# Poetry cache
du -sh ~/Library/Caches/pypoetry/ 2>/dev/null

CocoaPods

# CocoaPods cache
du -sh ~/Library/Caches/CocoaPods/ 2>/dev/null
pod cache clean --all

# CocoaPods repos
du -sh ~/.cocoapods/repos/ 2>/dev/null

6. Build Tool Caches

Gradle (Android / Java builds)

# 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 (Rust builds)

# 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 modules

# Go module cache
du -sh ~/go/pkg/mod/ 2>/dev/null

# Clean all Go caches
go clean -cache -modcache -testcache

7. System-Level Caches

# 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/*

8. Automated Weekly Cleanup Workflow

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 /
💡 Tip: Add workflow_dispatch to allow manual trigger when disk is critically low.

9. Monitoring Disk Usage

Set up proactive monitoring so you catch disk issues before builds start failing:

GitHub Actions step to check disk before each build

# 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

Using ClearDisk for visual monitoring

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

10. Quick Reference Cheatsheet

#!/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"

Monitor All Cache Paths Automatically

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

FAQ

How often should I clean my macOS CI runner?

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.

Is it safe to delete DerivedData on a CI runner?

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.

How much space can I typically recover?

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).

Should I clean caches on GitHub-hosted macOS runners?

GitHub-hosted runners are ephemeral — they're destroyed after each workflow run. Cleanup is only needed for self-hosted runners that persist between jobs.

Will cleanup break my cached dependencies?

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.