Apple shipped simctl in 2014, alongside the iOS Simulator's first real CLI. The tool was designed for one developer on one Mac, building one app, on one simulator, in one place. That's still a perfectly reasonable workflow. It's also no longer what most iOS teams do.
If you've ever:
Watched a CI job fail because a hardcoded UDID didn't exist on the runner image.
Had to update three scripts the day Xcode 16 rotated a runtime identifier.
Sat through a 5-second
xcruncall to figure out which simulator was booted.Tried to teach an AI agent to find a specific simulator without giving it your shell history.
Spent half a day reconciling someone else's
setup-simulator.shwith your machine.
…you've hit the friction this page is about. It compounds.
The problem with treating simulators as UDIDs
A UDID is a hash. It's unique per simulator per machine. The same "iPhone 16" running on your laptop and on your CI runner has two different UDIDs. The same "iPhone 16" before and after you reinstall Xcode has two different UDIDs. The same "iPhone 16" simulator on the same machine, after a simctl erase, keeps its UDID, unless someone deleted and recreated it instead, in which case it doesn't.
For scripts that move between machines, hardcoding UDIDs is unworkable. So every team eventually writes a wrapper. The wrapper inevitably:
Calls
xcrun simctl list -jto get the device list.Walks the deeply nested JSON to find the runtime you want.
Filters by name within that runtime.
Plucks the UDID.
Hopes only one matched.
Five lines of plumbing for what should be one command. Worse, the wrapper breaks the next time Apple changes the JSON shape (it has, more than once), and it silently picks the wrong simulator the day someone adds a second iPhone 16 Pro on a different runtime.
The wrapper is also written in every team's slightly different way. The author leaves; nobody else trusts it; it accumulates "just in case" branches; the day it finally breaks is the day before a release.
This isn't a tooling problem. It's a category problem. simctl was built for a developer; teams that ship at scale are using it as infrastructure. The tool isn't wrong. The job description has changed.
What changes when names are first-class
The shift is small, in code:
The shift is large, in practice. The script no longer cares about UDIDs. It no longer breaks across machines. It no longer breaks when Apple rotates runtime identifiers. The tool resolves the name at call time using rules you can predict:
If the argument looks like a UDID, use it as-is.
Otherwise, list only the simulators marked available.
Try a case-insensitive exact match on name. If exactly one matches, use it. If multiple match, prefer one that's already booted; otherwise, the first.
If no exact match, fall back to a case-insensitive substring match. If exactly one simulator's name contains the argument, use it. If multiple match, the tool refuses to guess and asks you to disambiguate.
You don't have to read the rules to use the tool. But if you read them once, your scripts get more deliberate. Predictable is the operative word. The tool will boot the wrong simulator if you ask it to (UDID match wins, even if that UDID is on a runtime you didn't expect), but it won't pick one when the name is ambiguous, that path is an explicit error, not a silent coin flip.
What this unlocks
For solo developers, name-based targeting is a minor convenience. For everyone else, it's structural:
CI matrices stop being a maintenance project.
A YAML job that runs flowdeck simulator boot "iPhone 16" works on every runner that has an iPhone 16 simulator installed. The job doesn't know or care about that machine's UDIDs.
Onboarding stops requiring a "fix my simulator" Slack thread.
The new engineer's local setup runs the same script as CI. If the script works in CI, it works on their laptop. Three days of "why doesn't this work for me" disappear.
AI agents get a sane targeting surface.
You can tell Claude Code or Codex to "boot iPhone 16" the way you'd tell a colleague. The agent doesn't need to learn shell-history-flavored UDID extraction.
Teams sharing scripts stop coordinating on UDIDs.
Open source iOS tooling that targets simulators can be portable without elaborate setup steps.
If you've ever wished simctl let you pass --by-name, this is the version of that idea where the wrapper is doing it on every operation, not just boot.
Evaluating a simulator-management tool
If you're considering swapping in a wrapper, FlowDeck or another, the criteria that matter aren't features. They're behaviors. Here's a short list to argue from, whether you're making the case internally or evaluating for yourself.
Predictability when things are ambiguous.
What happens when two simulators share a name? Pick a tool that documents its tie-breaking rules. "Just works" is not a behavior; it's an absence of one.
Composition with simctl.
A wrapper that replaces simctl entirely is a liability. The right wrapper composes: it accepts and returns the same UDIDs simctl uses, so you can drop down to simctl for the long tail (privacy grants, push, status bars).
JSON output on every operation.simctl supports --json only on list. A useful wrapper supports it on every operation, with a flat schema jq can work with on the first try.
Runtime install by version.
Installing iOS 18.0 should not require finding a .dmg URL. The tool should know how to download from Apple's catalog and tell you the install progress.
Bulk cleanup operations.
Simulators accumulate. Operations like "delete the simulators whose runtime is no longer installed" and "delete simulators that were created but never booted" should be one command each, not a shell loop with three filter steps.
Non-interactive everything.
Every command should be usable from a CI script without a terminal attached. No interactive prompts, no Xcode dialogs, no "press enter to continue."
When simctl is still the right call
FlowDeck doesn't replace simctl, and a tool that tried to would be making the wrong bet. Some operations are surface-level wrappers around Apple capabilities that are not worth abstracting. Use simctl directly for:
Privacy permission grants.
simctl privacygrants and revokes camera, location, contacts, and other privacy settings. Required for tests that need those permissions pre-granted.Push notification simulation.
simctl push <udid> <bundle-id> payload.apnsfor testing remote-notification handling.Status bar overrides.
simctl status_barfor marketing screenshots with a hand-set time and signal. Visual polish, not testing.Keychain manipulation.
simctl keychainfor resetting or pre-seeding keychain state across runs.Apple Watch pairing.
simctl pairfor watchOS testing topologies.
FlowDeck composes with simctl. The UDIDs FlowDeck returns are the UDIDs simctl expects. Nothing prevents you from using both in the same script. The design assumption is that if Apple's tool already does the job cleanly, the right thing to do is leave it alone.
Questions teams actually ask
We already have a simctl wrapper that mostly works. Why switch?
Is this another abstraction layer we’ll have to learn?
What happens on a CI image that has multiple “iPhone 16” simulators?
How does this play with Xcode Cloud or Fastlane?
Does this work for tvOS, watchOS, and visionOS simulators?
What does name-based targeting actually pick when there’s no perfect match?
How is this priced, and is it worth it for a small team?
