Purpose
Xcode 27 Device Hub can pull a real device's app data container and restore it into a simulator. The high-value use case: a tester hits a bug that only reproduces with their data, and a developer loads that exact app state locally instead of guessing at which combination of cache rows, feature flags, locale, and settings caused it.
This issue documents research and a feasibility prototype for the equivalent in agent-device as a typed API/CLI, plus an honest cost estimate. Recommendation: park for now — no one has requested it and the shipping cost is ~2x a reasonable slice. Revisit on demand, or when an app-data/artifact command family exists for a new operation to ride alongside.
Prototype and evidence
Prototype: scripts/prototypes/app-data-transfer.mjs on branch t3code/device-data-restore-research (commit 08b52df45, https://github.com/callstack/agent-device/tree/t3code/device-data-restore-research). Throwaway, self-contained (execFile + node:fs only, per the plain-.mjs rule), not product code.
Validated on the Xcode 27.1 beta seed:
-
Full export -> wipe -> import loop passes through the prototype's own code path against a booted simulator, with a sentinel file verified present after restore.
-
Interchange format should be a plain directory, not .xcappdata. simctl get_app_container <sim> <bundle> data + recursive copy round-trips reliably. simctl install_app_data requires Xcode's undocumented AppDataInfo.plist schema; probing it produced The data package was not valid. (AppDataInfo.plist exists = 0, AppData exists = 1, AppData is directory = 1), then The app data package corresponds to an app that isn't currently installed once a plist was added. Public reports describe the same fragility device->sim. Standardizing on a directory sidesteps it.
-
Device-side pull is one call each way, with the flag surface verified on this seed:
xcrun devicectl device copy from --device <id> \
--domain-type appDataContainer --domain-identifier <bundleId> \
--source . --destination <dir> -r true
copy to accepts the same appDataContainer / appGroupDataContainer domains, so restore onto a physical device is symmetric.
-
The hard Apple plumbing is already vendored here, currently aimed at narrow internal jobs:
packages/platform-apple/src/core/physical-device-files.ts:8 — copyCoreDeviceRunnerFile already runs devicectl device copy from --domain-type appDataContainer --domain-identifier <bundleId> (reached from core/physical-device-screenshot.ts:43).
packages/platform-apple/src/core/app-settings.ts:204 — simctl get_app_container <sim> <bundleId> data, then host-side directory enumeration and removal, for clearing app state.
- Physical-vs-simulator routing already resolved in
core/physical-device-routing.ts.
Why no existing CLI already covers it
| Tool |
Device -> host |
host -> simulator |
Agent/CI drivable |
One-shot device->sim |
| Xcode Devices / Device Hub 27 |
Download Container |
Upload Container / drag |
no (GUI) |
GUI only |
devicectl |
device copy from --domain-type appDataContainer |
copy to targets another device, not a sim |
yes |
no |
simctl |
n/a (sim only) |
get_app_container data + copy, or install_app_data |
yes |
no |
| iMazing / Burrow |
one click |
one click |
no (GUI) |
GUI only |
The blocks exist and are Apple's. The headless command that crosses device -> simulator, and that an agent or CI job can drive, does not exist anywhere. That is the only gap worth filling — and it is the gap agent-device is chartered for.
Cost: the per-operation registration tax
The core logic is roughly 150 LOC. What costs real money is registering a dispatched runtime operation: registry completeness requires it to be declared and fact-classified in every place that admits device runtime use.
Measured by the closest comparable single operation (sendPushNotification), which appears across roughly 18 production files: contracts operations interface and defineUse plan, canonical op-name list, registry descriptor, unavailable-operation map, src/platform-runtime-managed-owner.ts, packages/platform-apple/src/runner-demand.ts, a daemon handler, the CLI facet, the published client, and an available-or-typed-unavailable fact in each of platform-apple, platform-android, platform-harmonyos, platform-web, provider-webdriver and provider-limrun.
Estimated production LOC for app-data pull + app-data restore as two operations:
| Site |
~LOC |
| Apple core, generalizing the two existing helpers |
130-160 |
CLI facets: reader/writer/metadata/schema x2 (management/push.ts is 123 for two commands) |
~120 |
Daemon handler (analog src/daemon/handlers/session-app-deployment.ts = 248) |
~70 |
| Contracts options/input/result/operations interface (analog 95 + 16) |
~55 |
Registry descriptors x2 + flag definitions (packages/command-registry/src/registry.ts) |
~80 |
| Per-platform/provider op classification, 2 ops x ~8 modules |
~60 |
Client method, client-types, facades, versioned help |
~70 |
| Production total |
~585-615 |
Mirrored tests add roughly 400-700 more. There is no legitimate sub-300 path: shelling out directly instead of going through the runtime gateway is exactly the reach-around-the-dispatch pattern AGENTS.md forbids, and it would lose daemon, remote-transport, and typed-fact consistency. The leanest defensible slice — app-data restore only, directory format, reusing the existing pull helper — still pays registry + runtime-use + platform classification + help + client and lands ~350-450 production.
API sketch (if picked up)
Two single-device commands plus optional cross-target sugar. Both fit the one-session-one-device model; the cross-target transfer deliberately does not, so it should be a client composition rather than a session command (compare install-from-source, which is intentionally not session-scoped).
agent-device app-data pull <app> --out ./case # device or simulator source
agent-device app-data restore <app> --from ./case # import target
agent-device app-data transfer <app> --from <id> --to <id> # optional: client-side composition
client.apps.pullData({ app, output?, container?: 'data' | 'groups' });
client.apps.restoreData({ app, input });
Design commitments the prototype establishes:
- Interchange artifact is a plain directory tree of the data container. Treat
.xcappdata as an accepted input only if a consumer asks, and never as our output format.
- Close the app before restore, require the app to be already installed under the same bundle id, and report a typed reason when the target has no data container yet.
- Physical-device pull requires a dev-signed / debuggable build (
get-task-allow) or a shared App Group with --domain-type appGroupDataContainer. Release and TestFlight builds are not extractable; Device Hub shares this limit. Fail with that explanation rather than a generic COMMAND_FAILED.
- Non-transferrable state must be documented, not discovered: Keychain and Secure Enclave items are hardware- or device-bound, file-protection-class data may not decrypt, and apps that cached absolute container paths will not follow the move. The realistic win is
Documents, Library/Preferences, Application Support/SQLite and caches.
- Containers carry tokens, account rows, and locations. Publish exports as a session artifact path under
src/daemon/session-artifact-paths.ts conventions, warn on pull and restore, default to a temp directory with cleanup, and never commit a real container.
- Scope to Apple.
adb backup is deprecated and unreliable, so report typed-unavailable on Android/HarmonyOS rather than faking parity.
Completion conditions (if implemented)
app-data pull from a physical device and from a simulator both produce a directory tree that a later app-data restore accepts.
app-data restore into a booted simulator reproduces the exported state, proven by an on-device observation (a snapshot or app-visible value), not by exit code.
- Both paths reach the Apple runtime through a declared runtime use admitted by fact, with typed-unavailable facts elsewhere; registry completeness passes.
- A release-build or non-installed-app source fails with the debuggable-build / install-first hint.
- Versioned help and user docs describe the non-transferrable state and the PII handling.
Dependencies and sequencing
- No blocker, but the cost drops materially if an
app-data or session-artifact command family already exists, since a third operation on an existing surface is far cheaper than standing up a new family.
- Live device verification requires a cabled, dev-signed test app; see
docs/agents/device-verification.md.
Purpose
Xcode 27 Device Hub can pull a real device's app data container and restore it into a simulator. The high-value use case: a tester hits a bug that only reproduces with their data, and a developer loads that exact app state locally instead of guessing at which combination of cache rows, feature flags, locale, and settings caused it.
This issue documents research and a feasibility prototype for the equivalent in
agent-deviceas a typed API/CLI, plus an honest cost estimate. Recommendation: park for now — no one has requested it and the shipping cost is ~2x a reasonable slice. Revisit on demand, or when anapp-data/artifact command family exists for a new operation to ride alongside.Prototype and evidence
Prototype:
scripts/prototypes/app-data-transfer.mjson brancht3code/device-data-restore-research(commit08b52df45, https://github.com/callstack/agent-device/tree/t3code/device-data-restore-research). Throwaway, self-contained (execFile+node:fsonly, per the plain-.mjsrule), not product code.Validated on the Xcode 27.1 beta seed:
Full export -> wipe -> import loop passes through the prototype's own code path against a booted simulator, with a sentinel file verified present after restore.
Interchange format should be a plain directory, not
.xcappdata.simctl get_app_container <sim> <bundle> data+ recursive copy round-trips reliably.simctl install_app_datarequires Xcode's undocumentedAppDataInfo.plistschema; probing it producedThe data package was not valid. (AppDataInfo.plist exists = 0, AppData exists = 1, AppData is directory = 1), thenThe app data package corresponds to an app that isn't currently installedonce a plist was added. Public reports describe the same fragility device->sim. Standardizing on a directory sidesteps it.Device-side pull is one call each way, with the flag surface verified on this seed:
copy toaccepts the sameappDataContainer/appGroupDataContainerdomains, so restore onto a physical device is symmetric.The hard Apple plumbing is already vendored here, currently aimed at narrow internal jobs:
packages/platform-apple/src/core/physical-device-files.ts:8—copyCoreDeviceRunnerFilealready runsdevicectl device copy from --domain-type appDataContainer --domain-identifier <bundleId>(reached fromcore/physical-device-screenshot.ts:43).packages/platform-apple/src/core/app-settings.ts:204—simctl get_app_container <sim> <bundleId> data, then host-side directory enumeration and removal, for clearing app state.core/physical-device-routing.ts.Why no existing CLI already covers it
devicectldevice copy from --domain-type appDataContainercopy totargets another device, not a simsimctlget_app_container data+ copy, orinstall_app_dataThe blocks exist and are Apple's. The headless command that crosses device -> simulator, and that an agent or CI job can drive, does not exist anywhere. That is the only gap worth filling — and it is the gap
agent-deviceis chartered for.Cost: the per-operation registration tax
The core logic is roughly 150 LOC. What costs real money is registering a dispatched runtime operation: registry completeness requires it to be declared and fact-classified in every place that admits device runtime use.
Measured by the closest comparable single operation (
sendPushNotification), which appears across roughly 18 production files: contracts operations interface anddefineUseplan, canonical op-name list, registry descriptor, unavailable-operation map,src/platform-runtime-managed-owner.ts,packages/platform-apple/src/runner-demand.ts, a daemon handler, the CLI facet, the published client, and an available-or-typed-unavailable fact in each of platform-apple, platform-android, platform-harmonyos, platform-web, provider-webdriver and provider-limrun.Estimated production LOC for
app-data pull+app-data restoreas two operations:management/push.tsis 123 for two commands)src/daemon/handlers/session-app-deployment.ts= 248)packages/command-registry/src/registry.ts)client-types, facades, versioned helpMirrored tests add roughly 400-700 more. There is no legitimate sub-300 path: shelling out directly instead of going through the runtime gateway is exactly the reach-around-the-dispatch pattern
AGENTS.mdforbids, and it would lose daemon, remote-transport, and typed-fact consistency. The leanest defensible slice —app-data restoreonly, directory format, reusing the existing pull helper — still pays registry + runtime-use + platform classification + help + client and lands ~350-450 production.API sketch (if picked up)
Two single-device commands plus optional cross-target sugar. Both fit the one-session-one-device model; the cross-target transfer deliberately does not, so it should be a client composition rather than a session command (compare
install-from-source, which is intentionally not session-scoped).Design commitments the prototype establishes:
.xcappdataas an accepted input only if a consumer asks, and never as our output format.get-task-allow) or a shared App Group with--domain-type appGroupDataContainer. Release and TestFlight builds are not extractable; Device Hub shares this limit. Fail with that explanation rather than a genericCOMMAND_FAILED.Documents,Library/Preferences,Application Support/SQLite and caches.src/daemon/session-artifact-paths.tsconventions, warn on pull and restore, default to a temp directory with cleanup, and never commit a real container.adb backupis deprecated and unreliable, so report typed-unavailable on Android/HarmonyOS rather than faking parity.Completion conditions (if implemented)
app-data pullfrom a physical device and from a simulator both produce a directory tree that a laterapp-data restoreaccepts.app-data restoreinto a booted simulator reproduces the exported state, proven by an on-device observation (a snapshot or app-visible value), not by exit code.Dependencies and sequencing
app-dataor session-artifact command family already exists, since a third operation on an existing surface is far cheaper than standing up a new family.docs/agents/device-verification.md.