Repository navigation
iOS: make the device set configurable so Xcode tools can target a leased simulator #962
Description
Request: #946
Problem
Simlock's iOS simulators live in their own CoreSimulator device set. On Xcode 26 and older, no Xcode tool can target a simulator there by its UDID: xcodebuild -destination "platform=iOS Simulator,id=<udid>", react-native run-ios --udid, Expo run:ios, Detox and Maestro all read Xcode's own set. The only switch is a per-user Xcode default, which leaks into every xcodebuild run of that user and hides their own simulators while set. Xcode 27 takes the set as a per-command argument (-DVTSimulatorSetLocation=<set>), but only tools that pass extra xcodebuild arguments can use it; Expo run:ios cannot. The docs mention none of this. The only workaround they hint at is to build for generic/platform=iOS Simulator and install with simlock simctl install.
Android does not have this problem: adb, Gradle and the React Native CLI honour ANDROID_ADB_SERVER_PORT, which the lease exports.
Who it is for
Coding agents (agent-device first, then the React Native CLI, Expo, Detox and plain xcodebuild) that hold an iOS lease and need to build and run an app on that simulator. Also the operator who sets up the worker and chooses how much isolation to keep.
Outcome
- An operator can set
drivers.ios.deviceSetto"default". A worker set that way keeps its simulators in Xcode's own set. Any Xcode tool then targets a leased simulator by its UDID, with no extra argument and no detour. - With either setting, Simlock's own work (cleanup, the warm pool,
nuke,doctor --purge-orphans, reclaim) acts only on simulators this Simlock created. A user's own simulators in Xcode's set, and another Simlock's, are never listed as Simlock's, wiped or deleted by it. - An agent's
simlock simctlstill refuses the commands that change a device's lifecycle (create,erase,delete,shutdown all,runtime delete) under both settings. Under"default"it can list or install on any simulator in Xcode's set by UDID, as it can reach another agent's device today; scoping it to the lease is simlock simctl and simlock adb on a worker run only against the caller's lease, and fill the device in #948. simlock instructionstells an agent which build route its lease needs.- The default stays
"simlock", so nothing changes for a worker that does not opt in. - A new docs page, "Build and run your app", says how to build onto a leased device for each setting and each platform, and what a careless
xcrun simctl … alldoes to leased devices under"default".
How it works for the user
flowchart TD
A[Operator sets drivers.ios.deviceSet] --> B{Value}
B -->|simlock, or not set| C[Simulators in Simlock's own set, as today]
B -->|default| D[Simulators in Xcode's set]
B -->|anything else| E[iOS off; doctor says invalid-device-set]
D --> F{Simulators left from the other setting?}
C --> F
F -->|yes| G[iOS off; doctor says device-set-changed and the steps]
F -->|no| H[iOS starts]
H --> I[Agent leases a simulator]
I --> J[default: xcodebuild / run-ios --udid works directly]
I --> K[simlock: Xcode 27 argument, or generic build + simctl install]
The operator's session, switching to default:
$ simlock config set drivers.ios.deviceSet default
Updated drivers.ios.deviceSet in /Users/alex/.simlock/config.json; takes effect on daemon restart.
$ simlock daemon stop && simlock daemon start
$ simlock doctor
...
Unavailable [ios] device-set-changed: Refusing the ios device set: Simlock's own set at /Users/alex/.simlock/devices/ios still holds 2 simulators (6F1D2A8C-…, 0B7E…). Set drivers.ios.deviceSet back to simlock, run `simlock nuke --delete-devices` and `simlock doctor --purge-orphans`, then switch again. Delete any that remain with `xcrun simctl --set /Users/alex/.simlock/devices/ios delete <udid>`.After the steps, an agent's session:
$ simlock lease --platform ios --detach
...
$ UDID=6F1D2A8C-3B4E-4F7A-9C2D-8E5B1A7F3C90 # the device id the lease printed
$ xcodebuild -scheme MyApp -destination "platform=iOS Simulator,id=$UDID" build
** BUILD SUCCEEDED **Today (and under simlock on Xcode 26) the same xcodebuild prints Unable to find a device matching the provided destination specifier.
Examples
drivers.ios.deviceSet |
drivers.ios.deviceRoot |
Today | After |
|---|---|---|---|
| not set | not set | own set at ~/.simlock/devices/ios |
the same |
"simlock" |
not set | key ignored, own set | own set |
"simlock" |
/Volumes/sims |
key ignored, own set at /Volumes/sims |
own set at /Volumes/sims |
"default" |
not set | key ignored, own set | Xcode's set |
"default" |
/Volumes/sims |
key ignored | iOS off, invalid-device-set |
"Default" |
not set | key ignored | iOS off, invalid-device-set |
"", true, 1 |
not set | key ignored | iOS off, invalid-device-set |
A simulator in Xcode's set under "default" |
Simlock lists it? | doctor --purge-orphans may delete it? |
|---|---|---|
| this Simlock created it, it has a lease | yes | no (registered) |
| this Simlock created it, no registry record | yes | yes (an orphan) |
another Simlock (another SIMLOCK_HOME) created it |
no | no |
| the user's own, any name | no | no |
the user's own, named simlock-1234 |
no | no |
What could go wrong
- A user's own simulator, or another Simlock's, is treated as this Simlock's and wiped or deleted. Unlikely: Simlock claims only devices carrying the mark it writes, with its own instance id, when it creates one. It would be the worst failure.
- Under
"default", a user runsxcrun simctl shutdown all. Leased simulators stop; Simlock reboots them under their leases and agents lose running apps. One erased while stopped ends its lease. The docs say so. - An operator flips the key with simulators still around and iOS stays off until they follow the steps
doctorprints.
Words used
- device set: a folder of simulators that
simctland Xcode read, e.g.~/Library/Developer/CoreSimulator/Devices(Xcode's set) or~/.simlock/devices/ios(Simlock's own set). - Xcode's set: the default device set every Xcode tool reads when given no other.
- durable mark: the small file Simlock writes into a simulator's folder when it creates the simulator. It will name the Simlock instance; under
"default"it is what makes a simulator this Simlock's. - instance: one Simlock data folder (
SIMLOCK_HOME,~/.simlockby default) with its own id. One OS user can run more than one, e.g. the slow-lane tests. - generic build:
xcodebuild -destination "generic/platform=iOS Simulator", a build for no particular simulator, installed afterwards.
Non-goals
- Flipping the default to
"default". ADR 0048 §6 names the condition; a later issue does it. - Moving or recreating simulators when the setting changes.
- Writing the per-user
DVTSimulatorSetLocationdefault. - Any change on Android or for physical devices.
- Lease-scoped
simlock simctl(simlock simctl and simlock adb on a worker run only against the caller's lease, and fill the device in #948) andsimlock run(simlock run holds a lease around one command, and a detached lease follows the agent process #944).
Completion conditions
- With
drivers.ios.deviceSet: "default", a leased simulator is listed by a plainxcrun simctl list devicesand agent-device drives it (snapshot), on a real Mac. - With the key unset, every iOS behaviour is as today.
- Under
"default", a simulator this Simlock did not create is never in its device list, so cleanup, the warm pool,nukeanddoctor --purge-orphansnever touch it. - A bad value, or a switch with simulators left from the other setting, turns iOS off with a reason
simlock doctorshows; Android keeps running. (Today a refused driver makessimlock doctorfail with an internal error; iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 fixes that.) - The website has a "Build and run your app" page, the configuration, cleanup, device and how-it-works pages describe both settings, and
simlock instructionsgives the build route for each.
Open questions
Decisions
- ADR 0048 — An iOS worker may keep its simulators in Xcode's device set (PR docs(adr): 0048 an iOS worker may keep its simulators in Xcode's device set #964): the
drivers.ios.deviceSetkey, ownership by a durable mark naming the instance under"default", the refusal on a changed setting, no set in any environment under"default", the wrapper's refusals kept, and when to flip the default.
Shared values
none (one task)
Behaviours
| Behaviour (invoker, action, what it sees) | Seam | Cases | Task |
|---|---|---|---|
The iOS driver, given a deviceSet value, runs every simctl call with or without --set |
IosSimctlDriver (src/drivers/ios/index.ts), fake process runner |
key unset; "simlock"; "default" |
#968 |
An operator with a bad value or a conflicting deviceRoot gets iOS off with reason invalid-device-set |
IosSimctlDriver.create |
"Default"; ""; true; "default" with deviceRoot set |
#968 |
An operator who switches with simulators left gets iOS off with reason device-set-changed |
IosSimctlDriver.create |
default with devices in Simlock's set; default with Simlock's set empty or absent; simlock with a device in Xcode's set marked by this instance; simlock with only unmarked or other-instance devices there; the message names each UDID and the delete command |
#968 |
| A new or reclaimed simulator's mark names this instance, under both settings | IosSimctlDriver.provision, reclaim |
"simlock"; "default" |
#968 |
Under "default", listManaged reports only simulators whose durable mark names this instance |
IosSimctlDriver.listManaged |
this instance's mark; erasable mark gone (still listed, reads erased); no mark; a mark with no instance; another instance's mark; unmarked named simlock-… |
#968 |
Under "default", component.list's foreignDevices counts only simulators this instance's mark does not name |
IosSimctlDriver.listComponents |
one of this instance's, one unmarked, one another instance's, on the same runtime | #968 |
A lease holder under "default" gets no SIMLOCK_IOS_DEVICE_SET, and the screen-capture program gets none |
IosSimctlDriver.leaseEnvironment, screen-capture env |
"simlock"; "default" |
#968 |
An agent using simlock simctl under "default" gets plain xcrun simctl and the same refusals |
IosSimctlDriver.passthrough |
install <udid> app; erase <udid>; --set /tmp list |
#968 |
The agent-device simulator daemon under "default" gets no AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET |
AgentDeviceSupervisor |
lease environment with and without SIMLOCK_IOS_DEVICE_SET |
#968 |
An agent reading simlock instructions learns the build route: no SIMLOCK_IOS_DEVICE_SET in the grant means target the UDID directly; with it, add -DVTSimulatorSetLocation on Xcode 27+, else the generic build and install |
src/instructions/index.ts text test |
the section's required phrases | #968 |
An operator running simlock doctor while iOS is refused sees driver-unavailable with the reason, not an internal error |
src/daemon/dispatcher.ts doctor.run |
invalid-device-set; device-set-changed |
#968 |
A maintainer who stops a slow-lane run under "default" is left no simulator in their Xcode set |
scripts/slow-e2e.sh stop |
a run stopped mid-case | #968 |
An agent on a real Mac under "default" finds its leased simulator in plain xcrun simctl list and agent-device snapshots it |
slow lane, e2e/slow-agent-device.test.ts |
iOS | #968 |
Tasks
One task.
- iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 iOS simulators can live in Xcode's device set — an operator sets
drivers.ios.deviceSet: "default"and Xcode tools target leased simulators directly. Risk: a user's own simulator, or another Simlock's, is wiped by Simlock, or iOS will not start after a switch.
Completion conditions: all delivered by #968.
Open work in the same area:
- simlock simctl and simlock adb on a worker run only against the caller's lease, and fill the device in #948 (lease-scoped
simlock simctl/adb,request:new): changes the same passthrough andworking-with-a-device.mdx. Not specced; no Depends on. Whichever lands second takes the other's refusal list. - simlock run holds a lease around one command, and a detached lease follows the agent process #944 (
simlock run,request:new): would read the lease environment; under"default"it carries no set. Not specced; no Depends on. - device.exec fills in the lease's device and refuses another #958 (
device.execfills in the lease's device,task:ready, feature MCP: a run_on_device tool runs simctl, adb or devicectl against the session's lease #951, ADR 0047 PR docs(adr): 0047 device.exec fills in the lease's device, and refuses another (#951) #955): changes the iOSsimctlpassthrough insrc/drivers/ios/index.ts, the same function iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 changes. iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 depends on device.exec fills in the lease's device and refuses another #958. - Add
simlock instructions: the rules an agent must follow, printable and served over MCP #147 / PR feat(cli): simlock instructions prints the rules an agent must follow #960 (simlock instructions, feature One command installs the agent rules where agent tools read them #157): adds the "iOS builds" section insrc/instructions/index.ts, which says Simlock's simulators are never in Xcode's set, and pins its phrases insrc/instructions/index.test.ts. iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 rewrites that section for both settings. iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 depends on Addsimlock instructions: the rules an agent must follow, printable and served over MCP #147. - A standalone worker serves agent-device on localhost by default #957 (standalone worker serves agent-device, feature agent-device on the same Mac works with no operator step #952): changes the shared daemon setup of
e2e/slow-agent-device.test.tsande2e/helpers/agent-device.ts, which iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968's slow-lane case extends. iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 depends on A standalone worker serves agent-device on localhost by default #957. - Per-platform lease limit per requester, on a worker and a gateway #966 (per-platform lease limit, another feature): also changes contract shapes and bumps the protocol. iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 depends on Per-platform lease limit per requester, on a worker and a gateway #966.
- PR fix(daemon): exec children get a minimal environment; docs say what an agent token can do #891 (minimal env for exec children): reads the lease environment. Under
"default"there is less of it; no Depends on.
Written by an agent.
Activity
V3RON commentedon Oct 23, 2026
Spec updated: business sections, Decisions (ADR 0048, PR #964), Behaviours and the task list.
Written by an agent.
V3RON commentedon Oct 23, 2026
Spec updated: round-0 check fixes (instance-named marks, switch steps, overlaps #147 and #957), and task #968.
Written by an agent.
V3RON commentedon Oct 24, 2026
Delivery run
Merged: none
Parked: none
Waiting: #968 — on #966 (open, no PR yet)
Written by an agent.
docs(adr): 0048 amends safety rules, names itself in ADR 0001, settle…
docs(adr): 0048 narrows ADR 0001 §1 and §6 as well (#962)
docs(adr): list 0048 after 0047 in the index (#962)
V3RON commentedon Oct 24, 2026
Delivery run
Merged: none
Parked: #968 — PR #976 needs-hardware: slow lane unavailable (14.8 GiB free, needs 15); stack #977 (#964 → #976) reviewed, not gated
Decided: #968 — switch steps for a custom
deviceRoot→ keep it set during cleanup, no registry check (pending the maintainer)Written by an agent.
docs(adr): 0048 an iOS worker may keep its simulators in Xcode's devi…
feat(drivers): iOS simulators can live in Xcode's device set (drivers…
V3RON commentedon Oct 26, 2026
Delivery run
Merged: #964 (ADR 0048), #976 (#968)
Written by an agent.
github-actions commentedon Oct 26, 2026
Closing: all 1 sub-issues are closed. The PR that closed the last one walks the completion conditions (delivery rule 9); reopen this if one does not hold on
main. Flip its ADRs to Accepted if delivery has not.