October 10, 2026 · 12 min read
How to Clean Xcode DerivedData and Why It Grows to Tens of Gigabytes
When free space on a Mac starts running low, sooner or later your eye lands on the Xcode directory. iOS developers often have tens of gigabytes of DerivedData sitting there: caches, indexes, build products, Swift Package Manager dependencies.
Deleting everything in one command is easy. But afterward Xcode has to rebuild dependencies, recreate its index, and warm up its caches, so the first build after a wipe is noticeably slower than usual. It usually pays to understand what actually grew before deciding how to clean it.
The short answer: for most cases, grab a free dedicated tool like DevCleaner for Xcode from the Mac App Store or one of the open-source utilities listed below. A couple of clicks, no Terminal, typically reclaims 10–50 GB in a single pass. But if you want to understand what’s being deleted or wire size checks into your own workflow and CI, writing a small zsh script of your own is reasonable too.
This article walks through everything in order: what’s inside DerivedData, how to measure it, the various cleanup options, and at the end a dedicated section on my xcode-dd as an example of the DIY approach.
What DerivedData is for
DerivedData is Xcode’s working directory. It stores build products and intermediates, source-code indexes, Swift Package Manager data, logs, and caches. Keeping these files means Xcode does not have to repeat every step on each build.
The default location is:
~/Library/Developer/Xcode/DerivedData
You can view or change it under Xcode → Settings → Locations → Derived Data.
When you change one Swift file, the build system can often reuse previously compiled results and rebuild only what was affected. Indexing works similarly: Xcode keeps information needed for navigation and symbol search. This saves time at the cost of disk space.
Deleting DerivedData generally does not delete your source code. Xcode recreates what it needs, but the first build and indexing pass afterward may be noticeably slower.
What’s inside
A typical root directory looks like this:
DerivedData/
├── MyApp-hglrthxzzosvzfecqgfkqfqcjpvj/
├── AnotherApp-fgqhqozcpoxefycwqgslpklsgkzi/
├── CompilationCache.noindex/
├── ModuleCache.noindex/
└── SDKStatCaches.noindex/
Inside an individual project directory you may see:
MyApp-hglrthxzzosvzfecqgfkqfqcjpvj/
├── Build/
│ ├── Intermediates.noindex/
│ └── Products/
├── Index.noindex/
├── SourcePackages/
├── Logs/
└── info.plist
Build contains intermediate and final build products.
Index.noindex supports code navigation and symbol lookup.
SourcePackages stores Swift Package Manager checkouts and related data, which can become surprisingly large on projects with many dependencies.
Logs contains build and test logs.
The info.plist file is particularly useful: its WorkspacePath value points back to the .xcodeproj or .xcworkspace associated with that directory. You can inspect it with:
/usr/libexec/PlistBuddy -c 'Print :WorkspacePath' \
~/Library/Developer/Xcode/DerivedData/<project>/info.plist
The root-level CompilationCache.noindex, ModuleCache.noindex, and SDKStatCaches.noindex directories hold shared caches. They are not tied to one project, so removing a single project’s directory will not necessarily free all the space you expect.
Why it grows so much
Different destinations, build configurations, dependencies, and projects all leave data behind. Removing a checkout from disk also does not guarantee that its DerivedData disappears. Temporary Git worktrees created by coding agents can contribute to this accumulation.
Here is an illustrative, rounded breakdown of a 15.7 GB DerivedData directory:
10.5 GB Test-civxctdjvxfxvzewaauzvbgmsmgg
3.5 GB CompilationCache.noindex
1.6 GB ModuleCache.noindex
11.0 MB SDKStatCaches.noindex
Within the largest project, the distribution might be:
SourcePackages 7.6 GB
Build 2.7 GB
Index.noindex 135.0 MB
Logs 2.5 MB
In this example, dependencies occupy far more space than build products. That is why it helps to inspect the directory before deciding what to remove.
Finding the biggest directories
Start with the overall size:
du -sh ~/Library/Developer/Xcode/DerivedData
Then list the largest items:
du -sh ~/Library/Developer/Xcode/DerivedData/* 2>/dev/null | sort -hr
To inspect one project:
du -hd 1 ~/Library/Developer/Xcode/DerivedData/<project> 2>/dev/null | sort -hr
If you configured a custom DerivedData location, use that path instead.
Ways to clean DerivedData
There is more than one way to do it. Each has trade-offs.
1. Xcode Settings and Finder
Quit Xcode, open the DerivedData folder from Xcode → Settings → Locations (there is a small arrow next to the path that opens the folder in Finder), and remove the project directories you no longer need.
This is the safest option: you can see each folder’s size and delete only what you want to get rid of.
2. Terminal
To wipe the default location:
rm -rf ~/Library/Developer/Xcode/DerivedData/*
Be careful: rm -rf does not move files to Trash, and the * glob does not include hidden entries. Double-check the path before running it.
For build-related problems, try Product → Clean Build Folder first. It is not equivalent to removing all DerivedData: it does not completely reset indexing, package state, and shared caches. But for build failures it is often enough.
Not everything Xcode creates lives here. Archives sit under ~/Library/Developer/Xcode/Archives; simulator data lives under ~/Library/Developer/CoreSimulator. If those are what’s filling your disk, clearing DerivedData won’t do much.
3. Xcode 27: Product → Delete Derived Data
Xcode 27 finally added Product → Delete Derived Data (details). It clears the current project’s DerivedData directly from the IDE, so you don’t have to find the right directory manually.
A good option when you only want to reset the state of one project. Still, treating this as a mandatory ritual before every build usually wastes time rather than saving it: those cached files are what makes the next build fast. If the same error keeps disappearing only after a wipe, look for the real cause.
4. Free third-party tools
If you would rather not maintain a script or juggle Terminal commands, there are good off-the-shelf tools. All of the ones below are free, either freeware on the Mac App Store or open source on GitHub.
DevCleaner for Xcode: probably the most popular dedicated solution. A Mac App Store GUI tool that scans the whole ~/Library/Developer folder: DerivedData, old simulators, iOS Device Support, documentation caches. Freeware with a tip jar; typically reclaims 10–50 GB in one pass. For most users this is the “turnkey” answer, no script needed.
XcodeClean: an open-source menu-bar utility that bundles common Xcode cleanup actions, including DerivedData and package caches. What I liked most about it was precisely the menu-bar approach and the simple interface: a cleanup button one click away, no extra screens.
mac-dev-clean: an open-source CLI aimed at a broader range of developer caches. Useful if the space is coming from more than just Xcode.
ClearDisk: an open-source GUI for inspecting and clearing developer caches.
For anyone already working with coding agents: Xcode Disk Cleanup Agent Skill by Antoine van der Lee (open source). Instead of a fixed command, you can ask an agent to audit storage, propose deletion candidates, and discuss what is actually no longer needed. Automated analysis still doesn’t replace a sanity check on the paths before deleting.
5. Roll your own zsh script xcode-dd
If DevCleaner already exists, why write a script at all? Fair question — most people don’t need one. For a one-time 30 GB reclaim, the GUI tool is enough. A home-grown script makes sense when:
- you want to drop size checks into your usual workflow (a
git status-level habit, aliased in your shell), - you need to run it from CI, pre-commit hooks, or coding-agent scenarios without a GUI,
- you want custom protected paths, per-project filtering, or your own output format,
- everything you need is already in zsh and
du, and you’d rather not install another app.
If that fits, the rest of this section is for you.
xcode-dd: one command instead of several
xcode-dd has three modes:
| Command | Purpose |
|---|---|
xcode-dd / xcode-dd status | Total size, disk space, and largest directories |
xcode-dd details | Workspace path and sizes of Build, Index.noindex, SourcePackages, and Logs |
xcode-dd clean | Confirmed cleanup of DerivedData contents |
A sample xcode-dd run:
Xcode DerivedData
Path: /Users/eugene/Library/Developer/Xcode/DerivedData
Total: 15.7 GB
Disk: 460.4 GB
Free: 99.2 GB
Of disk: 3.4%
Of free: 15.8%
Largest:
10.5 GB Test-civxctdjvxfxvzewaauzvbgmsmgg
3.5 GB CompilationCache.noindex
1.6 GB ModuleCache.noindex
11.0 MB SDKStatCaches.noindex
xcode-dd details adds a per-project breakdown:
Test-civxctdjvxfxvzewaauzvbgmsmgg 10.5 GB
Workspace /Users/eugene/Test-ios/Test/Test.xcodeproj
Build 2.7 GB
Index.noindex 135.0 MB
SourcePackages 7.6 GB
Logs 2.5 MB
Core logic in one snippet
The idea is simple: du measures, sort and awk format. Here is a minimal status that does exactly that:
#!/bin/zsh
DD="$HOME/Library/Developer/Xcode/DerivedData"
echo "Total:"
du -sh "$DD" 2>/dev/null | awk '{print $1}'
echo
echo "Largest:"
du -sk "$DD"/* 2>/dev/null \
| sort -rn \
| head -10 \
| awk '{ kb=$1; $1=""; printf "%7.1f GB %s\n", kb/1048576, substr($0, 2) }'
That is already enough to see what grew in one command.
The full script adds: a per-project breakdown via WorkspacePath from info.plist (details mode), a confirmation prompt and AppleScript-driven wait for Xcode to quit (clean mode), a guard that compares the target against a list of protected paths before rm -rf, and a shared human_size helper for readable output.
The Freed figure in the full version is the difference between measured directory sizes, not necessarily the change in available APFS space.
Installation
Save the script below as xcode-dd, then run:
chmod +x xcode-dd
mkdir -p ~/.local/bin
mv xcode-dd ~/.local/bin/
If ~/.local/bin is not on your PATH, add this to ~/.zshrc:
export PATH="$HOME/.local/bin:$PATH"
Open a new Terminal window and run xcode-dd, xcode-dd details, or xcode-dd clean. Use XCODE_DD_PATH if DerivedData is stored elsewhere.
Full source
Test
cleanon a disposable directory before using it with real project data. The script was not executed on macOS as part of preparing this article.
Expand the full xcode-dd source
#!/bin/zsh
# xcode-dd: inspect and clean Xcode DerivedData.
# Usage: xcode-dd [status|details|clean]
# XCODE_DD_PATH overrides the default path.
set -u
DEFAULT_DD="$HOME/Library/Developer/Xcode/DerivedData"
DD="${XCODE_DD_PATH:-$DEFAULT_DD}"
FORBIDDEN_PATHS=("/" "/Users" "${HOME:A}" "${HOME:A}/Library" "${HOME:A}/Library/Developer" "${HOME:A}/Library/Developer/Xcode")
human_size() {
local kb="${1:-0}"
awk -v kb="$kb" 'BEGIN { if (kb >= 1048576) printf "%.1f GB", kb / 1048576; else if (kb >= 1024) printf "%.1f MB", kb / 1024; else printf "%.0f KB", kb }'
}
directory_size_kb() {
local target="$1"
[[ -e "$target" ]] || { echo 0; return; }
du -sk "$target" 2>/dev/null | awk -F '\t' '{print $1}'
}
scan_dd() {
SCAN_ENTRIES=()
SCAN_TOTAL_KB=0
local -a top_level
top_level=("$DD"/*(ND))
(( ${#top_level[@]} )) || return 0
local kb name
while IFS=$'\t' read -r kb name; do
[[ -n "$kb" ]] || continue
SCAN_ENTRIES+=("$kb"$'\t'"${name:t}")
(( SCAN_TOTAL_KB += kb ))
done < <(du -sk -- "${top_level[@]}" 2>/dev/null)
}
sorted_scan_entries() {
local entry
for entry in "${SCAN_ENTRIES[@]}"; do
print -r -- "$entry"
done | sort -t $'\t' -k1,1nr
}
show_status() {
[[ -d "$DD" ]] || { echo "DerivedData directory does not exist: $DD"; return 0; }
scan_dd
print_status_from_scan
}
print_status_from_scan() {
local disk_info total_kb free_kb
disk_info=$(df -k "$DD" | tail -1)
total_kb=$(echo "$disk_info" | awk '{print $2}')
free_kb=$(echo "$disk_info" | awk '{print $4}')
echo "Xcode DerivedData"
echo
printf "%-14s %s\n" "Path:" "$DD"
printf "%-14s %s\n" "Total:" "$(human_size "$SCAN_TOTAL_KB")"
printf "%-14s %s\n" "Disk:" "$(human_size "$total_kb")"
printf "%-14s %s\n" "Free:" "$(human_size "$free_kb")"
if (( total_kb > 0 )); then
awk -v dd="$SCAN_TOTAL_KB" -v total="$total_kb" 'BEGIN { printf "%-14s %.1f%%\n", "Of disk:", dd * 100 / total }'
fi
if (( free_kb > 0 )); then
awk -v dd="$SCAN_TOTAL_KB" -v free="$free_kb" 'BEGIN { printf "%-14s %.1f%%\n", "Of free:", dd * 100 / free }'
fi
echo
echo "Largest:"
(( ${#SCAN_ENTRIES[@]} )) || { echo " No DerivedData directories."; return 0; }
sorted_scan_entries | while IFS=$'\t' read -r kb name; do
printf "%12s %s\n" "$(human_size "$kb")" "$name"
done
}
workspace_path_for() {
local plist="$1/info.plist"
[[ -f "$plist" ]] || return 1
/usr/libexec/PlistBuddy -c "Print :WorkspacePath" "$plist" 2>/dev/null
}
show_details() {
[[ -d "$DD" ]] || { echo "DerivedData directory does not exist: $DD"; return 0; }
scan_dd
(( ${#SCAN_ENTRIES[@]} )) || { echo "No DerivedData directories."; return 0; }
sorted_scan_entries | while IFS=$'\t' read -r kb name; do
local dir="$DD/$name"
echo
printf "%-45s %s\n" "$name" "$(human_size "$kb")"
if [[ -d "$dir" ]]; then
local workspace=""
workspace=$(workspace_path_for "$dir")
[[ -z "$workspace" ]] || printf " %-12s %s\n" "Workspace" "$workspace"
local -a components=(Build Index.noindex SourcePackages Logs)
local component="" component_path="" component_kb=""
for component in "${components[@]}"; do
component_path="$dir/$component"
if [[ -e "$component_path" ]]; then
component_kb=$(directory_size_kb "$component_path")
printf " %-25s %s\n" "$component" "$(human_size "$component_kb")"
fi
done
fi
done
}
wait_for_xcode_to_quit() {
local timeout=30 elapsed=0
pgrep -x Xcode >/dev/null || return 0
echo
echo "Xcode is running. Asking it to quit..."
osascript -e 'tell application "Xcode" to quit' >/dev/null || return 1
while pgrep -x Xcode >/dev/null; do
sleep 1
(( elapsed++ ))
if (( elapsed >= timeout )); then
echo "Xcode did not quit after ${timeout}s. Nothing was deleted."
return 1
fi
done
echo "Xcode closed."
}
assert_safe_dd_path() {
local resolved="${DD:A}" forbidden
if [[ -z "$resolved" || "$resolved" == "/" ]]; then
echo "Refusing to clean unsafe path: ${resolved:-${DD:-<empty>}}"
return 1
fi
for forbidden in "${FORBIDDEN_PATHS[@]}"; do
if [[ "$resolved" == "$forbidden" ]]; then
echo "Refusing to clean protected path: $resolved"
return 1
fi
done
}
clean_derived_data() {
[[ -d "$DD" ]] || { echo "DerivedData directory does not exist: $DD"; return 0; }
assert_safe_dd_path || return 1
scan_dd
local before=$SCAN_TOTAL_KB
print_status_from_scan
echo
echo "This will delete the CONTENTS of:"
echo "${DD:A}"
echo
printf "Potential cleanup: %s\n" "$(human_size "$before")"
echo
local answer=""
read "answer?Delete all DerivedData? [y/N] "
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Cancelled."; return 0; }
wait_for_xcode_to_quit || return 1
assert_safe_dd_path || return 1
echo
echo "Deleting DerivedData..."
rm -rf -- "$DD"/*(ND) || return 1
local after freed
after=$(directory_size_kb "$DD")
freed=$(( before - after ))
(( freed < 0 )) && freed=0
echo
printf "%-10s %s\n" "Before:" "$(human_size "$before")"
printf "%-10s %s\n" "After:" "$(human_size "$after")"
printf "%-10s %s\n" "Freed:" "$(human_size "$freed")"
}
case "${1:-status}" in
status) show_status ;;
details) show_details ;;
clean) clean_derived_data ;;
*) echo "Usage: xcode-dd [status|details|clean]"; exit 1 ;;
esac
Git worktrees and CI
Coding agents increasingly create temporary Git worktrees. For example:
/Users/eugene/Test-ios
/Users/eugene/.codex/worktrees/5198/Test-ios
If a checkout was opened or built in Xcode, related DerivedData may remain after the worktree is removed. Not every worktree necessarily gets its own DerivedData directory; WorkspacePath helps establish the association.
On CI, you can specify the location explicitly:
xcodebuild \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-derivedDataPath /tmp/MyApp-DerivedData \
build
Whether you delete that directory after each job or cache it for subsequent builds depends on your reproducibility and performance requirements.
Final thoughts
DerivedData is large because Xcode uses it to avoid doing the same work repeatedly. Before clearing it, find out whether the space is going to build products, package dependencies, indexing, or shared caches. For a one-shot check, du is enough; for a single project, Xcode 27’s Product → Delete Derived Data is the shortest path; for a recurring overview, pick a free third-party tool or your own xcode-dd. Save the full wipe for when it is actually needed.