Skip to content

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.

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.

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 privacy for permission grants/revokes

Commands for device management, deep links, screenshots, UI hierarchy extraction, animation control, native logs, permissions, and locale. Key recommendations:

  • Use adb exec-out screencap -p for direct-pipe screenshots (skips device storage round-trip)
  • uiautomator dump --compressed provides full structured accessibility tree
  • Disable animations before test runs (window, transition, animator scales to 0)

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:

TaskTool
List devicesdevice_list
Screenshotdevice_screenshot
UI element treedevice_snapshot (returns @refs)
Tap by textdevice_find text="Sign In" action=click
Tap by refdevice_press ref=@e3
Fill inputdevice_fill ref=@e5 text="hello"
Scroll/swipedevice_swipe direction=up
System backdevice_back

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.

Terminal window
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.

OperationTimeSize
iOS screenshot (JPEG)80ms200KB
iOS screenshot (PNG)150ms800KB
Android screenshot (exec-out)300ms800KB
Android UI hierarchy (raw)300-500ms15-30KB XML
Android UI hierarchy (parsed)350-550ms2-3KB JSON

Decision table for build workflows:

SituationAction
App running + Metro connectedSkip, proceed to testing
Metro not running, app missingexpo_ensure_running.sh ios or android
Test a specific EAS buildeas_resolve_artifact.sh then expo_ensure_running.sh --artifact
SymptomFix
Simulator not bootingxcrun simctl shutdown all && xcrun simctl erase all
adb shows “unauthorized”Revoke USB debugging, reconnect, tap Allow
Screenshot hangsWait for home screen, verify boot completed
uiautomator dump failsWake screen with keyevent WAKEUP

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.keyboardGuardMeaning
offOrdinary-target guard disabled (RN_KEYBOARD_GUARD=0); exact iOS keyboard validation and raw-coordinate keyboard refusal remain enforced
no_keyboardNo keyboard was visible
not_occludedKeyboard visible but the fresh target rectangle was clear of it
dismissedAndroid: keyboard was occluding the tap point and was dismissed first
auto_dismissediOS: keyboard was dismissed before the app tap (meta.via records the tier)
keyboard_targetiOS: the exact latest-snapshot Key/Keyboard was re-proven live and activated once

Platform behavior differs because the dismissal mechanism differs:

  • Android — point containment + pressBack with 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/Keyboard type, 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_STALE performs 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. Otherwise KEYBOARD_DISMISS_FAILED refuses 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.