How do I do an XCTestDevices cleanup on Mac safely?

An XCTestDevices cleanup on Mac: where the per-test-run simulator clones hide, why xcrun simctl never sees them, and the safe Trash-first sweep that reclaims tens of GB.

8 min read · Published · Updated · Saad Belfqih

A friend opened Storage on a 1 TB MacBook Pro last week, found 84 GB of "System Data" he could not name, and ran a du -sh ~/Library/Developer/* while we were on a call. The first row back was XCTestDevices at 47 GB. He had never opened the folder. He had never typed the name. He runs xcodebuild test from a Makefile two or three times a week and the folder had been quietly stacking simulator clones for the last nine months. The pattern is the same one Jonathan Spooner described in his Xcode cleanup writeup: "The Cleanup Script I Wish I'd Written Years Ago." Even iOS pros end up writing personal scripts because Xcode never cleans itself, and XCTestDevices is the folder it forgets the hardest.

TL;DR
A safe XCTestDevices cleanup on Mac means auditing `~/Library/Developer/XCTestDevices/`, using `xcrun simctl --set` to point simctl at that device set, and moving stale `.xctestdevice` bundles to the Trash with Xcode quit. CleanMyDev maps the XCTestDevices folder alongside CoreSimulator, DerivedData, iOS DeviceSupport, Archives, and SPM caches with per-row receipts, last-used dates, and a one-click Move to Trash, so a 47 GB folder you never knew existed becomes a checkbox rather than a script.

What is the XCTestDevices folder on Mac?

~/Library/Developer/XCTestDevices/ is a second CoreSimulator device set that Xcode uses for test-time simulators. It exists alongside, but separate from, the named simulator set you manage from Xcode's Devices and Simulators window. Both sets share the same on-disk layout, both reference the same runtime images, but Xcode treats them as two different worlds.

When you click the Test diamond, run xcodebuild test, or run a Test Plan in CI, Xcode creates one or more throwaway .xctestdevice bundles inside this folder, boots them, runs the tests, and shuts them down. The bundles are supposed to be ephemeral. In practice Xcode keeps them around so the next run is faster.

The folder contains:

Each .xctestdevice bundle is a full simulator image. It contains a data directory with the simulator's Library, Documents, and per-app sandboxes, plus a device.plist that ties it to a runtime. On a recent iOS 18 setup a single bundle clocks in around 1.1 GB before any test even runs, because the runtime templates a working data tree on first boot.

Why does XCTestDevices get so large on a developer Mac?

Four reasons stack up on a Mac that runs Xcode tests regularly.

First, the folder never garbage-collects on its own. Xcode adds a .xctestdevice for any combination of runtime, device type, and destination it has not seen before. Last year's iPhone 15 Pro on iOS 17.4, this year's iPhone 16 Pro on iOS 18.2, and the iPhone SE 3rd gen you tested once on a bug report all stay forever.

Second, CI-style local runs multiply the bundles. A Makefile that runs the same test plan against three destinations creates three bundles. A Bitrise migration test that walks five iOS versions creates five.

Third, snapshot and UI tests pin extra simulator state. UI tests that record video, snapshot tests that store reference images inside the simulator's sandbox, and accessibility audits that capture trace logs all add to the per-bundle weight. A bundle that started at 1.1 GB can grow to 2.5 GB after a few hundred runs.

Fourth, swift package tests with UIKit dependencies also create XCTestDevices entries. Even a small SPM library that has a single XCUIApplication-touching test target will spin up a bundle on the host machine.

The table below is what the XCTestDevices footprint looks like next to the other Xcode storage offenders on a Mac with a year of mixed iOS work.

Folder Default Mac location Audit command Typical 12-month size
XCTestDevices ~/Library/Developer/XCTestDevices du -sh ~/Library/Developer/XCTestDevices 20 to 60 GB
CoreSimulator devices ~/Library/Developer/CoreSimulator/Devices du -sh ~/Library/Developer/CoreSimulator/Devices 40 to 120 GB
CoreSimulator caches ~/Library/Developer/CoreSimulator/Caches du -sh ~/Library/Developer/CoreSimulator/Caches 8 to 25 GB
iOS DeviceSupport ~/Library/Developer/Xcode/iOS DeviceSupport du -sh ~/Library/Developer/Xcode/iOS\ DeviceSupport 10 to 40 GB
DerivedData ~/Library/Developer/Xcode/DerivedData du -sh ~/Library/Developer/Xcode/DerivedData 15 to 80 GB
Archives ~/Library/Developer/Xcode/Archives du -sh ~/Library/Developer/Xcode/Archives 5 to 30 GB

Add the rows and a year-old Mac with one or two active iOS projects can easily sit at 100 to 250 GB just under ~/Library/Developer/. XCTestDevices is rarely the largest single offender, but it is consistently the most opaque, because nothing in Xcode's UI names it.

How do I audit XCTestDevices safely?

Three steps. Measure the parent folder, list each bundle with its size and last-modified date, then ask simctl to enumerate the set so you can map bundles to runtimes.

# 1. Honest size of the entire XCTestDevices device set
du -sh ~/Library/Developer/XCTestDevices

# 2. Per-bundle breakdown, biggest first, with timestamps
ls -lt ~/Library/Developer/XCTestDevices/*.xctestdevice 2>/dev/null
du -sh ~/Library/Developer/XCTestDevices/*.xctestdevice 2>/dev/null \
  | sort -hr \
  | head -20

# 3. Catalogue with runtime + device type via simctl
xcrun simctl --set ~/Library/Developer/XCTestDevices list devices

The third command is the one that turns an opaque UUID into a readable label like iPhone 15 Pro (iOS 17.4). Hold the output before you delete anything, because it tells you which bundles still match a runtime you have installed and which point at runtimes Xcode already removed.

For the wider Xcode storage audit beyond this single folder, see reclaim-disk-from-xcode-simulators and core-simulator-caches-mac.

Why doesn't xcrun simctl delete unavailable clean XCTestDevices?

Because simctl defaults to one device set, and XCTestDevices is the other one.

xcrun simctl reads ~/Library/Developer/CoreSimulator/Devices/ unless you tell it otherwise. The XCTestDevices set lives at ~/Library/Developer/XCTestDevices/, which is the same on-disk format but a separate path. None of the standard simctl delete unavailable, simctl erase all, or simctl shutdown all commands ever touches the XCTestDevices folder unless you pass --set explicitly.

The flag is documented but easy to miss. It is also the safest CLI path, because it lets simctl do the bookkeeping instead of you reaching in with rm -rf.

# Apply simctl to the XCTestDevices set explicitly
xcrun simctl --set ~/Library/Developer/XCTestDevices list devices
xcrun simctl --set ~/Library/Developer/XCTestDevices delete unavailable

# Erase every test simulator without removing the bundles
xcrun simctl --set ~/Library/Developer/XCTestDevices shutdown all
xcrun simctl --set ~/Library/Developer/XCTestDevices erase all

The delete unavailable call is the closest match to what most teams actually want. It removes only the bundles whose runtime is no longer installed, so it never deletes a simulator a future test run would have re-used. The same gap exists for stray runtimes the simctl tooling cannot reach, covered in orphaned-simulator-runtimes-mac.

How do I move XCTestDevices to Trash instead?

For a developer Mac, the safer mirror of simctl delete is a timestamped move of the whole folder into ~/.Trash. That keeps everything reversible from Finder, gives macOS its standard cleanup window, and lets you drag the folder back if a forgotten test plan turns out to need a specific simulator.

# 1. Quit Xcode and Simulator first
osascript -e 'quit app "Xcode"'
osascript -e 'quit app "Simulator"'

# 2. Confirm the folder size one more time
du -sh ~/Library/Developer/XCTestDevices

# 3. Timestamped Trash move, never rm -rf
STAMP=$(date +%Y%m%d-%H%M%S)
mv ~/Library/Developer/XCTestDevices "$HOME/.Trash/XCTestDevices-$STAMP"

# 4. Re-create on the next test run
cd ~/code/my-ios-app
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16'

Quitting Xcode and Simulator first matters more here than for DerivedData. The XCTestDevices folder hosts live simulators if a test is mid-run, and mv against a busy CoreSimulator device set can leave a half-attached image that Xcode then refuses to boot. The two osascript quits cost a second each and make the move clean.

mv is an APFS metadata operation on the boot volume, so even a 50 GB folder moves in well under a second. The timestamp matters because two cleanups in the same week would otherwise collide on the Trash name.

When should I not clean XCTestDevices?

If you are mid-investigation on a flaky UI test that only repros on a specific simulator, hold the cleanup until after the bug is filed and the trace is exported. The simulator's data directory may contain a database, defaults plist, or screenshot the next test run will not regenerate.

If your CI provisioning script seeds the XCTestDevices set with specific runtimes ahead of time, run xcrun simctl --set ~/Library/Developer/XCTestDevices list devices first and confirm the seeded bundles are not the ones you are about to delete. The same caveat applies to teams that share a simulator snapshot fixture for snapshot testing libraries.

If you are inside a UI test recording session with the Simulator window open, close it first. A live Simulator process holds file handles inside the bundle, and moving the parent folder leaves macOS confused enough that the simulator will not relaunch without a killall com.apple.CoreSimulator.CoreSimulatorService chaser.

The wider safety frame for moves like this is in is-deleting-deriveddata-safe, and the same Trash-first floor applies.

Cleanup playbook in order

  1. Quit Xcode and the Simulator app.
  2. Run du -sh ~/Library/Developer/XCTestDevices and xcrun simctl --set ~/Library/Developer/XCTestDevices list devices to see what is in there.
  3. Move ~/Library/Developer/XCTestDevices to Trash with a timestamp. Reclaims 20 to 60 GB on a year-old Mac.
  4. Run any test in the project to confirm Xcode rebuilds the set cleanly.
  5. Add xcrun simctl --set ~/Library/Developer/XCTestDevices delete unavailable to your monthly maintenance script.
  6. Sweep neighbouring folders the same way: DerivedData, Archives, iOS DeviceSupport, CoreSimulator caches.

Why use CleanMyDev for this instead?

The terminal flow above is honest, reversible, and free. It also asks you to remember the --set flag, quit Xcode before the move, name the Trash bundle with a timestamp, and re-audit every month.

CleanMyDev maps ~/Library/Developer/XCTestDevices/ next to CoreSimulator devices, CoreSimulator caches, iOS DeviceSupport, XCTestDevices, DerivedData, Archives, and SPM. Every row shows the path, the size, the last-modified date, the runtime label where it has one, and a risk label. Tick the bundles you want, hit Move to Trash, receipts stay in Finder for the standard cleanup window. No subscription, $9.99 lifetime, Move to Trash by default. See the pricing on the homepage.

Related reading

Stop wondering what System Data is.

CleanMyDev opens the box. 110+ developer-specific cleanup targets. Move-to-Trash by default. $9.99 lifetime.

Get CleanMyDev — $9.99