Commands
Find the command for a task, the flags it takes, and how it behaves on each platform.
For persistent defaults and project-scoped CLI settings, see Configuration.
CLI help matches your installed version. agent-device help lists the workflow and platform guides, and help <command> gives a command's exact flags:
If your agent runtime supports skills, install them to route agents automatically; they are optional. The CLI help topics are the reference for your installed version.
For MCP-aware clients that support direct tools, run:
For MCP client setup and what MCP does not expose, see AI Agent Setup.
Output and warnings
In text mode, every command prints each response warning as a Warning: line after its own output.
get, find, clipboard, and record print those lines on stderr instead, so
value=$(agent-device get text …) captures only the value. With --json, warnings stay in
data.warnings and no Warning: line is printed.
Navigation
Boot and shut down:
bootgets the selected target ready without launching an app. It needs an active session or a device selector. You mainly need it whenopenfails because no simulator or emulator is booted.- Android:
boot --platform android --device <avd-name>launches that emulator with a window; add--headlessto launch it without one. On macOS the window opens behind the focused app, which keeps focus (also whenopenboots a stopped AVD). This is best-effort: the emulator can still take focus if the focused app cannot be read, or if its window appears after the startup budget runs out. shutdownturns off the selected Apple simulator or Android emulator. It refuses the active session's device; useclose --shutdownto end the session and turn the device off.--timeout <ms>onboot,open <app>, andprepare ios-runneris the startup budget for booting an iOS Simulator or Android emulator (forprepare ios-runner, shared with runner preparation). A never-booted iOS Simulator can take several minutes. When the budget runs out, the command fails witherror.details.reason: boot_timeoutand the device keeps booting, so a retry finds it further along. See Sessions.
Select a device:
--platform appleis an alias for the Apple backend (ios,tvOS,macOS). Add--target mobile|tv|desktopto--platformto choose phone/tablet, TV, or desktop targets.- A command that needs one device fails with
AMBIGUOUS_MATCHand lists the candidates when you pass no--device,--udid, or--serialand several are equally preferred, such as two booted emulators. Virtual devices are preferred over physical ones and booted over offline ones first, and commands inside a session use the session's device.devicesstill lists everything. - Commands without
--sessionuse an implicit session scoped to your worktree and the selected platform. For named sessions,AGENT_DEVICE_SESSION, and--session-lock reject|strip, see Sessions.
Device claims and the daemon:
device statuslists host-local device claims without starting or contacting a daemon;--staleshows the stale claims it hides by default (owner exited, PID reused, state directory deleted, or daemon replaced), some of whose owners may still be running. Scope it with--platformplus--udid(Apple) or--serial(Android). A claim held by another live or uncertain owner blocksopen, unless the device rebooted after the claim was taken: thenopensettles that owner's durable resources, takes the claim, and reports the release inwarnings.device release --stalesettles the durable resources of an owner that provably can't release its claim (its process exited or a newer daemon replaced it), then clears the claim. Live, uncertain, PID-reused, corrupt, and deleted-state-directory claims are refused with the reason. See Sessions.- A command that needs the shared local daemon starts it for its state directory when none runs;
device statusanddevice release --stalenever contact one. When the running daemon comes from another version or build, the command replaces it only if it reports no work: no request in flight, no session, no repair session and no live lease. A resumable upload still streaming and a host human-control hold that names no lease are not counted, so a replacement can end them. Otherwise the command fails withdaemon-busyand leaves the daemon alone. A daemon from a release that predates this check is never replaced, and fails withdaemon-incompatible. End a busy or older daemon deliberately withdaemon stop. Node.js clients choose this per client; see Daemon lifecycle. daemon stop --state-dir <path>stops a detached local daemon with process signals after verifying its identity.--cleanalso removes the Apple runner processes and leases that daemon retained.daemon stoprefuses a live embedded runtime withdetails.reason: daemon-owner-embeddedand a hint naming the host PID. Close it in its host process with the handle returned bystartAgentDevice(). Stop refuses live unrecognized ownership withdaemon-owner-unrecognized. Neither refusal sends a request or signal. Stop retires an embedded or unrecognized registration when its recorded host lifetime has provably exited. Without--clean, an unreadabledaemon.jsonfails withdaemon-record-unreadable.--cleanrequires proven detached ownership; embedded, unrecognized and unreadable records fail withdaemon-clean-not-detached.daemon statusshowshost-ownedorowner-not-recognized. See Daemon lifecycle.daemon status [--state-dir <path>] [--json]reads the daemon's record without contacting it. For a running daemon it lists every provider it registered, built in or installed, and the reason any plugin failed to register. See provider plugins.
Open apps and links:
open [app|url] [url]boots the selected device when needed and opens the target in a session.open <url>opens deep links on Android and iOS, andopen <app> <url>opens a deep link in that app on iOS. See Desktop targets for macOS activation behavior.- On iOS devices,
http(s)://URLs open in Safari when no app is active. Custom-scheme URLs need an active app in the session. open <app> --wait <ms>waits up to that budget for a device another session holds, then fails withDEVICE_IN_USE. The wait extends the command's timeout, so you don't need a longer--timeout. A claim held by another workspace's daemon is not waited for;openreturns its recovery command at once. Device-mutating commands likeinstallandreinstallnever wait either: a claim held by another session fails them at once, and when the caller sends a workspace, another workspace's claim under the same session name fails too. See Sessions.open <app> --launch-console <path>captures launch-time stdout/stderr for iOS simulator app launches. It is not valid for URL opens or other targets.open <app> --capture-networkinjects the network capture probe into an iOS simulator app launch, sonetwork dumplists every URLSession request with its own full URL, status, and duration (see Network inspection). The app is relaunched under the probe. It is not valid for URL opens, other platforms, or non-URLSession stacks such as Flutter dart:io.open --platform macos --surface app|frontmost-app|desktop|menubarselects the macOS session surface;appis the default when you name an app. See Desktop targets.--save-scriptrecords the surface soreplayreopens it; with no surface given, replay opens an app session.- Android React Native:
open --relaunchneeds an installed package name. Install or reinstall the APK first;open <apk|aab> --relaunchis rejected. For Metro-backed JS changes, runmetro reloadbefore reaching foropen <app> --relaunch. - Android (adb): an intent target such as
open settingsrecords the package that opened asappBundleId.open settings --relaunchstops and restarts that app. If the intent matches more than one activity,--relaunchfails. - Android (ADB):
closeforce-stops the app, then checks for 2 seconds that it has left the foreground and for 2 more that its process has exited. If either check window ends first,closefails withdetails.reason: android-app-close-timeoutanddetails.phaseset toforegroundorprocess; apps Android restarts at once, such as the launcher, fail this way.
iOS Simulator launch behavior:
- An
openthat launches an app reportstiming.postOpenObservation. When it is present and not"observable", the next command that reads the screen waits briefly until two reads agree, so aclicklands where the element comes to rest. If the screen is still moving after that wait, the command proceeds and warns that the surface was still changing. - If CoreSimulator never answers a URL hand-off,
openfails within 20 seconds withdetails.reason: ios-simulator-openurl-timeout. The Simulator may still have received the URL; retry the open or answer the prompt withalert accept. - When
open <app> --launch-url <url>raises anOpen in "<App>"?prompt and the URL's scheme belongs to the session app,opentaps Open and returnslaunchConfirmation: "accepted". If the app still never starts,openhands over the URL once more. - When that prompt stays unanswered,
openreturnslaunchConfirmation: "unanswered"withlaunchConfirmationReason:alert-unrecognizedfor a prompt it did not recognize (such as a non-English Simulator), orurl-owner-unresolvedwhen no single installed app owns the URL's scheme or that lookup failed. The prompt stays on screen;warningsname the next step. Inspect withalert get, then answer withalert accept. - When
opencould not read whether a prompt is up at all (the alert read, the accept, or the runner itself failed),opensucceeds withlaunchConfirmationProbeFailureset toalert-read,alert-accept, orrunnerand nolaunchConfirmation, plus a warning to check withalert getbefore retrying. details.reason: launch_confirmation_unansweredmeans the app never started after the prompt was accepted, or the prompt stayed visible after a failed tap. Answer it withalert acceptoralert dismissbefore continuing.details.reason: launch_confirmation_foreign_appmeans another installed app owns the scheme (details.foreignAppBundleId). Close the prompt withalert dismissand pass a URL your app handles.- A prompt that appears after
openhas already seen the app is not reported and not answered; usealert accept.
Back, orientation, and hardware buttons:
backuses app-owned back navigation: visible in-app back UI on Apple targets, and the back keyevent on Android.--in-appnames this default explicitly.- On a single-screen iPhone,
backcan use a top-leading tap when the app tree is unreadable. It compares the screen before and after the tap. If the screen cannot be captured, the command reports an unknown outcome; inspect the screen before you retry. back --systemsends system back input: the Android back keyevent, the iOS back gesture, or the tvOS Siri Remote Menu button. macOS has no system back, so it reports unavailable.orientation portrait|portrait-upside-down|landscape-left|landscape-rightworks on iOS and Android mobile targets only. On ADB-backed Android it turns auto-rotate off, and the lock persists after the session ends until something turns it back on.orientation autohands rotation back to the sensor on ADB-backed Android, local or Limrun (accelerometer_rotation=1,user_rotationuntouched), and reportsorientation: "auto". It does not wait for the display to rotate, because there is no target rotation: the sensor decides, and an emulator usually settles on portrait. Every other backend refusesautowithUNSUPPORTED_OPERATION.action-buttonpresses the iPhone or iPad Action Button once. It takes no arguments, no hold duration, and no--settle, and it doesn't activate the session app or drive Camera Control. Other platforms refuse it, and a model without the button (an iPhone SE, most iPad simulators) fails withUNSUPPORTED_OPERATION.action-buttonreports that the press was dispatched, not what it triggered. Simulators run no Shortcuts or App Intents, so verify the outcome on a physical iPhone. Expect a press to take about five seconds on a Simulator.hardware-button <button>presses one phone hardware key once, with no hold and no--settle, and it doesn't activate the session app. The names arevolume-up,volume-down,power,mute,sleep,wake,media-play-pause,media-next,media-previous,media-rewind, andmedia-fast-forward. The response reports the dispatched press, not what the OS did with it: which audio stream a volume press moves, which media session a media key reaches, and whether the HUD shows is device behavior the command does not claim.- Android presses all eleven keys on phones and tablets: the ADB-backed and helper paths send
input keyevent, and an Appium-backed device presses the same keycodes through itsmobile: pressKeyroute. A physical iPhone or iPad presses volume only, through the local runner or Appium's volume button press; no Apple leaf presses the other keys. iOS Simulators refuse volume (XCUIDeviceexposes no volume press there), and Android TV and tvOS targets refuse with a pointer totv-remote. - A
powerpress toggles the Android screen.sleepturns it off andwaketurns it on without toggling, so wake the screen withhardware-button wakeafter a press that turned it off. After a press that turns the screen off, later commands may hit a locked screen.
Fold a foldable device:
fold closed|half-open|opensets the pose of a foldable iPhone simulator (iPhone Duo) or a foldable Android emulator (pixel_9_pro_fold,pixel_10_pro_fold,7.6in Foldable) and reports the pose only after confirming it on the device. Re-snapshot after every fold, even an interrupted one; refs and coordinates from before it are stale. On iOS, expect about 10 seconds forclosedoropenand 16 forhalf-open.- The iOS response names the lit panel and its size, marked
coordinateSpace: "native-panel". That size is not the next snapshot's viewport and cannot place a tap. - iOS only:
--keyframesplays a timed trajectory instead of a preset, for examplefold --keyframes '[{"atMs":0,"angle":0},{"atMs":1667,"angle":160},{"atMs":3333,"angle":100},{"atMs":5000,"angle":180}]'. Use 2–64 keyframes starting at 0ms, strictly increasing integer timestamps up to 60,000ms, and angles from 0 to 180°. - iOS needs an Xcode with the iOS simulator SDK and foldable HID support (verified on Xcode 27.1); Device Hub and Accessibility permission aren't required. The first fold builds a helper cached under
~/.agent-device/fold-helper; failures reportfold-helper-build-failedorfold-hid-dispatch-failed. COMMAND_FAILEDwithreason: fold-pose-unverifiedmeans the hinge reports another pose;fold-pose-unsettledmeans it reachedhalf-openbut never came to rest;fold-pose-dispatched-unverifiedmeans the pose was sent but the first hinge-angle read returned no sample, so check the lit panel with a screenshot;hingeReadFailureholds the read's own failure.UNSUPPORTED_OPERATIONcovers single-panel devices (single-panel-deviceon Android) and simulators in a non-default--ios-simulator-device-set(unsupported-device-scope; use the default set). Physical devices are refused.- On Android,
foldconfirms the pose from the device state the emulator commits and reportshingeAngleDegreesas the hinge sensor reads it: the angle the AVD profile defines for the posture, such as 0°, 90°, and 180° on the Pixel folds or 15°, 90°, and 165° on the generic 7.6in Foldable. It also dismisses the lock screen that folding to the cover display can raise. For panels, poses, and timings, seeagent-device help foldable.
Simulate an incoming call or SMS:
telephony call <number>starts an incoming call from<number>on an Android emulator;telephony accept <number>answers it andtelephony cancel <number>ends it.telephony sms <number> <text>delivers a text message from<number>. Use it to check how an app reacts when a call or message arrives, for example whether video playback pauses and resumes.- The events go through the emulator console (
adb emu gsm call|accept|cancel,adb emu sms send), so they work on Android emulators reached through the local adb executor. Other adb executors, such as an ADB provider, have not been verified. Physical Android devices refuse withUNSUPPORTED_OPERATIONandreason: unsupported-device-kind; iOS, macOS, tvOS, web, Linux, HarmonyOS, Vega, and Limrun and WebDriver (Appium) sessions refuse withUNSUPPORTED_OPERATIONtoo: agent-device sends telephony only through adb and does not drive Appium's own call or SMS simulation. <number>is digits with an optional leading+, up to 15 digits. Quote the SMS text, or pass it as the remaining words; the text is sent as one argument, with backslashes and line breaks (LF, or CRLF sent as LF) escaped for the emulator console. Tabs, lone carriage returns, and other C0 or C1 control characters are refused.- The emulator console answers some events with an error, such as
acceptfor a number with no call. That fails withCOMMAND_FAILED,reason: telephony-console-refused, and the console's ownconsoleReason.cancelfor a number with no call succeeds, because the console answers it withOK. - The emulator drops SMS characters outside the Basic Multilingual Plane, such as emoji, and delivers the rest of the text.
textLengthcounts the text as sent. - The call screen or message notification can cover the app. Re-snapshot before using refs from before the event.
Remote daemons and tenants:
--tenant,--session-isolation tenant,--run-id, and--lease-idenforce lease admission on tenant-scoped daemons.--daemon-base-url http(s)://host:port[/base-path]calls a remote HTTP daemon directly instead of starting a local one. Add--daemon-auth-token <token>(orAGENT_DEVICE_DAEMON_AUTH_TOKEN) for non-loopback daemon URLs.- Screenshots and recordings from a remote daemon are downloaded to the path you pass, so
screenshot page.pngandrecord start session.mp4work as they do locally.record startwithout a path downloads torecording-<timestamp>.mp4(.webmon web) in your working directory. - To drive a Mac's simulators and devices from another machine, see Remote Proxy. For hosted devices in CI, see BrowserStack, AWS Device Farm, or TestMu AI. For
connect --remote-config, see Remote Metro workflow.
Human Takeover
Use takeover with an active remote connection when a person needs to use its leased device
without racing the agent:
takeover holds the device until you press Ctrl+C. While it holds, state-changing commands fail with
DEVICE_IN_USE and details.reason: "human_control_active"; read-only diagnostics still work. It
needs a device-scoped remote lease, so local sessions aren't supported. Holds don't survive a daemon
restart; reconnect and take the hold again. See Remote Proxy.
Web Automation
--platform web drives a browser through agent-browser and requires Node 24+. Use it when a browser step belongs inside an agent-device session, replay, batch, MCP, or Node.js client flow. For standalone browser automation, use agent-browser directly.
Set up and verify the managed web backend before you open a web session:
web setupinstalls a pinnedagent-browserbackend into the agent-device state directory (--state-dir,AGENT_DEVICE_STATE_DIR) and reuses it when present. Run it before first use and in CI bootstrap: web commands never install the backend themselves and use only that managed install.web doctorverifies it.- Supported: URL open, snapshot refs,
get text/attrs,is visible/hidden/exists/absent/focused/text,find text/selector, click/press, hover, fill/type, wait,network dump,audio probe, screenshot, close, and replay scripts made from those commands. hover <@ref|selector|x y>moves the pointer without pressing so hover-gated UI (row toolbars, menus) appears. Add--settleto read what it revealed; on web,--settleneeds a selector or coordinate target because web refs carry no geometry.audio probe start [durationSeconds] [bucketMs]samples HTML media elements into RMS/peak dBFS buckets; see Debugging & Profiling.- Not supported: tab/window/devtools control, network routing/interception/HAR, cookies/storage, downloads/uploads, arbitrary page scripting, multi-page orchestration, and raw browser diagnostics. Use
agent-browserdirectly for those.
Device isolation scopes
--ios-simulator-device-set <path>limits simulator discovery and commands to that simulator set. iOS physical devices are not listed while it is set.- If the selected Xcode can't resolve a scoped simulator for the Apple runner, the runner start fails with
details.reason: "simulator_set_destination_not_found", naming the set and, when known, the Xcode version. --android-device-allowlist <serials>limits Android discovery and selection to comma- or space-separated serials.- Set the same scopes with the
iosSimulatorDeviceSetandandroidDeviceAllowlistconfig keys, orAGENT_DEVICE_ANDROID_DEVICE_ALLOWLIST. CLI flags override environment values, except under astripsession lock, which ignores conflicting per-call selectors. - Start the daemon with
AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET=<path>to scope every iOS simulator request to one simulator set. A request without--ios-simulator-device-setuses that set, so iOS physical devices are not listed on that daemon. A request naming a different path string, including the same path with a trailing slash, fails withINVALID_ARGSanddetails.reason: "ios_simulator_device_set_outside_daemon_set". macOS and other non-iOS requests ignore the set, and the host Mac stays listed. A daemon that the CLI starts inherits the CLI's environment, so later CLI calls with a different value still use the first value until the daemon restarts. - Scope applies before
--device,--udid, and--serial, so an out-of-scope selector fails withDEVICE_NOT_FOUND. - On macOS, daemon startup removes any symlink at
~/Library/Developer/XCTestDevices, including one you made yourself (for example to move it to another volume), and restoresXCTestDevices.agent-device-backupwhen it exists. A symlinkedXCTestDevicesis not supported. Only the link is removed, never the directory it points to;daemon.logrecords each step and any failed restore, which doesn't stop the daemon.
Device discovery
deviceslists available targets after applying--platformand isolation scopes.- Each device carries
model(such asiPhone 17 ProorPixel 9) andosVersion(such as26.1or16) when platform tooling reports them:simctlfor iOS simulators,devicectlorxctracefor physical iOS devices, andro.product.modelandro.build.version.releasefor running Android devices. Android emulators report the system image model, such assdk_gphone64_arm64. Stopped AVDs and other platforms omit both fields. They describe a device and are not selectors. capabilitieslists the commands the session device, or an explicit--platform/--device/--udid/--serialtarget, supports. JSON output includesdeviceandavailableCommands; useavailableCommandsinstead of keeping your own platform support table. Android also reportstargetReadiness.press: "bounded-geometry"when element presses check moving targets before dispatch. Test harnesses can use that capability to delegate tap readiness instead of adding a fixed transition delay. It does not wait for the result of the tap.
HarmonyOS command boundary
HarmonyOS support uses HDC and ArkUI uitest. On API 24 devices it supports lifecycle and HAP deployment, ArkUI snapshot/screenshot and selector reads, one-pointer touch and text actions, keyboard enter/dismiss, app logs, foreground app state, process RSS samples, and settings clear-app-state. Run agent-device capabilities --platform harmonyos for the command list of a selected device.
gesture pan|fling|swipeandswipeuse single-pointer input. Multi-touch gestures and target-authoredgesture dragreturnUNSUPPORTED_OPERATION.- Physical devices support whole-screen recording with
record start <path> --scope deviceor--scope system, without--fps,--quality, or--hide-touches. Emulator recording is rejected. - Not available: orientation, clipboard, alerts, network/audio capture, push and app-event delivery, React Native helpers, and trace capture.
settingssupports onlyclear-app-state.perf memory samplereports process RSS; frame health and memory snapshots are unavailable.
Diagnostics
doctordiagnoses device, app, Metro, and React Native setup for the selected target. It is read-only: it never boots, installs, or changes the device.--platform ios|android|vega|macos|linux|web|applelimits checks to one backend; without it,doctorchecks every discoverable target.--app <id-or-name>adds app checks (install state, Metro and React Native wiring) for one bundle id or app name.--remoteruns only the checks that don't need a booted device; use it in CI bootstrap.
Prepare Apple runner
- In Apple CI setup, run
prepare ios-runnerafter the device is booted and the app is installed, before the firstsnapshot,replay, ortest. It builds or reuses the Apple runner, starts it, and checks that it answers. --timeout <ms>is one budget shared by the Simulator boot and runner preparation; see Navigation.- In JSON output,
buildMs,connectMs, andhealthCheckMsmay overlap (connectMsincludesbuildMswhen the runner is built). Usetiming.additivePartsfor additive phase totals. - A cached runner that fails its health check is rebuilt once, and a runner stuck at launch is relaunched once.
- CI may cache
~/.agent-device/apple-runner/derivedwhen the cache key includes the exact agent-device package contents and Xcode version. A cached runner whose files no longer match their recorded digests is rebuilt. - Each runner source or Xcode change creates a new key under
~/.agent-device/apple-runner/derived/<platform-folder>/. After a build, agent-device deletes, on a best-effort basis, keys that are neither among the 3 last used nor used in the last day, and never one a build is holding or a live runner lease names; change the count withAGENT_DEVICE_IOS_RUNNER_CACHE_KEEP(see Configuration). A key that can't be deleted is reported as arunner_xctestrun_cache_eviction_failedwarning. A customAGENT_DEVICE_IOS_RUNNER_DERIVED_PATHis never swept or trimmed. runner_cache_uncertifiablemeans the build products couldn't be verified, for example a product outside the derived-data root, an unreadable entry, a file over 128 MB, or a socket. PointAGENT_DEVICE_IOS_RUNNER_DERIVED_PATHat a plain directory you own, or replace the tree as the error hint describes.~/.agent-device/ios-runnerbelongs to older agent-device versions and is never touched. Delete it withrm -rf ~/.agent-device/ios-runneronce no such version runs.- Runner build and start output goes to the session's
runner.log;daemon.logcovers daemon startup.
TV targets
- TV targets include Apple TV and Android TV simulators, emulators, and connected devices, plus Fire TV devices and emulators running Fire OS (Android-based). Vega OS support is limited to the Vega Virtual Device (VVD).
- TV targets are focus-first: move focus with
tv-remotebefore selecting a control. Don't use rawadb shell input keyeventin command plans. tv-remoteis TV-only. Android phones, iPhones, and WebDriver (Appium) sessions refuse it withUNSUPPORTED_OPERATION.- All three TV backends support
up,down,left,right,select,menu,home, andback. Android TV also acceptsvolume-up,volume-down,mute,power,sleep,wake,media-play-pause,media-next,media-previous,media-rewind,media-fast-forward,channel-up,channel-down,last-channel,guide,info,captions,tv-input,color-red,color-green,color-yellow, andcolor-blue. tvOS and Vega OS refuse the additional buttons before dispatch. - For an Android TV remote button without a name, use
android-keycode:<n>with a decimal code from 1 to 2147483647. Other TV backends refuse raw Android codes. An unknown name or invalid code fails withINVALID_ARGS. powerandsleepcan turn the screen off. A successful response confirms that the button was sent; it does not verify playback, volume, or display state, and it does not wait for the app to settle.- Android TV: app listing reads
LEANBACK_LAUNCHERandLAUNCHERactivities together; app launch prefersLEANBACK_LAUNCHERand falls back toLAUNCHERwhen the app declares no leanback activity.tv-remotesends ADB keyevents;longpress <button>selects the long-press preset, and any positive--duration-msuses Android's longpress keyevent, which has no exact hold time. - Fire TV: a Fire TV device or emulator running Fire OS connects as an Android TV device over ADB. On the device, enable ADB debugging in Settings > My Fire TV > Developer options, then
adb connect <device-ip>:5555and use--platform android --target tv. Discovery reads theamazon.hardware.fire_tvsystem feature, so a Fire TV that declares no leanback feature is still detected as a TV target and runs the Android TV backend. The Android snapshot helper requires Android 6.0 (API 23) or newer. - tvOS supports the same runner-driven commands as iOS (
snapshot,wait,press,fill,get,scroll,back,home,app-switcher,record, and selector flows).backis the Siri Remote Menu button,homeis Home, and the app switcher is a double Home press;--duration-msis an exact hold. Helpers that are simulator-only on iOS, such assettingsandpush, are simulator-only on tvOS too. tvOS refuses multi-touch gestures such asgesture pinch. - Vega OS: use
--platform vega --target tvwith Vega component IDs such ascom.example.app.main, and--serial VirtualDeviceto select the VVD explicitly. Commands go through the SDK-matched Vega CLI and VDA. The VVD supports app open/close,back,home, and the eight navigation buttons listed above, with exact holds. - Some Fire TV devices ship with Vega OS instead of Fire OS. Vega OS is a different OS and is supported only in the Vega Virtual Device: app inventory, snapshot, screenshot, selectors, install, touch/text/gesture, logs, and performance report unsupported for Vega targets, and physical Vega OS devices are not supported. Use the VVD display as visual truth.
- On Android TV and tvOS, use
screenshot --overlay-refsfor visual focus evidence, especially when focus metadata is missing or transient.
Desktop targets
--platform macos, or--platform apple --target desktop, selects the host Mac as adesktoptarget.- agent-device automates Electron and other Chromium-based macOS apps through the macOS accessibility tree. On the native backend,
snapshotturns the app's Chromium accessibility tree on before reading it; once on, the tree stays on for the app's lifetime, so other surfaces see it too. See the Chromium note under the native backend below. - Use an
appsession for normal app control:open,snapshot,click,fill,press,scroll,back,screenshot,record. In app sessions,screenshotcaptures the app window, not the whole desktop. - Use the other surfaces to inspect desktop-wide UI before choosing an app, mainly with
snapshot,get,is, andwait:frontmost-appis the focused app,desktopis the visible windows across the desktop, andmenubaris the active app's menu bar and system menu extras. To click or fill inside one app afterwards, open it in anappsession. open <app> --platform macosattaches to a copy of the app that is already running, including one you started yourself from a script or supervisor. It opens that copy by its own app path, so another build registered for the same bundle ID, such as an earlier dev build at another path, does not start beside it. If that copy has not finished registering with macOS, the open waits briefly for it before handing the URL over, so a copy that registers within that window keeps the arguments it was started with; a copy still unregistered after the wait, or copies running from more than one path, get the plainopen -b, which can start a second instance.open <app> --platform macos --surface menubartargets one app's menu bar extras. Prefer it for status-item apps, which often expose little through theappsurface.- Prefer selectors and
@refs; window positions shift between runs, so raw x/y is less stable. Useclick --button secondaryfor context menus, then runsnapshot -iagain. - On
frontmost-appandmenubar,pressandclickpost synthetic mouse events (desktopis inspect-only).--hold-mskeeps the button down (at least 40 ms, default 60 ms),--countsends independent clicks, and--double-tapsends double-click pairs. Apps like Finder read two clicks at the default interval as one double-click; pass an--interval-mslonger than the system double-click time to keep them apart. Long schedules get the time they need, and a cancelled or interrupted click releases the button.--jitter-pxis ignored on these surfaces. - macOS also supports
clipboard read|write,trigger-app-event,logs,network dump,audio probe,alert, and part ofsettings(see Settings helpers).audio probesamples host system audio and needs Screen Recording permission; see Debugging & Profiling. - Mobile-only commands are unsupported on macOS:
boot,shutdown,home,orientation,app-switcher,action-button,fold,install,reinstall,uninstall,install-from-source, andpush.
XCTest opens bring the named app to the front. open --surface frontmost-app takes no app target and opens the app that is already frontmost. --foreground requests an initial snapshot; it does not choose activation. Run agent-device help macos for the versioned contract.
Set AGENT_DEVICE_MACOS_APP_BACKEND=native to run macOS app sessions without XCTest. There is no Automation Mode overlay, the app can stay behind other windows, and your pointer stays yours while a drawn pointer shows each action.
With the native backend, each app you open on the app surface is a device of its own. Several agents can drive different apps of one Mac at the same time, in one daemon or in daemons that share a claims directory. Two sessions on the same app conflict. A desktop, menubar, or frontmost-app session holds the whole Mac, so it conflicts with every app session. open --wait waits for a conflicting session in the same daemon only; another daemon's session refuses the open at once. An app session:
- Opens its app in the background, so the frontmost app stays in front.
- Opens and closes only its own app.
open <url>opens the link in that app. Use a new--sessionfor another app or surface. - Refuses
clipboard,settings, andaudio probe, which act on the whole Mac, withUNSUPPORTED_OPERATION(reason: unsupported-device-scope). Use a--surface desktopsession for them. - Cannot see UI that another process shows, such as a system permission prompt.
Copies of one app need distinct bundle ids. Screenshots run one at a time across the Mac. Use one agent-device version per claims directory.
click,press, andfilluse accessibility actions on the element at the target point, which must be inside one of the app's on-screen windows, not its menu bar.typeinserts at the focused control or falls back to key events.scrollmoves the scroll bar under the front window's center. Electron and other Chromium-based desktop apps expose their web content in the snapshot: the firstsnapshotturns the app's Chromium accessibility tree on, and a snapshot warns when that tree did not populate.screenshotcaptures only the app's front window, even when it is covered. The front window is the app's frontmost visible, nonempty normal-level (layer-0) window on the current Space: floating palettes and app-modal alerts at window level 8 are skipped, and a sheet is captured as its own window. If that window cannot be captured,screenshotfails; it never captures another window.record,prepare,back,longpress,--hold-ms,--double-tap,--button secondary, gestures, and clicks on an element with no accessibility action fail withUNSUPPORTED_OPERATION(reason: unsupported-device-backend). Use the default XCTest backend for those.
Snapshot and inspect
--rawreturns the full provider tree, including offscreen nodes and structural wrappers. See iOS capture behavior and Android capture behavior.- Android snapshots install the bundled snapshot helper on first use; a source checkout must run
pnpm build:androidbefore Android verification. truncated: truemeans the capture hit a backend node limit and the last elements in document order are missing; see Truncated captures.viewport: { width, height }names the box node rects are measured in; see Coordinates and viewport.--scope <text|@ref>re-roots the snapshot at the first matching node; see Scope a snapshot.--actionslists custom accessibility actions an element merged away (iOS simulator only, rejected with--raw); see Snapshots.diff snapshot(aliassnapshot --diff) compares with the previous session baseline, then updates it.
Wait and alerts
waitaccepts a millisecond duration,text <value>, a snapshot ref (@eN), a selector, or strictabsent <selector>. Every form except a plain duration takes an optional[timeoutMs]. Selector waits, including those insidereplay, poll snapshot captures with the same backend and recovery asis.waitresolves selectors the same way asclick,fill,get, andis.wait absent <selector>is strict absence, not a visibility check: it passes only when a complete, readable capture has zero matches, so hidden or off-screen matches keep it pending. It rejects--scopeand--depth. Sparse, truncated, incomplete, and Android unreadable-content captures never satisfy it.wait @refneeds a snapshot from a priorsnapshotcommand. It resolves the ref to its label or text, then waits for that text, so a duplicate label can match a different element than the original ref.- Wait failures carry
error.details.reasonin--jsonoutput:wait_target_absent: a positive wait never found a match.wait_target_present: strictwait absentreached its deadline with valid captures that still contained matches.predicate_failedmeans strictwait absentcould not prove absence because no valid capture arrived, with the final observation/diagnostic preserved.wait_capture_stalled: no readable capture arrived. Retriable.wait_deadline_exceeded: a later capture used up the budget after an earlier readable capture.wait_readiness_exhausted: the deadline ended while the iOS runner was still starting (readinessPhase: runner-start) or the Simulator app was still being found (target-discovery). Retry with a timeout that covers that work. The runner start keeps going on its own budget (10 minutes by default when no startup timeout is set), so the retry joins it; a start that spends that budget fails withrunner_start_budget_exhausted, and the next command starts over.wait_landmark_identity_mismatch: a replay destination guard refused the match.wait_stable_timeout: the UI did not settle.
- Branch on
readableCaptures,waitedMs,matches, andfirstMatchinstead of parsing error text.firstMatchcarries identity and text only; absence failures make no visibility or rect claim. - Polling wait timeouts, and a replayed wait refused with
wait_landmark_identity_mismatch, also carrycaptures,readableCaptures, andpollsso you can see where the budget went. Each poll hasstartedMs,durationMs, and anoutcome:readable,unreadable,retriable,deadline,runner-restart, orreadiness. Long waits keep the first 5 and last 25 polls. A wait that never saw a readable capture reports the error its polls hit, with that error's code and retry details, instead of a generic timeout;wait stableand a never-readable strict absence keep their own diagnostics.logPathlinks the full request log. alert(same asalert get) inspects or handles system alerts on iOS simulator, macOS desktop, and Android permission dialogs. On iOS it also covers app-owned modal popups with native blocking markers. The[timeout]argument applies toalert waitonly, on every backend:alert wait <ms>takes a budget up to 2147483647 ms (~24.8 days), larger is refused asINVALID_ARGSbefore anything runs, whileget/accept/dismissrun their own fixed per-backend budgets and ignore it.- On the snapshot-derived backends (Android, Limrun iOS)
alert get --jsonanswers with a typedstatus:visible,none, orundecided(an alert-shaped capture that resolves to no single alert). Branch onstatus, not on the message. - On those backends,
alert acceptpresses the lasting grant when a permission sheet offers several: Android orders its grants by the platform's own ids (full-access "Allow all", then foreground-only, then always, then the picker-limited "Select photos and videos", then one-time) whatever the button language — this is what makes the Android 14+ partial-media sheet answer "Allow all" — and elsewhere the English label table decides ("while using the app" over "only this time"). Android acts only on the grant ids it declares with a role and a rank; a sheet carrying an allow- or deny-shaped grant id it does not declare (a vendor fork's own button, or a newer Android grant) is refused for bothacceptanddismiss—dismissanswers with the Back key, the cancel that grants nothing. A sheet whose choices no rank orders — or that names the same choice at two points — is refused withalert_button_undecidedanddispatched: no, never pressed by position; read the labels withalert getand press your choice by label/ref. - On those backends, when
alert accept/dismissfails before pressing anything,error.details.reasonnames why (a capture or transport failure that is not a driver refusal may instead surface as its own error without areason). A budget never cancels a running capture or press: it only ends the polling loop, so every check that started inside the budget finishes and counts. Before any press,dispatched: noand the reason isalert-not-found(the action budget ended with no alert on screen),alert_capture_undecided, oralert_button_undecided(the sheet is too ambiguous to act on). After a press,dispatchedis absent and the reason isalert-window-expired; “did not dismiss the visible alert” means the latest post-press capture that answered named the same dialog again, “could not be confirmed” means the press went out but the latest read was unresolvable or never answered, so the dialog may already be gone. If you cancel the request at any point from the press onward — the tap in flight, or the post-press checks — the cancellation reportsdispatched: unknown: the device may already have received the tap. The one exception is a cancel whose post-press check nevertheless finished and proved the alert gone; then the command reports the handled alert, because the outcome is known. Runalert getbefore acting again. - On those backends a slow capture is never lost to its budget: a check that straddles the deadline still answers, which is why a quiet
alert accept/dismissalways reportsalert-not-foundand never a timing-dependent failure (alert getanswersstatus: noneandalert waittimes out on a quiet screen). The cost is a wait: the command stays open until the capture it started returns. On Android your cancellation stops the capture and the tap (the adb child is killed). On Limrun a request already sent to the instance cannot be stopped: the command ends when that last request answers (the Limrun client gives a request that stops answering its own ~30-second timeout) or the connection drops, and cancelling does not free it early. If a Limrun alert command stalls, restarting the instance session is what clears it. - Use
alert getfor a quick check, andalert wait <short-ms>only when a prompt may appear after async work. Don't usesettings permissionto answer a dialog already on screen; it is for setup before a flow. acceptanddismissare sent once on every platform and never replayed, even if the response is lost. On iOS the runner activates the button once, then only observes until the alert disappears, changes, or the deadline expires. A changed alert does not prove a permission was granted; verify the app outcome.- The Apple XCUITest runner (iOS, tvOS, and visionOS simulators and devices) and Limrun iOS use the same English label preferences after finding an alert:
Allow While Using AppoutranksAllow Once, and equal-ranked choices refuse.Open,Done, andConfirm…accept;Keep Browsing,Deny, andSkipdismiss. Unknown labels refuse even for a lone button; readalert get, then choose explicitly withsnapshot -iandpress @ref. Limrun still needs at least two actionable buttons to recognize an alert. Ordinary Android dialogs also recognize these labels; Android permission-button ids keep priority. - On the Apple XCUITest runner,
dismissactivates only a button its decline list recognizes (Cancel,Don't Allow,Ask App Not to Track, and similar English labels), or the close button of a popover whose native marker reads "dismiss popup". It never taps anything else: unknown, reworded, or fully localized decline copy fails witherror.code: ALERT_BUTTON_NOT_FOUNDanddetails.dispatched: no, tapping nothing, rather than guessing at an affirmative (#3627). A button choice that finds no button to name answers with that code anddispatched: noforaccepttoo. To answer a prompt with unrecognized wording, read the labels withalert get, then press the button by label throughsnapshot+press @ref. The macOS helper'sdismisskeeps its own rules (the app's cancel-button attribute, its label list, and a last-button fallback). - An unreadable or ambiguous capture after the action fails with
error.details.runnerErrorCode: ALERT_CONFIRMATION_UNAVAILABLE; an expired runner deadline fails withALERT_DEADLINE_EXCEEDED. Neither proves the alert is gone or that nothing happened. Runalert getbefore acting again. - iOS runner refusals carry
error.details.reason:runner_busymeans the runner ran nothing (dispatched: no);runner_main_thread_timeoutmeans the action may still land (dispatched: unknown). - If a sheet or permission prompt is visible but
alertreports none, treat it as app UI: runsnapshot -i -s "<visible label>", thenpress @ref. Android alert support is snapshot-derived, and not every iOS simulator permission surface is a native alert.
Interactions
A successful click or press means the action was sent to where the command found the target. It does not confirm that the element received it or that the screen changed. When the next step depends on it, add --verify (reports changedFromBefore), add --settle (returns the diff once the UI goes quiet), or follow with wait <selector>. Before retrying a failed interaction, see Retry after a failed command.
A click or press whose target the daemon resolves to a point (@ref or selector) re-checks that target's position before dispatching when the session's previous action animated the screen, and dispatches where it came to rest. When that check cannot confirm a stable position — the target was still moving, or a re-check could not measure it — the command refuses with target_still_moving and dispatched: no: nothing was dispatched, the point the command resolved is in error.details.rect, and where the target was last seen is in error.details.movedToRect when that last look measured one. Run wait <selector> on the target and retry the same command, or add --settle so the action waits for the UI to go quiet. A refused @ref press needs a fresh snapshot -i and a selector, because the wait retires the ref. Two dispatch paths hand off before the check: a press at raw coordinates has no resolved target to re-check, and a dispatch the backend performs on the element itself — click @ref on a backend with element dispatch, or a Maestro non-hittable coordinate fallback — goes to that backend rather than through a resolved point.
Text entry
fillclears then types.typeappends to the focused field and accepts text only: usefill @ref "text"to target a field, orpress @refthentype "text".- Use plain
fillortypefirst. Add--delay-msonly when a debounced or search-as-you-type field misses characters, or the app must receive each incremental update; delayed typing enters characters one at a time instead of using a clipboard-style fallback. fill --text-stdinreads the text from stdin so a secret never appears in process arguments. Pipe it withprintf %s. It reads at most 64 KiB, removes one trailing newline, and refuses a terminal, empty input, invalid UTF-8, or an extra text argument. Output shows the value as${VAR}with--record-as <VAR>and as[REDACTED]otherwise, including in error messages and details. While script recording is armed, pass--record-as <VAR>or--no-record; passing neither is an error. Batch steps don't accept it.- The privacy controls for typed input (
--text-stdinand--record-as) keep the literal out of agent-device's own responses, diagnostics, and recorded scripts. They do not rewrite what the platform records: on iOS, XCTest writes its own diagnostic logs and its.xcresultbundle, and typed text can appear in both, on a simulator or a physical device. Treat those as sensitive, and use test-only credentials when they may be kept or shared. See Security & Trust. - Android text entry is owned by
agent-device. Don't fall back to rawadb, clipboard, or paste; if non-ASCII is unsupported on the current backend, report the gap. Androidfillverifies the text and fails instead of retrying when the IME captures input.
Text-entry errors and what to do:
TEXT_INPUT_NOT_FOCUSED(type): focus a visible text input and retry. If accessibility does not expose the input, focus it by coordinates.NO_TEXT_INPUT_AT_POINT(iOSfillunder a degraded XCTest channel — coordinate, ref, selector, and replay fills all aim at a point): the runner identified no text input under that point, so it refused before dispatching anything (error.details.reasonisno_text_input_at_point,dispatched: no) and no field changed. A ref or selector fill aims at the centre of the rect it resolved to, so an outer container or a non-input node can land outside the real field: runsnapshot -i, pick the input node itself, and aim inside its rect.TEXT_INPUT_PROBE_UNAVAILABLE(iOSfillunder a degraded XCTest channel): the runner's input lookup for the point could not answer, so it refused before dispatching anything (error.details.reasonistext_input_probe_unavailable,dispatched: no). Unlike the refusal above this does not mean the point has no input — retry the fill, or runsnapshot -ionce its capture reads back.TEXT_INPUT_SYNTHESIS_UNAVAILABLE(iOStype "\n"with the software keyboard hidden): show the software keyboard, then retry.TEXT_INPUT_COMMIT_NOT_OBSERVED(iOSfill): the runner could not confirm the text landed before its deadline, or the text equals the field's placeholder. The field may hold none, part, or all of it. Runsnapshot -i; if it doesn't match, retryfillwith the full text quoted and--delay-msset to the value in the error hint.TEXT_INPUT_CLEAR_NOT_OBSERVED(iOSfill): the old text was not seen cleared, so nothing was typed. Inspect withsnapshot -i, then retryfill.TEXT_INPUT_SYNTHESIS_BUDGET_EXCEEDED(iOSfill): the text is too long to type in one runner command, and nothing was typed. Fill up to the character limit the hint names for your--delay-ms, then append the rest withtype.--delay-mslowers the limit; a longer timeout does not help.TEXT_ENTRY_MISMATCH(iOSfill): the settled value contains only characters, in order, from the field's value after clearing plus your text. That covers dropped characters and an unchanged value. This can also reject legitimate filtering, trimming, or masks (5into$0.00may show$0.05). Inspect the field and retryfillonly if it needs correction, using the--delay-msvalue from the error hint.
Except for TEXT_INPUT_SYNTHESIS_BUDGET_EXCEEDED, where you append the rest with type after the shorter fill, recover from these with fill, never type, which appends to whatever the field already holds. iOS fill types the replacement once and does not clear and retype a mismatch on its own.
On iOS, a single-line fill ending in a submit key, or a field that became unreadable or disappeared after typing, succeeds without verification; assert the resulting screen.
fill returns verification: "unconfirmed" when the field's settled value differs from the request but no failure is proven, such as app formatting (00 062 91 77 for 000629177, $10.00 for 1000) or an OTP field announcing 6 of 6 digits. Missing, reordered, duplicated, or stale text can also produce it. Inspect requested, before, and after, then assert the value or the resulting screen:
Pointer and gestures
click --button secondaryopens a macOS context menu;--button middleis unsupported on macOS.longpressis supported on iOS and Android.hoveris web only: it moves the pointer over a target without pressing so hover-gated UI appears, and--settlereturns what it revealed. Touch platforms reject it; uselongpressfor the hold gesture.swipeis a quick, fixed-duration throw.gesture pan x y dx dy [durationMs]is a deliberate timed drag with one pointer, or two with--pointer-count 2. Repeated swipes allow at most 200 repetitions, 10000ms pauses, and 60000ms in total.gesture fling up|down|left|right x y [distance]is a fast directional throw. Neitherswipenorgesture flingtakes a duration, andgesture rotatetakes no velocity; see Rewrite Old Gesture Commands to update old scripts.gesture drag source destination [sourceHoldMs] [moveMs] [destinationHoldMs]holds one pointer down from source to destination. Endpoints are selectors or refs, resolved before dispatch and recorded as selector chains. Defaults are 800, 500, and 0ms, capped at 10000ms in total. It works on Android touch devices and iOS/iPadOS; other backends refuse it before touching the screen.gesture pinch <scale> [x] [y],gesture rotate <degrees> [x] [y](the sign sets direction),gesture transform x y dx dy scale degrees [durationMs], and two-fingergesture panwork in Android, iOS, and iPadOS app sessions, on simulators and physical devices. On Apple they use the XCTest runner, so iOS sessions reached through a WebDriver provider refuse them. One-fingergesture panworks wherever coordinate drags do. A path that does not fit the viewport fails before injection instead of being clamped. Useorientationto rotate the device.gesture tap x1 y1 x2 y2 [... x5 y5]touches 2 to 5 distinct points and holds them for--hold-ms(default 50ms, at least 16ms) before lifting.--countrepeats the same contacts up to 200 times with--pause-msbetween repetitions; the series must fit in 60000ms, counting about 500ms of overhead per repetition. It works in Android and iOS/iPadOS app sessions; WebDriver providers refuse it unless they declaretapGesturesupport. On iOS the contacts touch down and lift together; on Android they touch down up to 8ms apart and lift 8ms apart in reverse order. Three or more fingers can trigger iOS system gestures such as undo, so verify app state afterwards. Usepressfor one finger.- Cancelled
gestureandswiperequests returnCOMMAND_FAILEDwithdetails.reason=request_canceledand record no action. Repetition pauses are interruptible. - App recognizers may not report exactly the requested pan, scale, and rotation. Verify app-level state such as
pan changed yes, and prefer separategesture pan,gesture pinch, orgesture rotatecommands when exact values matter. Seeagent-device help gestures.
Scroll
scroll <direction> [amount]takes the finger path as a fraction of the viewport (0.5is half), or--pixels <n>for a fixed distance. Large distances are clamped. The app's scroll physics decide the final offset, so a scroll does not land on an exact position.scroll <direction> --until <selector>repeats passes until the selector is on screen and fails if the content runs out first. It stops after at most 12 gestures by default. For a longer list, usescroll down --until 'id="row-512"' --max-passes 90(1–200 gestures). The pass limit does not extend the 90-second request timeout. Large pass budgets can hit that timeout; inspect the current screen and continue with another search if that happens. Add an amount (scroll down 0.8 --until <selector>) to widen each pass. Use it in.adscripts too, instead of fixed amounts that depend on screen size; see Replay & E2E.- Default snapshot text summarizes off-screen interactive content instead of listing refs.
scroll <direction> --settle(andback --settle) waits for the UI to go quiet and returns the diff with fresh refs, so you don't need anothersnapshot -i. Settle is best-effort and never fails the action. - On Android, settle ignores an allowlist of system-bar containers and shade/keyguard status headers; other SystemUI overlays remain content. Appium settle also applies package rules for keyboard and system chrome.
- With a keyboard up on iOS or Android, the swipe stays above the keyboard (reported as
keyboardAvoidedandkeyboardMinY) and the keyboard stays open. If too little room is left, the command refuses withscroll_keyboard_occludes_surface; runkeyboard dismissfirst. - A directional scroll reports what it observed as
movement:moved,at-edge(no change and no more content that way),unchanged(no change, no edge signal), orunobserved(no reliable comparison). If nothing changed, the swipe ran inside a container, and that container still has hidden content in that direction, it refuses withscroll_no_progress; the hint names the usual causes: a focused keyboard, a nested scroller, or a list that needs a rawswipe. Seeagent-device help scroll.
Retry after a failed command
When a command fails, read error.details.dispatched before you retry:
no: the action never reached the device. Checkdetails.reasonbefore retrying; a bounded resource refusal must run as a separate command.unknown: the action may have landed. Take a snapshot before you retry; a blind retry can tap, type, or navigate twice.- Missing: no guarantee. Treat it as
unknown.
For a failed batch or replay, inspect completed steps and details.dispatchedSteps first. A
resource step refused with details.reason: bounded_composite_resource_unsupported has
details.dispatched: no for that step. details.standaloneCommandName names a known public
command; it is not a complete command. Run the original step separately with its original
arguments and options, the same --session value, and the same private CLI context. Earlier
steps may still have run.
Read-only commands such as get, snapshot, and wait report no, and so do record, trace,
and perf, whose controls the device refuses to repeat. On Android, dismissing a blocking system
dialog or ANR prompt first does not count as a dispatched step. For other failures, once any step
of a request reached the device, the failure is unknown and error.details.dispatchedSteps
counts those steps. A bounded resource refusal reports no for its refused step even when earlier
batch or replay steps changed the app; check dispatchedSteps before retrying.
These refusals carry dispatched: no and a typed error.details.reason, so branch on the reason, not the message:
session_or_device_selector_required: the request had neither an active session nor a device selector. Open a session or pass--platform. Commands that require a session or a device selector before they read the request's target (boot,clipboard,push,apps, and siblings) answer this even when the request named a session the daemon does not hold.named_session_not_found: the request named a session this daemon does not hold, with no device selector to fall back on (or no selector it can answer without the session —appstateon an Apple target refuses this even with an explicit device). The daemon lost its sessions (replaced or restarted), or the name is wrong;agent-device session listshows what it holds. Commands that require a session before they read anything else (interactions,findclicks, RN tooling,screenshot-style generic commands) answer the same missing-session cause as a message-levelSESSION_NOT_FOUNDcarrying nodetails.reason— some carry adispatchedstamp from the disclosure layer, some carry nodetailsat all; read the cause from the message, and read a presentdispatchedwith its usual meaning.session_app_required: a per-app setting was requested but the session binds no app and none was named. Runopen <app>, or name the app on the request for a setting that takes one (settings clear-app-state com.example.app).app_not_installed: the named app resolves to no installed app on the target. Runapps --allfor the exact bundle id or package, or install the app, then retry.webdriver_route_unsupported: on a WebDriver-backed device cloud, the driver does not implement the route; the limit is the cloud's driver, not the device. A command with a second route (orientationhas two) tries it before failing. After an ambiguous failure on that transport, only read-only requests are resent.
Find (semantic)
find locates an element by text, label, or role and runs an action on it (click by default). See Selectors for the action list, ambiguous matches, and --first/--last.
Assertions
isevaluates UI predicates against a selector expression and exits non-zero on failure. It takes selectors, not refs like@e3, and the same snapshot flags asclick,fill,get, andwait.- Supported predicates are
visible,hidden,exists,absent,editable,selected,focused, andtext. is visiblepasses when the element is in the visible viewport, or has no rect of its own but sits inside a visible ancestor.is existsonly checks that the selector matches.is text <selector> <value>compares the element's text with the value.wait textchecks that text is present, not that it can be tapped.is absentpasses only when the selector has zero matches in one readable, complete, settled, unscoped, full-depth capture. It does not mean hidden:--scopeand--depthare rejected, and sparse, unreadable, truncated, or unsettled captures fail.- Predicates that name one element (all except
existsandabsent) andget attrsfail withAMBIGUOUS_MATCHwhen the selector matches more than one node; that is not a missing element.error.details.matcheshas the count, and up to five candidate@refs are listed, pinned toerror.details.refsGeneration, so you can act on one directly. Without a stored tree (a sparse capture), only the count is reported. See Selectors. - On iOS, React Native text appears as two nodes with the same label. Selectors that must resolve one element (
is,get attrs,screenshot --crop-on, replay) treat the pair as one;find ... listandsnapshot --jsonshow both. - On the web, on-screen text the browser cannot hand an action to (static text, a
statusannouncement) is in the tree and readable bywait text,find text,get text, andtext=, but carries no@refand has no rect:find ... listprints it as(no ref), and it is never a tap target. Such text is published onvalue, sovalue="X"can match page text as well as a field holding that text, and a predicate that needs one element then reportsAMBIGUOUS_MATCH; usetext=for page text, or addeditable=trueto name the field. - The first read after
scroll,swipe, orgesture swipewaits for two captures to agree. When that proves something about the gesture,is,get,find,wait, interactions that captured it, andsnapshotadd a warning, and all butsnapshotreportpostGestureOutcome({ "kind", "gesture": { "action", "positionals" } }) indataorerror.details:kind: "unsettled": the screen was still changing. A miss is not proof of absence; read again.is absentrefuses withobservation: "unsettled", andwait absentkeeps polling.kind: "no-effect": nothing changed. The list may be at its edge or ignore synthesized scrolls; a rawswipe x1 y1 x2 y2inside it moves such lists.
Replay
--save-script [path]onopenrecords a replay script when the session closes.replayruns deterministic.adscripts. Script paths are read on the calling machine, so the same command works with a local or remote daemon.--keep-sessionskips only an authored finalclose.testruns.adfiles, directories, or globs as a serial suite and streamspass/fail/skipon stderr (--verboseprints every result).--platformfilters by each script'scontext platform=...header,--timeoutand--retries(max3) apply per attempt, and--artifacts-diroverrides.agent-device/test-artifacts.- A failing step returns a
REPLAY_DIVERGENCEreport;replay --from <n> --plan-digest <sha256>resumes at plan stepn(testrejects--from). See Resume a failed replay. replay -u/--updateis accepted and does nothing; it does not rewrite the script.
See Replay & E2E for recording and CI workflow details, and Run Maestro flows for Maestro YAML.
Batch
batchruns a JSON array of{ command, input, runtime? }steps in a single daemon request and stops at the first failing step.--max-steps <n>sets the per-request step limit.- In non-JSON mode, successful batches print a short per-step summary.
See Batching for payload format, response shape, and usage guidelines.
App install (in-place)
install <app> <path>installs from a binary path without uninstalling first, keeping existing app data where the platform allows. Use it for upgrade flows.- Supports Android devices/emulators, iOS simulators, and CoreDevice-backed iOS physical devices. On xctrace-only devices, install the app with Xcode before opening it by bundle ID.
- Formats: Android
.apk/.aab, iOS.app/.ipa, HarmonyOS.hap. For an.ipawith several app bundles,<app>picks one by bundle id or name. .aabrequiresbundletoolinPATH, orAGENT_DEVICE_BUNDLETOOL_JAR=<absolute-path-to-bundletool-all.jar>withjavainPATH; it installs a universal APK.- Remote daemons upload local app files automatically; prefix the path with
remote:to use a path on the daemon host as is.
App reinstall (fresh state)
reinstall <app> <path>uninstalls and installs in one command, for login/logout resets and deterministic test setup. Device coverage, formats, and remote upload matchinstall.
App uninstall
uninstall <app>removes an installed app, for example to start a test without the app installed. Android (ADB), iOS simulators, and CoreDevice iOS devices are supported.- A package name or bundle id that is not installed succeeds with
wasInstalled: false; another name that matches nothing installed fails withAPP_NOT_INSTALLED.
App install from source URL
install-from-source <url>installs an artifact the daemon can reach at a URL; useinstallorreinstallfor local files. Device coverage matchesinstall.- A URL may point to an Android
.apkor.aab, or, from any public host, to an archive containing one.apk,.aab,.ipa, or iOS.app. --github-actions-artifact <owner/repo:artifact>lets a compatible remote daemon fetch a GitHub Actions artifact with its own credentials. A numeric artifact is treated as an id, anything else as a name.- Repeat
--header <name:value>for authenticated or signed downloads. Sensitive headers are not forwarded to other origins. --retain-pathskeeps the downloaded and extracted files after install;--retention-ms <ms>sets how long.- Limits: at most five redirects, no HTTPS-to-HTTP downgrade, 2 GiB download, 4 GiB extracted, 100,000 entries, and three nested archive layers. Links and special archive entries are rejected.
HTTP_PROXY,HTTPS_PROXY, andNO_PROXYare honored. These match the Node.js client'sinstallFromSource()checks.
Push notification simulation
push <bundle|package> <payload.json|inline-json>simulates push notification delivery to the session device or the one named by--platform,--device,--udid, or--serial.- iOS: simulator only, with an APNs-style JSON object payload.
- Android: sends a broadcast with the payload
{"action":"<intent-action>","receiver":"<optional component>","extras":{...}}. Extras may be strings, booleans, or numbers.
App event triggers (app hook)
trigger-app-event <event> [payloadJson]opens your app's deep-link hook with an app-defined event; it does not post OS-level notifications.payloadJsonmust be a JSON object.- Needs an active session or a device selector (
--platform,--device,--udid,--serial). On iOS physical devices, open the app in the session first. - Set the URL template with
AGENT_DEVICE_APP_EVENT_URL_TEMPLATE, or per platform withAGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE,AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE, orAGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE. Placeholders are{event},{payload}, and{platform}, for examplemyapp://agent-device/event?name={event}&payload={payload}.
Settings helpers
Platform support:
- iOS
settingsworks on simulators only. - macOS supports only
settings appearance <light|dark|toggle>andsettings permission <grant|reset> <accessibility|screen-recording|input-monitoring>. settings appearancesets macOS appearance, iOS simulator appearance, or Android night mode.settings location set <lat> <lon>sets coordinates on iOS simulators and Android emulators.- A target that does not serve a setting refuses it with
UNSUPPORTED_OPERATIONanderror.details.reason(for exampleunsupported-platform-leaf) before anything changes. A request that names an app the change cannot take is refused first, withINVALID_ARGSandsetting_app_not_consumed.settings --helpdescribes where each setting runs.
Device settings:
settings animations off|on(Android) sets the window, transition, and animator scales. Turn animations off to stabilize runs with heavy animation, and back on afterward.settings bluetooth on|off(Android over adb) turns the Bluetooth adapter on or off withsvc bluetooth, from Android 8.1. A device or emulator image without a Bluetooth adapter fails withUNSUPPORTED_OPERATIONanderror.details.reason: android_bluetooth_adapter_missingwithout changing anything. A device that has an adapter but runs a build older than Android 8.1 fails the same way withandroid_bluetooth_command_unavailable. It waits up to 2 seconds for the device to hold the requested state and reports it asbluetooth; a change the device refuses, such as one a device policy blocks, fails withCOMMAND_FAILEDanderror.details.reason: bluetooth-not-applied, orbluetooth-unreadableif the final read is not a known state. The app may take longer to react, so poll it rather than asserting immediately.settings airplane on|off(Android) needs the connectivity service'scmd connectivity airplane-modecommand, available from Android 11. It reports the resultingairplaneMode; a build without that command fails withUNSUPPORTED_OPERATIONwithout changing anything. Connectivity takes a moment to settle, so poll the app rather than asserting offline behavior immediately.settings mobile-data on|off(Android over adb) switches mobile data withsvc data enable|disableand reports the resultingmobileDatafrom the globalmobile_datarow. If that row does not settle to the request within 2 seconds, it fails withCOMMAND_FAILED:error.details.reasonismobile-data-unreadableif the final read is neither0nor1, ormobile-data-not-appliedif it is the opposite state. Pair it withsettings wifi offto move a session such as a video stream from Wi-Fi to cellular, and back withsettings wifi on. Poll the app while the network hands over.- The read-back covers single-SIM targets only. A multi-SIM device stores the switch per subscription and can leave
mobile_dataunchanged, so the command may fail there even when the switch took effect. A target without an active SIM can also acceptsvc datawithout changing the row, and with Wi-Fi off it is offline rather than on cellular. - Mobile data and Bluetooth are device-wide, so
--appis refused withsetting_app_not_consumed. Every target without adb refusesmobile-datawithUNSUPPORTED_OPERATIONbefore anything runs. settings text-sizereads the current size;settings text-size <category>sets one:extra-small,small,medium,large,extra-large,extra-extra-large,extra-extra-extra-large,accessibility-medium,accessibility-large,accessibility-extra-large,accessibility-extra-extra-large, oraccessibility-extra-extra-extra-large. It works on iPhone and iPad simulators and on Android; every other target refuses both read and write. The read reportscategoryand the target's ownplatformValue(iOS may capitalize it, for exampleextra-Small).- Android has only a font-scale multiplier, mapped so
largeis1.0andaccessibility-extra-extra-extra-largeis3.2. A value between rungs reads as the nearest category, with the exact multiplier inplatformValue; an unset scale reads aslarge. - A running app adopts a new text size only at its next configuration change. Relaunch it (
open <app> --relaunch) before asserting rendered sizes.
Biometrics:
faceidandtouchidwork on iPhone and iPad simulators with any Xcode; Apple TV and Vision Pro simulators refuse them.enroll/unenrollchange enrollment.fingerprintworks on Android targets wherecmd fingerprintoradb emu fingeris available; physical devices usecmd fingerprintonly.matchandnonmatchsimulate a valid or invalid Face ID, Touch ID, or fingerprint.
App state:
settings clear-app-state [app-id]clears the data of the session app or the named app (positional or--app, not two different ones). On Android this removes preferences, databases, files, and cache; on iOS simulators it empties the app's data container. It is unsupported on iOS physical devices and macOS.- It does not touch the keychain, so keychain-backed credentials such as Firebase auth survive.
settings reset-keychain clearresets the iOS simulator keychain for every app on that simulator, not only yours; pair it withclear-app-statefor a fresh-install reset. It is unsupported on iOS physical devices, Android, and macOS. - An app with no data container, such as a simulator's built-in
com.apple.Preferences, refuses withUNSUPPORTED_OPERATIONanderror.details.reason: app-no-data-container. Built-in apps cannot be uninstalled either; erase or recreate the simulator to reset one.
Permissions:
- On iOS and Android,
permissionand iOS simulatorlocation on|offapply to the session app. Pass--app <bundle-id, package, or name>to target an installed app without opening it, for example to grant a permission before first launch. The app need not be running. Unlikeopen, which passes an unknown id through, settings resolve only installed apps, the same wayclear-app-statedoes. On an iOS simulator, a dotted name is first matched against installed bundle ids, and any name against installed display names (--app Mapsiscom.apple.Maps). On Android, a package must be installed, and any other name is matched against installed package names. A Limrun iOS session resolves only thesettingsalias. --appcounts only when passed on the command line;AGENT_DEVICE_TARGET_APPandtargetAppin~/.agent-device/config.jsonnever retarget a settings change.- Refusals for
permission,location, andclear-app-state, all withdispatched: noand nothing changed:app_not_installed(APP_NOT_INSTALLED): the app resolves to nothing installed.session_app_required: no session app and none named. Runopen <app>, or name the app.setting_app_not_package(INVALID_ARGS, Android): the name is an intent alias such assettings, not a package.setting_app_not_consumed: the change is device-wide and cannot take an app:location set, the Androidlocation on|offtoggle, macOS permissions,mobile-data, andbluetooth.
- iOS targets:
all,camera,microphone,photos(fullorlimited),contacts,contacts-limited,notifications,calendar,location,location-always,media-library,motion,reminders,siri. The simulator decides which it can change; one it refuses fails withUNSUPPORTED_OPERATIONnaming it. On current runtimes that isnotifications, whichallalso leaves untouched. - Android targets:
all,calendar,camera,contacts,location,media-library,microphone,notifications,photos. Targets covering several permissions apply only the ones the app declares and fail if it declares none;allapplies every declared permission. The response reportspermission(what you asked for) andpermissions(what was changed). - macOS targets:
accessibility,screen-recording,input-monitoring.grantchecks or requests access and opens System Settings when needed; it never grants silently.denyis unsupported. full|limitedapplies only to iOSphotos.- Android
resetrevokes. Changes apply to the device's foreground user; if it cannot be determined, the command fails withCOMMAND_FAILEDand changes nothing. - Android kills a running app when one of its granted permissions is revoked.
denyandresetreportpriorGrantState(granted,not_granted, orunknown) and warn you to runopen <app> --relaunchunless it wasnot_granted.
App state and app lists
- Android and HarmonyOS
appstatereport the live foreground package and activity. - iOS and macOS
appstateneed a session and report that session's app:appName,appBundleId, and asource. On iOS, when a live runner answers the read, the answer carries itsstate(runningForeground,runningBackground,runningBackgroundSuspended,notRunning, orunknown) withsource: runner. They never start a runner; when no live runner answers — none is running, or it cannot reply right now — they reportsource: sessionand nostate, and on macOS the answer is always session-scoped. On iOS, afterhomethe app reports a background state; which app took the screen is not reported. A command that had to bring the app back reports atargetActivationdisclosure. appsshows user-installed apps. Add--allto include system and OEM apps.
Foreground repairs on iOS
An iOS session is bound to one app, but a deep link, a system sheet, or simctl openurl can hand the
screen to another app. The next command brings the session app back so it can answer, and reports
that repair:
The snapshot response carries targetActivation and a warning:
- Anything captured earlier in the session described the other app. Capture again, or drive the other app in its own session.
reasonnames the check that caught it:bundle_changed(the runner's cached target differs from the requested bundle),stale_target(the cached target no longer answers as foreground),missing_after_wait(a wait never saw the app), orinteraction_foreground_guard(an interaction's guard tripped before dispatch).priorStateis the app's state before the repair; it is neverrunningForeground.otherActiveApplicationPidappears only when exactly one other app held an accessibility session. It does not prove that app owned the screen, and its absence is not a failure.snapshot,find,get,is,wait, and interactions that captured a tree report it at every--level, includingdigest; on failure it is inerror.detailsand the hint. In text mode it prints as aWarning:line after any earlier warnings; see Output and warnings. Reads answered from a cached tree did no repair and report none.- Silence is not proof. A coordinate
press, apress @refanswered from a live ref frame, or await <text>satisfied on its first poll may re-activate the app without saying so. When it matters, runsnapshot -iand read its disclosure (#2694). - No read launches a stopped app.
snapshot,wait,is,get, a readingfind, and the leading read ofgestureor a selectorclick/fillbring a backgrounded app forward but never start one that is not running, because a launch could drop a URL waiting behind an "Open in …?" prompt. They fail withCOMMAND_FAILED,details.runnerErrorCode: "APP_NOT_RUNNING", andretriable: true;waitkeeps polling. Answer the prompt withalert accept, or relaunch withopen. Commands that act without reading first (press, coordinatefill,swipe,scroll, hardware keys) still start a stopped app.
Clipboard
clipboard readreturns the target's clipboard text;clipboard write <text>replaces it. Treat read output as sensitive: it can contain copied secrets.- Works with the session device or a device selector (
--platform,--device,--udid,--serial). - Supported on macOS, iOS simulators, and Android builds whose clipboard service answers
cmd clipboard. iOS physical devices returnUNSUPPORTED_OPERATION. - On Android 16 (API 36), both actions return
UNSUPPORTED_OPERATIONandcapabilitiesomitsclipboard; see Known Limitations.
Keyboard
keyboard status(orkeyboard get) reports keyboard visibility and a best-effort input type on Android emulators and devices.keyboard dismisstaps a safe control such asDonewhen available and checks that the keyboard closed. It works on Android and, best-effort, on iOS simulators and devices. If it returnsUNSUPPORTED_OPERATION, press a visible app control such asDoneonly when that is the intended fallback.- Works with the session device or a device selector (
--platform,--device,--udid,--serial).
Performance diagnostics
- Name one area:
frames,memory,cpu, ortrace. Bareperf,perf sample,perf metrics, and themetricsalias fail with guidance to the focused replacements. perf framesreturns frame/jank health (droppedFramePercent,worstWindows).perf memory samplereturns a compact process memory sample in kilobytes.perf memory snapshotwrites a heap or memgraph artifact and returns its path.perf cpu profileandperf tracerecord Apple (--kind xctrace) or Android (--kind simpleperf,--kind perfetto) profiles and traces, and return artifact paths plus compact metadata; raw contents stay on disk.cdpsamples and snapshots the React Native JavaScript heap through Metro CDP; see Debugging & Profiling.- App startup duration is returned in
open'sstartupresult. - Perf commands need an app session with a package or bundle ID; run
open <app>first. - Platform support:
perf frames: Android emulators and devices, and connected iOS devices. iOS simulators and macOS report it unavailable because Apple tooling exposes no trustworthy hitch data there.perf memory sample: Android, HarmonyOS devices, iOS simulators and devices, and macOS.perf memory snapshot --kind android-hprof: Android, for a running debuggable or profileable process that permits heap dumps.--kind memgraph: iOS simulators and macOS; physical iOS devices report it unavailable with a recovery hint. Native heap tracing (perf memory trace --kind heapprofd) is not available.--kind xctrace: iOS simulators, connected iOS devices where xctrace can attach to the app process, and macOS when the app process resolves from the bundle ID (forperf trace, when the template supports the target). It does not profile Android.--kind simpleperfand--kind perfetto: Android app sessions with a running package process only.- HarmonyOS is memory-only: CPU profiling, frame sampling, and memory snapshots are unavailable.
For the measurement workflow, data sources, and output examples, see Performance snapshots.
React Native components
treeprints the mounted component tree with@crefs. A chain of single-child wrappers, such as providers, prints on one line. Three or more consecutive siblings whose subtrees match exactly, in names, types, and logged errors and warnings, print as one counted line; other siblings, including same-name screens with different contents, keep their own lines. Pass a ref or a unique name to print one branch.findlists components whose name contains the text, ignoring case, or equals it with--exact, each with the components that rendered it.inspectprints a component's owners, props, state, hooks, and the errors and warnings it logged while rendering. It takes a ref, or a name only one mounted component has.errorslists the components that logged console errors or warnings while rendering, with the messages. When the app's React DevTools backend does not attribute every log to a component, as on React Native 0.74 and 0.76, it says the list can be incomplete.- These commands connect the same way as profiling, below. A connected React DevTools window owns the component tree, so
tree,find, anderrorsfail withtree-unavailableuntil it is closed and the app reloads.
React Native performance profiling
profile startbegins recording React renders in the app;profile stopends it and prints the commit count, render time, commits over the 16 ms frame budget, and up to five findings (--limitchanges that). It also saves the recording for the session, soreportandcommitread it without the app.profile reportlists components by self time with why they rendered;profile report <Component>shows every component of that name, such asRowand memoizedRow, each with its causes, changed props, owners, costliest commits, and findings.profile commitlists commits in order, or the slowest ones in order when there are more than--limit(default 20);profile commit <N>shows what rendered in one commit.--limitcaps the rows of the list views.- Profiling connects through the app's Metro dev server and needs no app or Metro setup. It works with development builds of React Native 0.74 and later on Android emulators, iOS simulators,
agent-device proxyconnections, and remote bridge sessions. On a remote bridge it uses the Metro server next to the agent: the session's binding frommetro prepare, or--metro-host/--metro-port. Metro accepts the debugger connection only from its own machine, so the bridge ormetroPublicBaseUrlcannot carry it. --app <bundle-id>picks the app when several share the dev server, and--metro-device <name>picks the device when one app runs on several.--metro-portand--metro-host, or--bundle-urlfor a dev server behind a URL path, pick the dev server when it is neither the session's Metro server norlocalhost:8081.- With
--json,profile stopreturns the findings and the path of the saved profile. - See Debugging & Profiling for the findings and the workflow.
React Native overlays
react-native dismiss-overlayclears a development overlay (redbox/LogBox error or collapsed warning banner) that blocks interaction, and is a no-op when none is present. It works on iOS simulators/devices and Android emulators/devices, and is the onlyreact-nativesubcommand.
Multiple React Native worktrees
You can reuse one installed iOS simulator debug build across local worktrees when its native binary is compatible with both JavaScript trees. Run one Metro server per worktree on its own port, then open the app on different simulators with explicit Metro hints:
- Use a different simulator and session per worktree; one simulator cannot run two copies of the same bundle id.
- On iOS simulators,
openwrites React Native's per-simulator debug server settings before launch. Expo dev clients ignore those settings, so when the app registers a singleexp+<slug>URL scheme,openlaunchesexp+<slug>://expo-development-client/?url=<server>instead; if the scheme check is inconclusive, the app opens normally.--launch-urloverrides this. A dev build that boots straight into its embedded bundle still needs a native rebuild. - Each session remembers its dev server for a flagless
metro reload --session <name>; see Metro reload. - This covers JavaScript and Metro-resolved changes. Rebuild and reinstall when native code, native dependencies, bundle identifiers, entitlements, or generated native project files change.
- Close every session when done:
Metro reload
metro reloadasks connected React Native dev builds to reload JavaScript without restarting the native process, like pressingrin the Metro terminal.- The target server comes from per-call flags first, then the dev server the session last bound through
metro prepareoropen's--metro-host/--metro-port/--bundle-url, thenhttp://localhost:8081/reload. A host or port flag overrides only that field. A reused session name never reloads a previous project's server:closeclears the binding, and so does a freshopenwithout Metro hints. Aftermetro prepare, reload uses the local server address even when the device loads through a public or proxy URL. - A bundle URL keeps its mount prefix:
http://host/tenant-42/.expo/.virtual-metro-entry.bundlereloadshttp://host/tenant-42/reload. - When the server has no HTTP
/reloadroute (Expo), the reload is broadcast over the server's/messagewebsocket. The result'stransportfield says which channel delivered it. - Use
open <app> --relaunchinstead when the app is not connected to Metro, reload fails, or the native process must restart. Seeagent-device help metrofor the full binding rules.
Media and logs
Screenshots
--scale <factor>resizes both dimensions, from0.01to1(full resolution). SetAGENT_DEVICE_SCREENSHOT_SCALE=0.3orscreenshotScalein config as a token-conscious default; an explicit--scaleoverrides it. Leave it unset or pass--scale 1for pixel-diff baselines.--max-sizeis refused everywhere (CLI,.adscripts, Node options, config,AGENT_DEVICE_SCREENSHOT_MAX_SIZE) with guidance to use--scale.- ADB-backed Android screenshots capture immediately by default. Use
--normalize-status-barto set deterministic status-bar chrome and wait for transient UI to fade. In the Node client,stabilize: truealso enables this wait. --overlay-refstakes a fresh snapshot and burns visible@eNrefs and their rectangles into the PNG. With--scale, refs are drawn at the final size, so avoid scales where labels become unreadable. In--jsonmode each ref also has a screenshot-spacecenterforpress <x> <y>fallback.--crop-on <selector>crops the capture to the frame the selector resolves on the same screen. It works only on iOS simulators, Android emulators, and physical iPhones. A physical iPhone crop is verified only in portrait using CoreDevice/devicectl; the XCTest backend and runner fallback are not accepted. Physical iPads remain rejected for crop, and landscape iPhone crop remains unverified. It needs exactly one framed node, cannot be combined with--overlay-refsor--fullscreen, and warns when the frame is clipped. The crop is re-encoded, so do not byte-compare it with an older crop.--fullscreenon macOS applies only to app sessions. Other surfaces (desktop,menubar,frontmost-app) always capture the main display and refuse--fullscreenwithINVALID_ARGS.--normalize-status-bartemporarily normalizes iOS simulator status-bar chrome for reusable baselines.- On single-display iOS simulators and Android devices, the result may report
displayRotation(portrait,portrait-upside-down,landscape-left, orlandscape-right). It is absent when the device returns no reading in time, on other targets, and on multi-display devices such as foldables. The saved image is always upright.
Screenshot diffs
diff screenshotcompares a live capture, or a savedcurrent.png(no session needed), against--baseline. Inputs may be PNG or JPEG regardless of extension; HarmonyOS captures are JPEG. Output ranks changed regions with rectangles, changed-pixel counts, and share of the diff;--outwrites a diff PNG with changed pixels tinted red.ocrandnonTextDeltasare never populated, so read the images yourself for qualitative review.--threshold <0-1>sets the per-pixel RGB tolerance (default0.1). Keep it above0when either input is JPEG.- The current image scales proportionally to the baseline width and aligns at the top left. Its scaled height rounds to the nearest pixel, with a minimum of one row. The comparison includes the full height of both images: extra or missing bottom rows count as differences, including blank or transparent rows, even at
--threshold 1. Your original files stay unchanged. This does not crop system bars or move content into alignment. - When dimensions differ, JSON
normalizationreportsbaseline,current,scaledCurrent, andcompareddimensions.currentScale.xand.ymap original current pixels to comparison pixels; the height scale includes pixel rounding. Region rectangles and ref-match rectangles use comparison pixels, and normalized region rectangles use percentages of the compared canvas. The separate current-screen overlay keeps its original capture size. --overlay-refsalso writes a separate current-screen ref overlay and lists refs under changed regions. It needs a live capture, so it is unavailable with a savedcurrent.png.- Live iOS simulator diffs normalize status-bar chrome by default.
Recordings
record startdefaults to app scope and needs a session fromopen <app>. Use--scope deviceor--scope systemfor whole-screen capture (multiple apps, settings, home screen) where the backend supports it.- iOS recording works on simulators and CoreDevice-backed physical devices. Physical devices capture runner screenshots, not a native video stream: no audio, dropped frames are expected, and the default is 15 FPS.
--fps <n>(1-120) caps the rate; a busy app or long command yields fewer frames. --quality medium|high(defaultmedium) sets export quality on Android and Limrun; Apple export always keeps the captured resolution. Numeric5-7map tomediumand8-10tohigh.- With touch visualization on, recordings also produce a gesture telemetry sidecar. Burned-in touch overlays export only on macOS hosts; elsewhere
record stopreturns the raw video and telemetry withoverlayWarning. The overlay renders at most 30 FPS (or--fpsif lower) within a fixed budget, which also covers the video checks that run before and after it; an export past the budget returns the raw video withoverlayWarning. On a host that has not run a recording before, part of that budget goes to one-time tool setup, so the firstrecord stopof a long recording is more likely to return the raw video. - Android uses
adb shell screenrecord(180s limit per clip). Longer recordings are split into MP4 chunks while the daemon stays alive; after a daemon restart,record stoprecovers only chunks listed in the device manifest. A clip can end beforerecord stopwhen the screen stopped changing;durationMsis wall-clock time, andcapturedDurationMsplus a warning report how much the video covers. - Limrun sessions always record the whole screen and refuse
--fpsand--hide-toucheswithINVALID_ARGS. A failed download is retried by the nextrecord stop; nothing survives a daemon restart. - A busy iOS simulator recording slot returns non-retriable
DEVICE_IN_USEwithdetails.reason: apple_simulator_recording_busy. Runrecord stopin the session that owns the recording; if no recording is active, ask the host operator to restart the CoreSimulator stream service. record stopis safe to repeat. If the request window ends during export, a secondrecord stopin the same session returns the finished recording. A recording whose video file is gone reportsno active recording.record stopmay also reportrecorder(confirmedorlost) andnativePathDisposition(retirableorretired). Seeagent-device help recordand ADR 0024.record contact-sheet <video.mp4> [--out <sheet.png>]turns an exported MP4 into one PNG grid of the frames where the screen visibly changed, each labeled with its elapsed time. It needs no session or device. The default output is<recording>.contact-sheet.png, and--outcannot point at the recording itself.--jsonadds per-cell times and change shares plussampledFrameCount,decodedFrameCount, andskippedSampleCount.- A contact sheet samples at most 48 times across the clip (every 250ms for short clips) and prints at most 24 cells, so a transient between samples can be missing;
warningsays when cells were thinned or samples skipped. It runs only on macOS hosts (details.reason: contact_sheet_unsupported_hostelsewhere) and refuses WebM withcontact_sheet_container_unsupported.
Session app logs: See Debugging & Profiling for the clean-repro and grep workflow.
logsworks on iOS simulators, iOS physical devices, Android, and macOS (app-scoped Unified Logging). On iOS, capture relies on Unified Logging (os_log); plain stdout/stderr may be limited.logs startappends toapp.logand rotates at 5 MB. Override withAGENT_DEVICE_APP_LOG_MAX_BYTESandAGENT_DEVICE_APP_LOG_MAX_FILES; setAGENT_DEVICE_APP_LOG_REDACT_PATTERNSto a comma-separated regex list for write-time redaction. On Apple simulators,logs startreturns once the simulator log stream is running, so lines that the app logs after that are captured. If the stream does not start within 10 s,logs startfails withdetails.reason: "simulator-log-stream-launch-timeout".openprintsSession state: <path>; see Sessions for the files it contains.eventsreads the session timeline, which leaves out user-entered content;--no-recordsuppressesaction.recordedentries.events.ndjsonrotates at 5 MB (AGENT_DEVICE_EVENT_LOG_MAX_BYTES) and keeps one generation. Cursors stay valid across rotation; a cursor older than the retained window fails withdetails.reason: "EVENT_LOG_CURSOR_EXPIRED"anddetails.earliestCursor. If the retained files were edited or deleted,eventsfails withdetails.reason: "EVENT_LOG_WINDOW_UNVERIFIED".network dump [limit] [summary|headers|body|all]parses recent HTTP(s) entries fromapp.log, or from managedagent-browserrequest history on web;network logis an alias. Prefer--includeover the positional detail level. It scans up to 4000 log lines, returns up to 200 entries, and truncates fields at 2048 characters. Web does not expose bodies.- On iOS,
network dumpcan miss requests the app does not log and cannot name every request on a reused connection, so treat a missing endpoint as unproven.open --capture-networkremoves that limit on the simulator: the dump then reportssource: probefrom the injected probe instead of the app log. See Network inspection for per-platform behavior.
Crash symbols: debug symbols symbolicates an Apple crash artifact against local dSYMs, writes the symbolicated artifact to --out, and prints a compact crash report. See Crash symbolication.
Tracing
trace start [path]begins trace-log capture for the active session;trace stop [path]stops it and writes or finalizes the artifact at that path.- Use
tracefor lower-level session diagnostics thanrecordorlogsprovide.
Remote Metro workflow
When the cloud control plane owns the connection profile, connect discovers it:
For local profile files, create an agent-device.remote.json:
For self-contained scripts, pass the same profile to each step:
connectfetches the cloud connection profile;connect --remote-config <path>uses a local profile with the host, tenant/run, and optional session, platform, lease backend, and Metro settings. Both save connection state and defer the device lease and Metro preparation until a command needs them (including abatchstep that opens an app). Direct-provider profiles need their provider credentials whenconnectruns. Cloud profiles must be a JSON object atconnection.remoteConfigProfile; the olderconnection.remoteConfigstring is rejected.- Cloud auth:
connectrefreshes a stored CLI session into a short-livedadc_agent_...token. Without a session, interactive shells start login; CI and non-interactive shells fail with API-token setup instructions.--no-logindisables implicit login. Useadc_live_...service tokens for CI. Manage the CLI session withagent-device auth status,auth login, andauth logout; login never stores anadc_live_...token. AGENT_DEVICE_CLOUD_BASE_URLis the bridge/control-plane API origin, not necessarily the dashboard; its/api-keysroute may redirect to the dashboard.connectwithout--sessionalways creates a fresh remote session and prints the generated--session. Concurrent processes must pass that value on every command; the active-connection fallback is only for one sequential workflow. To replace a connection, pass--session <name> --force;--forcewithout a session adds a connection without releasing the previous one.- After
connect, normal commands (install-from-source,open,snapshot,press,screenshot, and others) reuse the connection state.connection statusshowsleaseId=pendinguntil the first platform-bound command allocates the lease. Explicit flags override connected defaults;openwith explicit remote flags but no saved runtime hints warns that React Native apps may launch without Metro hints. - End with
disconnect(ordisconnect --remote-config <path>) to release the lease and stop the Metro companion owned by the connection. It does not stop your Metro server. metroProxyBaseUrlis the bridge origin; do not add/api/metro/...paths. The bridge supplies iOS wildcard HTTPS hints and Android runtime routes.metroPublicBaseUrlis only needed for direct, non-bridge bundle hints.metro prepare --remote-config ...is an inspection/debug path that can write--runtime-file <path>.
Session inspection
session listshows active daemon sessions for your workspace, or for the named session scope when--sessionorAGENT_DEVICE_SESSIONis set. Add--jsonfor raw session metadata.
Cloud provider artifacts
artifactslists the provider-hosted artifacts your provider returns, such as videos, logs, and dashboard links, ascloudArtifacts(what each provider returns: Device Clouds), separate from the localartifactsthat screenshot, record, install, and replay return. Plain text prints ready URLs.- Looking up a past session needs
--provider-session <id>and--provider <name>. For credentials, see Device Clouds. AWS Device Farm infers the region from the session ARN when possible. When an embedding host registers the cloud runtime in-process,artifactsinfers the active provider session before disconnect. disconnect --jsonandclose --jsoninclude provider release data when the runtime returns final artifacts. Providers may finalize URLs only after the session stops, so retryartifactsif the first response ispending.
iOS physical-device prerequisites
For CLI-discoverable setup guidance, run agent-device help physical-device.
- Older devices discovered only through
xctraceuse the XCTest backend automatically; its runner commands travel through macOSusbmuxd, so keep the device connected by cable. - XCTest-backed devices support open/close, interactions, snapshots, and screenshots. App inventory, install/reinstall, logs, performance sampling, recording, deep links, and launch arguments require CoreDevice.
- For Xcode, pairing, Developer Mode, and Apple runner signing, see Installation.
iOS SpringBoard, widgets, and system-UI surfaces
For CLI-discoverable workflow guidance, run agent-device help ios-system-ui.
agent-device open com.apple.springboard --platform iosbinds the session to SpringBoard on iOS simulators and physical iPhones. On a physical iPhone,snapshot -iand selector presses on visible home-screen icons are verified; the widget add/edit/remove flow below is verified on simulators only (see #1296).- The full widget add/edit/remove flow is selector-driven from a fresh
snapshot -i, except two coordinate-based steps: the empty-space long-press that enters edit mode, and the widget-gallery search-result rows, which return unlabeled accessibility nodes. - SpringBoard labels vary by iOS version and locale; discover them from the current snapshot rather than hard-coding strings like
EditorAdd Widget. - Reopen the app bundle under test to return to normal app automation after a SpringBoard step.
