| name | simulator-automation |
| description | Run-and-observe loop for iOS/iPadOS apps on the iOS Simulator and real devices (iOS 26 / Swift 6 / Xcode 26): build, boot, install, launch, drive the UI, screenshot, logs, debug. Picks the driver (XcodeBuildMCP, xcrun simctl, xcodebuild, devicectl, idb, AXe, Maestro, XCUITest, WebDriverAgent), environment-first diagnostics, accessibility-tree UI interaction, visual regression, leak hunting, simulator disk cleanup. Use when: "run the app", "build and run", "boot a simulator", "take a screenshot", "tap/type in the UI", "simulate a deeplink/push/location", "why won't the simulator boot", "BUILD FAILED" — any make-it-run-on-sim/device request. NOT for feature code or signing/provisioning. |
Simulator Automation
The agent's loop for making an iOS app actually run and behave on the
Simulator (or a connected device) — discover → boot → build → install → launch →
inspect → interact → verify → capture evidence. This is the difference between
"it compiles" and "I watched the login button work."
Core principle (read this first): ~80% of "mysterious" build/run failures are
environment problems — stale DerivedData, a stuck simulator, zombie
xcodebuild processes, a poisoned SPM cache — not code bugs. Before you debug
code, run the environment-first checklist.
It saves 30-minute rabbit holes.
Step 0 — Pick your driver
Pick ONE per session and stay on it; mixing them causes confusion.
| Driver | Use when | Strengths | Cost |
|---|
XcodeBuildMCP (mcp__XcodeBuildMCP__*) + ios-simulator-mcp | The MCP servers are installed (see .mcp.json below) | One tool per action, session defaults, structured UI tree, built-in screenshot/log/gesture | Needs npx + the MCP servers running |
Native CLI (xcrun simctl, xcodebuild, xcrun devicectl, idb) | No MCP server, CI, or you want zero extra dependencies | Always present with Xcode, fully scriptable, identical on local + CI, no version drift | No session state — you pass UDID/scheme every call |
AXe (axe …, references/axe-reference.md) | No MCP, but you want selector taps + named gestures + video on the sim | Tap by accessibility --id/--label (survives layout shifts), slider self-verifies, batch flows, built-in video — the modern successor to idb | brew install cameroncooke/axe/axe; sim only |
| Maestro (references/maestro-reference.md) | You want a durable declarative UI flow without a test target, auto-waiting through animations | YAML flows, auto-wait+retry, scrollUntilVisible, conditional/looping flows, cross-platform | Needs Java 17 + the Maestro CLI; runs flows, not interactive pokes |
| XCUITest (in-project UI test target) | You need robust, repeatable UI flows that run in the test runner & CI | Native accessibility queries, waitForExistence, tvOS focus testing | Requires a UI-test target; slower than poke-the-pixels |
| WebDriverAgent (references/webdriveragent.md) | UI interaction on a physical iPhone/iPad (devicectl can't tap/swipe/type) | One server, same action vocabulary on sim and device; what Appium wraps | Build+run a runner; device-XCUITest is flaky on iOS 26 |
Default recommendation: if XcodeBuildMCP is available, use it for the
interactive loop (describe_ui → tap-by-id is the fastest reliable path). No MCP?
On the simulator prefer AXe when elements are labeled (selector taps beat
idb's coordinate taps), or plain native CLI for zero deps. Use Maestro for a
durable declarative flow without a test target; XCUITest when the flow must run
in the test runner; WebDriverAgent for UI on a real device.
Decision flow:
Is XcodeBuildMCP installed? ── yes ──▶ MCP loop (Step 2A)
│ no
▼
Target a PHYSICAL device's UI? ── yes ──▶ WebDriverAgent (references/webdriveragent.md)
│ no (simulator)
▼
Need a durable, re-runnable UI flow? ── yes ──▶ XCUITest (references/xcuitest.md)
│ or Maestro (references/maestro-reference.md)
│ no
▼
Elements labeled? ── yes ──▶ AXe selector taps (references/axe-reference.md)
│ no
▼
Native CLI loop (Step 2B) — works everywhere, zero deps
.mcp.json to enable the MCP path
{
"mcpServers": {
"XcodeBuildMCP": {
"command": "npx",
"args": ["-y", "xcodebuildmcp@latest", "mcp"],
"env": { "XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,ui-automation,debugging,logging" }
},
"ios-simulator": { "command": "npx", "args": ["-y", "ios-simulator-mcp"] }
}
}
XCODEBUILDMCP_ENABLED_WORKFLOWS trims the tool surface to what you need (fewer
tools = less context). Xcode 26 also ships a native MCP bridge — xcrun mcpbridge
— enable it only if you specifically want Xcode-side tooling.
Step 1 — Discover the target (do this every time)
You cannot do anything until you know the UDID and the scheme. Never
hard-code a simulator name — UDIDs are stable, names drift between Xcode releases.
xcrun simctl list devices | grep -i booted
xcrun simctl list devices available --json
UDID=$(xcrun simctl list devices available --json \
| jq -r '.devices[][] | select(.name=="iPhone 17 Pro") | .udid' | head -1)
xcodebuild -workspace App.xcworkspace -list
- Always prefer
.xcworkspace over .xcodeproj when both exist (CocoaPods/SPM
workspaces won't build right from the bare project).
- If there's no Xcode project at all (
Package.swift-first repo), this is a SwiftPM
package — use swift build / swift run <product> / swift test instead, and
don't assume an .app bundle exists.
MCP equivalent: mcp__XcodeBuildMCP__list_sims, then
mcp__XcodeBuildMCP__list_schemes.
Full command catalogs: references/simctl-reference.md (simulator),
references/devicectl-reference.md (physical device).
Environment-first — ALWAYS do this before blaming code
When you see BUILD FAILED with no clear error, tests that "passed yesterday,"
old code running after a change, or a stuck simulator — fix the environment
before reading a single line of source.
ps aux | grep -E '\bxcodebuild\b|Simulator' | grep -v grep
killall xcodebuild 2>/dev/null; killall Simulator 2>/dev/null
rm -rf ~/Library/Developer/Xcode/DerivedData
xcrun simctl shutdown all
xcrun simctl erase all
rm -rf ~/Library/Caches/org.swift.swiftpm
| Symptom | First fix | Time |
|---|
| Stale builds, old code runs | Delete DerivedData | ~2 min |
| "No such module" after SPM change | DerivedData + SPM cache | ~3 min |
| Simulator stuck / "Unable to boot" | simctl shutdown then boot; erase if needed | ~2 min |
10+ zombie xcodebuild procs | killall xcodebuild | ~1 min |
| Intermittent success/failure | Full reset (all of the above) | ~10 min |
The whole point: spending 2–10 minutes on the environment beats 30+ minutes
debugging code that was never broken.
Step 2A — The MCP loop (XcodeBuildMCP)
list_sims → session-set-defaults → build_run_sim → describe_ui → (tap/type/gesture) → screenshot → logs
- Discover & default:
list_sims; if none booted, ask the user to boot one
(or boot per their request). Then session-set-defaults with workspacePath
(or projectPath), scheme, simulatorId, configuration: "Debug",
useLatestOS: true — so you don't repeat them every call.
- Build & run:
build_run_sim. On failure, read the output and retry only
when justified, optionally preferXcodebuild: true.
- Verify launch BEFORE interacting:
describe_ui or screenshot. Never tap
into a screen you haven't confirmed is up.
- Interact (describe → act):
describe_ui to get the accessibility tree.
- Tap by
id or label first; fall back to coordinates only when nothing is
addressable.
type_text after focusing a field.
gesture for scrolls and edge swipes.
- Re-run
describe_ui after any layout change — stale coordinates are the
#1 cause of flaky interaction.
- Logs:
start_sim_log_cap (with the app bundle id; captureConsole: true
for stdout) → reproduce → stop_sim_log_cap → summarize the important lines.
- Launch-only path: if the app is already built,
launch_app_sim. Unknown
bundle id? get_sim_app_path → get_app_bundle_id.
Stop interacting after an unhandled build/launch failure — fix that first.
Step 2B — The native-CLI loop (works everywhere, zero deps)
This is the exact sequence XcodeBuildMCP runs under the hood. Use it in CI or when
no MCP server is present.
UDID="<from Step 1>"; WS="App.xcworkspace"; SCHEME="App"; BUNDLE="com.example.app"
xcrun simctl boot "$UDID" 2>/dev/null || true
open -a Simulator
xcodebuild -workspace "$WS" -scheme "$SCHEME" \
-destination "platform=iOS Simulator,id=$UDID" \
-derivedDataPath /tmp/build -quiet \
build CODE_SIGNING_ALLOWED=NO
APP=$(find /tmp/build/Build/Products/Debug-iphonesimulator -maxdepth 1 -name "*.app" | head -1)
xcrun simctl install "$UDID" "$APP"
xcrun simctl launch --console "$UDID" "$BUNDLE"
xcrun simctl io "$UDID" screenshot /tmp/shot.png
xcrun simctl openurl "$UDID" "myapp://deeplink"
Bundle id unknown? defaults read "$APP/Info.plist" CFBundleIdentifier.
UI interaction without MCP — two options. Prefer AXe when elements are
labeled (taps by accessibility id/label, survives layout shifts); fall back to idb
(coordinate taps) otherwise:
axe describe-ui --udid "$UDID" > tree.json
axe tap --id "login_submit" --udid "$UDID"
axe type "user@example.com" --udid "$UDID"
idb ui describe-all --json --nested > tree.json
idb ui tap 200 400
After launch, poll a launch anchor instead of a blind sleep — re-describe-ui
in a bounded loop until a known first-screen id appears (axe-reference.md). An
empty/tiny a11y tree is a state probe (still launching / crashed / modal / Home
screen), each with a different fix.
Full catalogs: references/simctl-reference.md (boot/install/launch/screenshot/
record/openurl/push/privacy/status-bar/logs/location/GPS/clipboard/a11y-setters/
capability-table), references/idb-reference.md, references/axe-reference.md.
Build settings & flags (iOS 26 / Swift 6 era)
The modern, simulator-correct xcodebuild invocation and the flags that matter:
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-destination "platform=iOS Simulator,id=$UDID" \
-configuration Debug \
-derivedDataPath /tmp/build \
-quiet \
build \
CODE_SIGNING_ALLOWED=NO
Destination forms: id=<UDID> (most reliable) · name=iPhone 17 Pro (drifts) ·
generic/platform=iOS (for archive only). Actions: build, clean build,
test, archive. Scope a test run with -only-testing "AppTests/MyClass".
Sanitizers for race/overflow hunts: -enableThreadSanitizer YES
-enableAddressSanitizer YES.
Swift 6 / iOS 26 build settings that change behavior: SWIFT_VERSION=6,
SWIFT_STRICT_CONCURRENCY=complete (data races become compile errors),
SWIFT_DEFAULT_ACTOR_ISOLATION=MainActor, SWIFT_APPROACHABLE_CONCURRENCY=YES.
Common failures and their real causes — including the Swift 6 concurrency ones —
are tabulated in references/build-troubleshooting.md.
Specialized passes
Reach for these once the app is running. Each has a dedicated reference.
| Pass | What it proves | Reference |
|---|
| UI smoke test | App launches & key screens render without crashing | references/qa-passes.md |
| Visual regression | A change didn't move/break pixels (baseline vs current diff) | references/qa-passes.md |
| Accessibility audit | Interactive elements have labels, 44×44pt targets, traits | references/qa-passes.md |
| Memory-leak hunt | A specific retained type/path disappeared after a fix | references/memory-leaks.md |
| App-state capture | Full bug-report bundle (screenshot + tree + logs + sandbox) | references/qa-passes.md |
Visual / accessibility golden rule (text-before-pixels): capture the
accessibility tree (describe_ui / idb ui describe-all) and act on element
ids/labels. Pixel coordinates are a last resort and the main source of flakiness.
Physical-device mode (when the target is a real iPhone/iPad)
Switch to xcrun devicectl + xcodebuild -destination 'id=...'. The two
identifiers are different and must not be mixed:
- Build/test use the
xcodebuild destination id (from
xcodebuild -showdestinations).
- Install / launch / diagnose use the
devicectl device identifier (from
xcrun devicectl list devices).
Prefer a connected device over one that is only available (paired); treat
unavailable devices as diagnostic targets only. Signing/provisioning failures on
device are out of scope here — those belong to a build-settings / xcode-build
skill. Full device command set and troubleshooting:
references/devicectl-reference.md.
Disk hygiene (when the Mac runs out of space from Xcode)
Simulator runtimes alone can hoard 10–55 GB. Scan by category, show the biggest
items with last-used info, then offer safe deletions. Never rm -rf a simulator
device directly — always use simctl. Full per-category scan + delete commands
(runtimes in both MobileAsset and CoreSimulator locations, device data, DerivedData,
CocoaPods/SPM caches, archives, device support): references/disk-cleanup.md.
Fast wins:
xcrun simctl delete unavailable
xcrun simctl runtime list
xcrun simctl runtime delete 'iOS 18.5'
Hard boundaries
- This skill makes apps run and behave. It does NOT write feature code, design
signing/provisioning, or author pure unit tests.
- Pin one device dimension per task (simulator OR device) and stay there.
- A build that succeeds in a sandbox is not final acceptance — the
authoritative result comes from building in the real project root, and for device
targets, from a connected device.
- Don't report a leak fixed from a "smaller memgraph" alone — prove the specific
type/ownership path disappeared (references/memory-leaks.md).
- Signing/cert/profile/Archive/CI issues → hand off to a build-settings skill.
References
- references/simctl-reference.md — full
xcrun simctl catalog (lifecycle +
bootstatus/CoreSimulatorService recovery, install/launch + terminate-first,
screenshot/video + sips-resize, openurl, push, location/GPS, privacy,
clipboard + addmedia, accessibility setters, status-bar override,
log streaming + crash isolation, env injection, sim capability table).
- references/idb-reference.md —
idb UI automation + HID keycode table (no-MCP
coordinate path; prefer AXe when labeled).
- references/axe-reference.md — AXe HID-level sim driver: selector taps, named
gestures, self-verifying
slider, batch flows, hardware buttons, HID keys,
built-in video, launch-anchor poll, empty-tree disambiguation.
- references/maestro-reference.md — Maestro declarative-YAML UI flows
(auto-wait,
scrollUntilVisible, conditional/looping flows, cross-platform).
- references/webdriveragent.md — physical-device UI via WDA: setup, raw W3C/HTTP
recipes, bundle-id rewrite, stale-session recovery, keep-runner-alive, exit-74 /
DTX failure matrix, Codex-Seatbelt gotcha, device crash-log pull.
- references/devicectl-reference.md — physical-device build/install/launch/
diagnose + destination-id-vs-device-id rule + JSON field map + crash logs.
- references/build-troubleshooting.md —
xcodebuild failure → cause → fix table
(incl. sim-only third-party-framework linker + device-fallback rule), Swift 6
concurrency, environment-first recovery, persistent-DerivedData staleness trap,
idb-from-source macOS 26, agent build-gate hook, sanitizers.
- references/qa-passes.md — UI smoke, visual-regression + overlay approval gate,
accessibility + Dynamic Type passes, localized screenshots, deterministic-capture
harness, best-device auto-pick, structured-log driving, debug URL scheme,
computer-use driving + destructive-flow safety, app-state capture.
- references/memory-leaks.md — memgraph capture/summarize/prove (incl.
--groupByType + macOS 26.x MallocStackLogging abort).
- references/disk-cleanup.md — 8-category scan + runtime cleanup + MUST/FORBIDDEN
table + before/after report.
- references/xcuitest.md — durable XCUITest flows, tvOS
XCUIRemote focus
testing, fastlane snapshot recipe, warmup-run pattern.