skarol.dev

girl who code • mostly iOS  👩🏼‍💻

Where did my disk go? Xcode, worktrees and AI agents

12 Oct 2026

A few weeks ago my builds started failing in strange ways. A linker step died. Then lipo. Then the module cache complained that a file "has been modified since the module file" was built. Clean builds didn't help.

The disk was full. Nothing in those errors said so.

This happens a lot more once AI agents are part of your workflow. I run two or three agents at once, each in its own git worktree, each building the app and running tests. Each worktree also gets its own copies of the simulators. Agents sharing one simulator would install over each other's builds and tap around in each other's screens. Every build, test run and simulator copy leaves gigabytes behind, and none of it cleans up after itself.

TL;DR

  1. When a build fails strangely, run df -h /System/Volumes/Data first.
  2. Trust df, not du: clones make du overstate everything.
  3. Clean up a worktree's DerivedData, test products and simulators when you remove it, with a skill, a hook or both.
  4. Point every build of the same worktree at the same DerivedData.
  5. When space is tight, delete whole regenerable folders, not pieces of them.
  6. Every now and then, open mo analyze and look.

Xcode won't tell you the disk is full

When the disk fills up mid-build, the error you see is rarely "No space left on device". The ones I hit:

  • Linker or lipo failures. These do mention "No space left on device", but buried in a long log.
  • Module cache errors like "has been modified since the module file", left behind by a half-written cache.
  • A wall of build-system errors. One build printed 23 lines of "unexpected incomplete target" and, at the very end, the real cause: database or disk is full.

So the first rule: when a build fails in a way that makes no sense, check the disk before anything else. And read the last error, not the first.

Measure the right thing

Checking free space has two traps of its own.

df -h / lies. On modern macOS, / is the sealed, read-only system volume. One day mine reported 5.2 GB free while the volume where my files actually live was 99% full. Check the data volume instead:

df -h /System/Volumes/Data

du double-counts. Xcode and the simulator copy files with APFS clones, which share their storage until one copy changes. du counts each copy at full size. I once deleted a folder du reported as 3 GB and got about 90 MB back, because DerivedData still held the same blocks. A built worktree of the app I work on looked like twice its real size in du.

So judge any cleanup by how much df changes, not by what du said the folder weighed.

Where the space goes

How much space each of these takes depends on your app: its size, its packages and how many tests you run. What doesn't depend on your app is what grows. All of these live under ~/Library/Developer/:

WhatWhereGrows with
Agent test runsXcodeBuildMCP/workspaces/every test run
Deleted worktreesXcode/DerivedData/every worktree you remove
Cloned simulatorsCoreSimulator/Devices/every clone and its apps
Module cachesXcode/DerivedData/ModuleCache.noindexXcode and SDK updates

For scale, I work on a modular UIKit app of about 300,000 lines of Swift in 70 packages. For me, one test run left 6–7 GB, a built worktree cost 25–30 GB and a cloned simulator about 3 GB. Yours might be a tenth of that, or more.

The first row surprised me most. XcodeBuildMCP, which my agents use to build and test, keeps a full copy of the test products for every test run, outside DerivedData, so Xcode's clean doesn't touch it. One afternoon of UI and snapshot tests left 96 GB there and filled the disk twice.

The second row is the slow leak. Every worktree you delete leaves its DerivedData folder behind, and if you also build it with plain xcodebuild, that's a second folder. I found 65 orphaned folders. You can tell them apart because a live folder has an info.plist whose WorkspacePath points at a project that still exists. An orphan's either has no info.plist or points at a path that's gone.

To see your own numbers, list the biggest folders in each place:

cd ~/Library/Developer
du -sh XcodeBuildMCP/workspaces/* Xcode/DerivedData/* CoreSimulator/Devices/* \
  2>/dev/null | sort -h | tail -15

du is fine for finding the big ones. Just don't expect deleting them to free exactly that much (more on that above).

Clean up when the worktree goes

The best moment to clean up is when you remove a worktree. You know exactly what belonged to it, so nothing has to be guessed later:

  1. The worktree and its branch.
  2. Its DerivedData folders: every folder whose info.plist has a WorkspacePath inside the worktree.
  3. Its XcodeBuildMCP workspace, named after the worktree directory.
  4. Its simulators. Name your clones after the worktree (I use wt-<ticket>-<device>) and they're easy to find.
  5. Anything else you keep per worktree, like screenshots or test diffs.

I wrote this down as an agent skill: a cleanup-worktree instruction file the agent follows after a PR is merged. It checks that the PR really merged and nothing is left uncommitted, removes everything above and reports how much space came back. It also sweeps orphans from worktrees deleted earlier, but asks before deleting those, because matching them is guesswork.

If you'd rather not depend on remembering to ask, make it automatic. I manage worktrees with worktrunk, which runs hooks when a worktree is removed. A user-level hook could clean the Xcode leftovers every time:

# ~/.config/worktrunk/config.toml
[post-remove]
xcode-leftovers = """
cd ~/Library/Developer
# XcodeBuildMCP names workspaces <worktree>-<12-character hash>
for dir in XcodeBuildMCP/workspaces/*; do
  case "${dir##*/}" in
    {{ worktree_name }}-????????????) rm -rf "$dir" ;;
  esac
done
for plist in Xcode/DerivedData/*/info.plist; do
  case "$(plutil -extract WorkspacePath raw "$plist" 2>/dev/null)" in
    {{ worktree_path }}/*) rm -rf "${plist%/info.plist}" ;;
  esac
done
"""

The skill and the hook complement each other: the hook makes sure the obvious things go every time, and the skill handles the judgement calls, like whether the branch is really merged.

Stop making new copies

Cleaning up is half of it. The other half is not creating duplicates in the first place.

Share one DerivedData per worktree. My agents build through XcodeBuildMCP, which uses its own DerivedData path. But some scripts called xcodebuild directly, with the default path, so the same worktree got built twice in two places. Now every direct xcodebuild call passes the same -derivedDataPath as the MCP. The second build compiles nothing.

Keep your own Xcode separate. I deliberately left builds from the Xcode app in the default DerivedData. That way, opening a worktree in Xcode never fights an agent's build over the same folder.

Delete whole trees, not pieces. Because of the clones, deleting test products alone frees almost nothing. When the disk is really full, what actually helped was:

  • the module caches (ModuleCache.noindex and SDKExplicitPrecompiledModules), which come back immediately and are rebuilt on the next build;
  • the whole XcodeBuildMCP workspace of a worktree that no longer exists;
  • simulator clones nobody uses.

Look before you clean

Before any of this, it's worth seeing for yourself where the space actually went. Mole (brew install mole) is a free, open-source Mac cleanup tool with a very good disk explorer:

mo analyze

It shows your disk as a tree you can walk through, largest folders first, which is the quickest way to see where the space actually went before you delete anything. It also has mo clean and mo purge for caches and build artifacts, both with --dry-run so you can see what they'd remove first.

I'd run it by hand, though, not give it to an agent. The point is for you to understand where the space goes, and then to automate the parts you understand.

None of this is hard, but none of it happens on its own. Look once with mo analyze to see where your space goes, then let a hook or a skill clean up after every worktree, and you can stop thinking about it.