A Mac used for development fills up in places Finder doesn’t show: build folders in every project, package caches in hidden folders, and container disk images whose reported size isn’t what they use. Most of it rebuilds itself. Some of it doesn’t.
Below is each of those places: where it lives, how to measure it, what’s safe to delete and the tool’s own cleanup command. First, though, a short read-only script that measures all of them at once, so you clean what’s big on your Mac rather than what a generic guide lists.
The checklist
| What | Where | Comes back? | Clean up with |
|---|---|---|---|
| Xcode build data | ~/Library/Developer/Xcode/DerivedData |
Yes, next build | Delete a project’s folder |
| Xcode archives | ~/Library/Developer/Xcode/Archives |
No | Window > Organizer |
| Device symbols | ~/Library/Developer/Xcode/iOS DeviceSupport |
Yes, on reconnect | Delete old OS versions |
| Simulators | ~/Library/Developer/CoreSimulator/Devices |
No | xcrun simctl delete unavailable |
| Simulator runtimes | Outside your home folder | Re-downloadable | Xcode > Settings > Components |
| Docker Desktop | ~/Library/Containers/com.docker.docker |
Images yes, volumes no | docker system prune |
| OrbStack | ~/Library/Group Containers/HUAQ24HBR6.dev.orbstack |
Images yes, volumes no | The same docker commands |
| node_modules | Every JavaScript project | Yes, on install | Delete, then npm ci |
| npm, Yarn, pnpm | ~/.npm, ~/Library/Caches/Yarn, ~/.yarn/berry, ~/Library/pnpm/store |
Re-downloaded | npm cache clean --force, yarn cache clean, pnpm store prune |
| Rust builds | target in every Rust project |
Yes, next build | cargo clean |
| Cargo downloads | ~/.cargo/registry, ~/.cargo/git |
Re-downloaded | Automatic since Rust 1.88 |
| Rust toolchains | ~/.rustup/toolchains |
Reinstallable | rustup toolchain uninstall |
| Gradle | ~/.gradle/caches, ~/.gradle/wrapper/dists |
Re-downloaded | Automatic, or delete with daemons stopped |
| Android emulators | ~/Library/Android/sdk/system-images, ~/.android/avd |
Images yes, emulator data no | SDK Manager, Device Manager |
| Homebrew | ~/Library/Caches/Homebrew, old versions |
Downloads yes | brew cleanup |
| CocoaPods, pip, uv, Go | See the last section | Re-downloaded | Each tool’s cache command |
“Comes back” means the tool recreates it when it next needs it, at the cost of time and bandwidth. “No” means deleting it loses something for good.
Measure first with one read-only script
Save this as dev-disk-audit.sh:
#!/bin/bash
# dev-disk-audit.sh: size the usual developer storage on a Mac, largest first.
# Read-only: it sizes things with du, find and test, and changes nothing.
#
# bash dev-disk-audit.sh [projects folder] (zsh dev-disk-audit.sh works too)
#
# The projects folder (default: your home folder) is searched for node_modules
# and Cargo target folders. Hidden folders, ~/Library and ~/go/pkg are skipped
# there. Sizes are space allocated on disk, in decimal GB and MB like Finder.
root=${1:-$HOME}
L=$HOME/Library
# Print "KB<tab>label<tab>path" if the path exists; stay silent if it doesn't.
measure() {
if [ -e "$2" ]; then
printf '%s\t%s\t%s\n' "$(du -sk "$2" 2>/dev/null | cut -f1)" "$1" "$2"
fi
}
# One "KB<tab>path" line per node_modules folder, and per target folder that
# Cargo has tagged with CACHEDIR.TAG (other tools' target folders are left out).
projects=$(find "$root" -mindepth 1 \
\( -name '.*' -o -path "$L" -o -path "$HOME/go/pkg" \) -prune -o \
-type d -name node_modules -prune -exec du -sk {} + -o \
-type d -name target -prune -exec test -f {}/CACHEDIR.TAG \; -exec du -sk {} + \
2>/dev/null)
# Print size lines as a table: "KB<tab>label<tab>path" or "KB<tab>path".
table='function size(k) { b = k * 1024
return b >= 1e9 ? sprintf("%7.1f GB", b / 1e9) : sprintf("%7.1f MB", b / 1e6) }
function short(p) { return index(p, home) == 1 ? "~" substr(p, length(home) + 1) : p }
NF < 2 { next }
{ sum += $1
if (NF == 2) printf "%s %s\n", size($1), short($2)
else printf "%s %-28s %s\n", size($1), $2, short($3) }
END { if (total) printf "%s Total measured\n", size(sum) }'
printf 'Developer storage, largest first\n\n'
{
measure "Xcode DerivedData" "$L/Developer/Xcode/DerivedData"
measure "Xcode Archives" "$L/Developer/Xcode/Archives"
measure "iOS device support" "$L/Developer/Xcode/iOS DeviceSupport"
measure "watchOS device support" "$L/Developer/Xcode/watchOS DeviceSupport"
measure "Simulator devices" "$L/Developer/CoreSimulator/Devices"
measure "Simulator caches" "$L/Developer/CoreSimulator/Caches"
measure "Docker Desktop disk image" "$L/Containers/com.docker.docker/Data/vms/0/data/Docker.raw"
measure "OrbStack disk image" "$L/Group Containers/HUAQ24HBR6.dev.orbstack/data/data.img"
measure "OrbStack disk image" "$L/Group Containers/HUAQ24HBR6.dev.orbstack/data/data.img.raw"
measure "npm cache" "$HOME/.npm"
measure "Yarn 1 cache" "$L/Caches/Yarn"
measure "Yarn 2+ global cache" "$HOME/.yarn/berry/cache"
measure "pnpm store" "$L/pnpm/store"
measure "Cargo registry" "$HOME/.cargo/registry"
measure "Cargo git dependencies" "$HOME/.cargo/git"
measure "Rust toolchains" "$HOME/.rustup/toolchains"
measure "Gradle caches" "$HOME/.gradle/caches"
measure "Gradle wrapper downloads" "$HOME/.gradle/wrapper/dists"
measure "Android system images" "$L/Android/sdk/system-images"
measure "Android emulators (AVDs)" "$HOME/.android/avd"
measure "Homebrew downloads" "$L/Caches/Homebrew"
measure "Homebrew packages" "/opt/homebrew/Cellar"
measure "Homebrew packages" "/usr/local/Cellar"
measure "CocoaPods cache" "$L/Caches/CocoaPods"
measure "pip cache" "$L/Caches/pip"
measure "uv cache" "$HOME/.cache/uv"
measure "Go module cache" "$HOME/go/pkg/mod"
measure "Go build cache" "$L/Caches/go-build"
printf '%s\n' "$projects" | awk -F'\t' -v r="$root" '
$2 ~ /\/node_modules$/ { n += $1; nc++ }
$2 ~ /\/target$/ { t += $1; tc++ }
END { if (nc) printf "%.0f\tnode_modules folders (%d)\t%s\n", n, nc, r
if (tc) printf "%.0f\tRust target folders (%d)\t%s\n", t, tc, r }'
} | sort -rn | awk -F'\t' -v home="$HOME" -v total=1 "$table"
if [ -n "$projects" ]; then
printf '\nLargest project folders\n\n'
printf '%s\n' "$projects" | sort -rn | head -n 10 |
awk -F'\t' -v home="$HOME" -v total=0 "$table"
fi
Then run it with the folder that holds your projects:
bash dev-disk-audit.sh ~/code
It runs under the zsh and bash 3.2 that come with macOS, installs nothing and only reads: du, find and test look at your files, and sort and awk print the table. Paths that don’t exist are skipped. Without a folder argument it searches your whole home folder, which is slower, and macOS may ask before Terminal reads Desktop, Documents or Downloads.
Some choices in it are deliberate:
- Allocated space, not apparent size. Docker’s and OrbStack’s disk images are sparse files:
ls -lshows what they could grow to,dushows what they occupy. - A
targetfolder counts only if Cargo tagged it withCACHEDIR.TAG, a cross-tool convention meaning “everything in here can be regenerated”. Maven builds intotargetfolders too, and those stay out of the Rust total. - OrbStack’s disk image is sized as a file, not by listing its folder. On macOS 15 and later, reading inside another app’s group container can trigger a permission prompt.
Two caveats. du counts a hard-linked file once per run but counts every APFS clone in full, and on macOS pnpm clones files from its store into node_modules by default, so read the total as an upper bound. And simulator runtimes live outside your home folder; the next section shows how to size them.
Xcode
DerivedData
~/Library/Developer/Xcode/DerivedData holds build products, intermediate files and logs, mostly one folder per project. Deleting a project’s folder costs a full rebuild and nothing else. Quit Xcode first. If you’ve moved it, Xcode > Settings > Locations shows where.
Archives
Product > Archive saves to ~/Library/Developer/Xcode/Archives, and nothing recreates an archive once it’s gone. Each one holds your app’s binaries and their dSYM files, and Apple’s documentation says to keep the archive for every build you distribute, or you may not be able to diagnose its crash reports. Review them in Window > Organizer, delete archives for builds that never shipped, and move the rest to another disk if you need the space.
Device support
When you connect an iPhone, iPad or Apple Watch, Xcode copies its system symbols into ~/Library/Developer/Xcode/iOS DeviceSupport (or watchOS DeviceSupport, and so on) so it can symbolicate system frameworks in crash reports. Each OS release and device model gets its own set.
Sets for versions you no longer test against are reasonable to delete. Xcode copies them again when a device on that version connects, and xcodebuild -prepareDeviceSupport can fetch them on request. Keep any you need for symbolicating your users’ crash reports.
Simulators and runtimes
Each simulator keeps its installed apps and data in ~/Library/Developer/CoreSimulator/Devices, and deleting a simulator deletes everything in it. List first:
xcrun simctl list devices
xcrun simctl delete unavailable
The second command removes only the simulators your current Xcode SDK doesn’t support.
Runtimes, the system images that simulators boot, are stored outside your home folder, so ask simctl about them:
xcrun simctl runtime list
xcrun simctl runtime delete <identifier>
list ends with their total size. Xcode > Settings > Components does the same job (click the info button next to a runtime, then Delete) and can download older runtimes again. Run xcrun simctl runtime on its own to see what your Xcode version supports; current versions can preview a cleanup with delete --notUsedSinceDays <days> --dry-run.
Docker Desktop and OrbStack
Start with Docker’s own accounting:
docker system df
docker system df -v
It splits usage into images, containers, local volumes and build cache, with a RECLAIMABLE column for space nothing is using; -v lists every item. Then prune, narrowest first: docker builder prune removes only the build cache, docker image prune -a removes every image no container uses, and docker system prune removes stopped containers, unused networks, dangling images and the build cache. Each asks for confirmation unless you add -f:
docker builder prune
docker image prune -a
docker system prune
Images and build cache come back when you pull or build again. Stopped containers don’t, and neither does anything you changed inside them. Volumes are the real risk: they hold databases and other container state, and removing them is permanent. The commands above leave volumes alone, but docker system prune --volumes adds anonymous volumes, and docker volume prune -a also removes named volumes that no container is using. Check docker volume ls first.
Docker Desktop’s disk image
Docker Desktop keeps all of this in one sparse file, Docker.raw, in ~/Library/Containers/com.docker.docker/Data/vms/0/data. Settings > Resources > Advanced shows its location, its maximum size and what it actually uses, while some tools report only the maximum. In Terminal:
ls -klsh ~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw
The first column is the space it occupies; the size column further along is the maximum.
Does it shrink? Docker’s current documentation says deleting images frees space on your Mac, though it can take a few minutes to show, while deleting files inside a running container doesn’t free it automatically. To reclaim space on demand, Docker documents this command:
docker run --privileged --pid=host docker/desktop-reclaim-space
Don’t move or delete Docker.raw in Finder, because Docker Desktop can lose track of it; change its location in Settings instead. And lowering the Disk image size slider deletes the current image, with every container and image inside.
OrbStack
OrbStack keeps images, containers and Linux machines in a sparse disk image (data.img or data.img.raw) in ~/Library/Group Containers/HUAQ24HBR6.dev.orbstack/data. Its documentation explains that the file can show an apparent size of 8 TB while using only what you store, and that it shrinks automatically when you delete data. du -h on the file, or “size on disk” in Finder’s Get Info window, gives the real figure.
OrbStack creates a Docker context called orbstack, so the commands above work against it. If you run more than one engine, docker context ls shows which one your commands reach, and docker --context orbstack system df asks OrbStack directly.
node_modules and package caches
Every JavaScript project carries its own node_modules. To list them by size:
find ~/code -type d -name node_modules -prune -exec du -sh {} + | sort -h
-prune stops find from descending into each one, so nested node_modules aren’t counted twice. Deleting a project’s node_modules is safe while its package.json and lockfile are intact: npm ci reinstalls exactly what package-lock.json specifies, and pnpm install or yarn install rebuild it for those tools.
The shared caches:
- npm keeps its cache in
~/.npm(npm config get cacheconfirms it). npm’s documentation calls the cache self-healing and says clearing it should only be needed to reclaim disk space.npm cache verifygarbage-collects data that isn’t needed;npm cache clean --forceempties it. - Yarn 1:
yarn cache dirprints the location, under~/Library/Caches/Yarnon a Mac, andyarn cache cleanempties it. Yarn 2 and later keep a global cache in~/.yarn/berry/cacheby default, andyarn cache clean --mirrorclears it. - pnpm keeps one content-addressable store,
~/Library/pnpm/storeon macOS (pnpm store pathprints it).pnpm store pruneremoves packages no project references. pnpm’s documentation calls that harmless but suggests not running it too often, since switching to an older branch may need those packages again.
Rust
Every Cargo workspace builds into a target folder at its root. cargo clean deletes it, and the next build compiles everything again; in current Cargo, cargo clean --dry-run shows what would go. To find every Rust build folder without catching other tools’ target folders, look for Cargo’s tag:
find ~/code -type d -name target -prune -exec test -f {}/CACHEDIR.TAG \; -exec du -sh {} + | sort -h
Cargo’s shared downloads live in ~/.cargo/registry (the index, downloaded .crate archives in cache and unpacked sources in src) and ~/.cargo/git. The Cargo Book says you can remove any part of this cache and Cargo will restore what a build needs. Since Rust 1.88, Cargo also cleans it on its own, removing files downloaded from the network after three months without use and files from your own system after one month. Leave ~/.cargo/bin alone: it holds the tools you installed with cargo install.
Installed toolchains live in ~/.rustup/toolchains. rustup toolchain list shows them, and rustup toolchain uninstall <name> removes old nightlies or pinned versions you no longer build with.
Gradle and Android
Gradle’s user home, ~/.gradle, keeps downloaded dependencies and build caches in caches and wrapper distributions in wrapper/dists. Gradle prunes these itself: by default it checks every 24 hours and removes entries unused for 30 days (downloaded resources, released wrapper distributions) or 7 days (created resources, the build cache).
In a project, ./gradlew clean deletes the build directory. To clear the shared cache entirely, run ./gradlew --stop to stop that Gradle version’s daemons, then delete ~/.gradle/caches. The next build downloads its dependencies again, and offline builds fail until it has.
Android emulators use two kinds of storage:
- System images live in the SDK, by default
~/Library/Android/sdk/system-images. In Android Studio’s SDK Manager (Tools > SDK Manager), which also shows the SDK’s real location, clear the checkbox next to an image you no longer use and click Apply. You can download it again later. - Each emulator’s own data, its installed apps and settings, lives in
~/.android/avd. In Device Manager, an emulator’s ⋮ menu has Wipe Data, which resets it to new, and Delete, which removes it. Both are permanent.
Homebrew
brew --cache prints the download cache, ~/Library/Caches/Homebrew by default. brew cleanup removes old versions of installed formulae, stale lock files and downloads older than 120 days (HOMEBREW_CLEANUP_MAX_AGE_DAYS changes that). Preview it first with -n, which only shows what would be removed. --prune=all removes all cached files, whatever their age, and brew autoremove -n lists dependencies nothing needs any more:
brew cleanup -n
brew cleanup
brew cleanup --prune=all
brew autoremove -n
Unless HOMEBREW_NO_INSTALL_CLEANUP is set, Homebrew already cleans up the packages involved after brew install, upgrade and reinstall, and everything every 30 days. brew autoremove without -n uninstalls formulae that were only installed as dependencies and are no longer needed. Installed packages live in $(brew --prefix)/Cellar; remove them with brew uninstall, not by deleting folders.
Other caches worth checking
- CocoaPods:
~/Library/Caches/CocoaPods.pod cache listshows what’s there,pod cache clean --allempties it, and the nextpod installdownloads what it needs. - pip:
pip cache dir(~/Library/Caches/pipon macOS),pip cache infofor its size andpip cache purgeto empty it. pip’s documentation says the cache layout can change between versions, so use these commands rather than deleting files by hand. - uv:
uv cache dir(~/.cache/uv),uv cache prunefor unused entries anduv cache cleanfor everything. - Go: the module cache is
go env GOMODCACHE(~/go/pkg/modby default) and the build cache isgo env GOCACHE(~/Library/Caches/go-buildon a Mac). Go creates module cache folders read-only, sorm -rfails part-way; usego clean -modcache. Go trims the build cache itself, andgo clean -cacheempties it.
Before you delete anything
Moving something to the Trash in Finder frees its space only when you empty the Trash, while rm -rf and each tool’s own prune and clean commands are permanent. Archives, Docker volumes and stopped containers, simulator and emulator data, and ~/.cargo/bin don’t come back. Everything else here rebuilds or downloads again, at the cost of time and bandwidth, so the caches for this week’s projects are usually worth keeping.
Disk Whisk, the Mac app behind this blog, shows the same kind of breakdown without a terminal, including what OrbStack, Docker Desktop, Colima and Lima hold, and clears containers through each engine’s own tools rather than by deleting its disk image.