Reference
Device Control
The device control skill provides comprehensive reference for controlling simulators and emulators, capturing screenshots, reading UI state, managing permissions, and handling Expo/EAS builds.
When loaded
Section titled “When loaded”Activates when Claude encounters tasks involving simulator/emulator interaction: taking screenshots, booting devices, installing apps, reading UI hierarchy, opening deep links, granting permissions, streaming native logs, or managing device settings.
Key Knowledge Areas
Section titled “Key Knowledge Areas”iOS Simulator (simctl)
Section titled “iOS Simulator (simctl)”Commands for boot/shutdown, app install/launch/terminate, deep links, screenshots, native log streaming, and permissions. Key recommendations:
- Prefer JPEG screenshots (80ms, 200KB) over PNG (150ms, 800KB)
- Use
processImagePath ENDSWITH "/BinaryName"for precise native log filtering - Use
xcrun simctl privacyfor permission grants/revokes
Android Emulator (adb)
Section titled “Android Emulator (adb)”Commands for device management, deep links, screenshots, UI hierarchy extraction, animation control, native logs, permissions, and locale. Key recommendations:
- Use
adb exec-out screencap -pfor direct-pipe screenshots (skips device storage round-trip) uiautomator dump --compressedprovides full structured accessibility tree- Disable animations before test runs (window, transition, animator scales to 0)
Device tools
Section titled “Device tools”Cross-platform unified device interaction that eliminates platform-specific branching. The device_* tools route through the in-tree runners (iOS: rn-fast-runner; Android: rn-android-runner) — the sole device backend:
| Task | Tool |
|---|---|
| List devices | device_list |
| Screenshot | device_screenshot |
| UI element tree | device_snapshot (returns @refs) |
| Tap by text | device_find text="Sign In" action=click |
| Tap by ref | device_press ref=@e3 |
| Fill input | device_fill ref=@e5 text="hello" |
| Scroll/swipe | device_swipe direction=up |
| System back | device_back |
Exact-device state snapshot
Section titled “Exact-device state snapshot”The package-local helper captures state sequentially on one validated device.
Resolve <package-root> to the selected Claude or Codex plugin package; agents
derive it from the exact active skill location rather than scanning caches.
IOS_SNAPSHOT_RESULT=$(bash "<package-root>/scripts/snapshot_state.sh" ios --device-id "$IOS_UDID" --output-dir "$SNAPSHOT_DIR")ANDROID_SNAPSHOT_RESULT=$(bash "<package-root>/scripts/snapshot_state.sh" android --device-id "$ANDROID_SERIAL" --output-dir "$SNAPSHOT_DIR")
# Read "$IOS_SNAPSHOT_RESULT/screenshot.jpg"# Read "$ANDROID_SNAPSHOT_RESULT/screenshot.png" and "$ANDROID_SNAPSHOT_RESULT/ui_elements.json"--device-id is required. The helper fails closed if the identity is missing,
not connected, or ambiguous across platforms. If --output-dir is omitted, it
creates an owner-only private directory, removes it when capture produces no
artifact, and reports the retained result path after a successful capture. A
supplied directory must be owned by the current user, use mode 0700, and not
be a symlink. Each complete capture is atomically published as its own immutable
result directory, so concurrent runs cannot mix or overwrite evidence.
Benchmark Reference
Section titled “Benchmark Reference”| Operation | Time | Size |
|---|---|---|
| iOS screenshot (JPEG) | 80ms | 200KB |
| iOS screenshot (PNG) | 150ms | 800KB |
| Android screenshot (exec-out) | 300ms | 800KB |
| Android UI hierarchy (raw) | 300-500ms | 15-30KB XML |
| Android UI hierarchy (parsed) | 350-550ms | 2-3KB JSON |
Expo/EAS Build Integration
Section titled “Expo/EAS Build Integration”Decision table for build workflows:
| Situation | Action |
|---|---|
| App running + Metro connected | Skip, proceed to testing |
| Metro not running, app missing | expo_ensure_running.sh ios or android |
| Test a specific EAS build | eas_resolve_artifact.sh then expo_ensure_running.sh --artifact |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Fix |
|---|---|
| Simulator not booting | xcrun simctl shutdown all && xcrun simctl erase all |
| adb shows “unauthorized” | Revoke USB debugging, reconnect, tap Allow |
| Screenshot hangs | Wait for home screen, verify boot completed |
| uiautomator dump fails | Wake screen with keyevent WAKEUP |
Keyboard-occlusion guard
Section titled “Keyboard-occlusion guard”device_press and device_longpress guard against tapping through a software keyboard by default. For ordinary app targets, the runner checks whether a visible keyboard’s frame (a sanity-checked rect — min height 120pt iOS / 150px Android, so a predictive-text accessory bar doesn’t count) collides with the target before choosing a safe dismissal or refusal. Exact iOS keyboard targets take the separately validated activation path below.
meta.keyboardGuard | Meaning |
|---|---|
off | Ordinary-target guard disabled (RN_KEYBOARD_GUARD=0); exact iOS keyboard validation and raw-coordinate keyboard refusal remain enforced |
no_keyboard | No keyboard was visible |
not_occluded | Keyboard visible but the fresh target rectangle was clear of it |
dismissed | Android: keyboard was occluding the tap point and was dismissed first |
auto_dismissed | iOS: keyboard was dismissed before the app tap (meta.via records the tier) |
keyboard_target | iOS: the exact latest-snapshot Key/Keyboard was re-proven live and activated once |
Platform behavior differs because the dismissal mechanism differs:
- Android — point containment +
pressBackwith a bounded wait for the UI to settle (~3.6s measured). Only fires when a real input-method window with sane bounds contains the tap point, so it never triggers an unrelated back-navigation. - iOS exact keyboard target — only a ref whose runner-retained generation, node index, canonical
Key/Keyboardtype, identity, and frame uniquely match the operation-local live keyboard qualifies. The tap must remain inside that exact current target, which is activated directly once. Raw coordinates, forged wire types, stale/ambiguous refs, relayout, missing software keyboards, case/region guesses, and custom-keyboard Button/Other nodes never qualify.KEYBOARD_TARGET_STALEperforms no gesture or dismissal; take a fresh snapshot. - iOS ordinary app target — only a positively identified hide/dismiss button inside the keyboard may dismiss natively; Return/Done keys and app accessory toolbars are excluded. There is no automatic keyboard swipe. When CDP is connected, injected
Keyboard.dismiss()remains available with a fresh hidden-state proof. After safe dismissal the snapshot/ref is refreshed, uniquely re-resolved, and tapped once. OtherwiseKEYBOARD_DISMISS_FAILEDrefuses without a native gesture.
If you hit KEYBOARD_DISMISS_FAILED on iOS, connect CDP for the JS tier or dismiss manually, then retry the app target from a fresh snapshot. If an old runner lacks EXACT_KEYBOARD_TARGET_GUARD, reopen the exact-device session so it rebuilds before dispatch.
Only device_press/device_longpress-style taps are guarded — tapSeries, text-based taps, swipes/scrolls/drags, and doubleTap are unaffected.