Portable operating instructions for AI agents controlling Android and iOS devices—locally or in the cloud.
Mobile Harness is a compact Markdown harness, not an agent runtime. Its primary
control path is Python's mobilerun_core, with optional client apps where
needed.
We evaluated mobile-harness on AndroidWorld and iOSWorld using a standard coding agent with GPT-6 Astra low, app cards, and persistent memory.
AndroidWorld tasks · iOSWorld tasks
Mean score averages the task scores. Binary pass rate counts only tasks that received a full score of 1.0.
Explore the results and task replays for screenshots and recorded interactions, or read the methodology for evaluation details.
Copy paste it into your agent:
Set up https://github.com/droidrun/mobile-harness for me.
Read `install.md` and follow the steps to install `mobile-harness`.
- Android through
mobilerun-coreusing local ADB with optional Portal, Portal HTTP-only, or cloud. - iOS through
mobilerun-coreusingmobilerun-ioslocal HTTP, cloud, or theios-portalapp on Simulator.
Install the harness as an agent skill with the skills CLI:
npx skills add droidrun/mobile-harnessWhen you want to update an installed copy, run npx skills update from the same
project. First use still requires the Python setup in install.md.
/plugin marketplace add droidrun/mobile-harness
/plugin install mobile-harness@mobile-harness
The plugin keeps .venv/ and local/ in its data directory, which plugin
updates leave in place. First use still requires the Python setup in
install.md.
Install the full public control API:
cd /path/to/mobile-harness
"$(command -v python3.13 || command -v python3.12 || command -v python3.11)" -m venv .venv
.venv/bin/python -m pip install "mobilerun-core[local]"
.venv/bin/python -c "from mobilerun_core import Mobilerun; from importlib.metadata import version; print('mobilerun-core', version('mobilerun-core'), '/ mobilerun-core-local', version('mobilerun-core-local'))"Needs Python 3.11-3.13; if none is on PATH, uv python find '>=3.11,<3.14' prints one.
Tell agents which Python runtime to use:
Use /path/to/mobile-harness/.venv/bin/python for mobile-harness.
Normal sessions check the installed dependencies and use the existing versions.
Updates happen only when requested or needed to fix a confirmed compatibility
problem. See UPDATE.md; pinned versions stay fixed unless you approve a change.
Base mobilerun-core includes cloud support through mobilerun-sdk. The
local extra installs mobilerun-core-local, which mobilerun-core uses
internally for local Android and iOS backends.
from mobilerun_core import Mobilerun
m = Mobilerun()
device = m.connect("<cloud-device-id>", backend="cloud")
device = m.connect("R5CT123456", backend="local-android-adb")
device = m.connect(backend="local-ios-http", url="http://127.0.0.1:8080")
device = m.connect(
backend="local-android-http",
url="http://127.0.0.1:18080",
token="...",
)
device.ui()
device.screenshot()
device.start_app("com.android.settings")After connecting, agents should use device.supports(...) before optional
operations.
device.execute_script("<js>") runs JavaScript in the device's foreground
Chrome tab and returns its JSON result. Cloud devices only; local backends
raise UnsupportedOperation. Gate it with
device.supports("execute_script"); the platform guides describe this key's
network-probe behavior and its two server-side errors.
Use these helpers through the device returned by Mobilerun.connect(...):
device.find_nodes(...)searches the accessibility tree.any_contains=matches case-insensitive substrings across text, content description, resource id, and accessibility identifier. Nodes may carryoffscreen: True(outside the viewport; scroll to reach it) andhidden: True(reported not visible; scrolling alone may not reveal it). A missing flag is not proof of visibility.device.tap_node(node)taps the center of an accessibility node and raises if the node has no usable bounds. Before any bounds check, it raises a distinct error for a node flagged hidden unless the node is also offscreen.device.tap_text("label")taps the first on-screen, non-hidden match across text, description, resource id, and accessibility identifier. It raises a distinct error when matches exist but none are tappable on-screen.device.scroll(direction, distance=0.5, ms=..., verify=False)scrolls content-relative;verify=Truereturns whether the viewport actually moved.device.scroll_until(text_contains=..., direction="down", max_swipes=10)scrolls until a match is on-screen, returning the node orNone. It stops early withNonewhen the viewport stops moving; do not re-call it blindly.device.type("text", clear=True)clears the focused field before typing when the backend supports text input.device.clear_input()is available on cloud and every local backend.device.list_apps()excludes system apps by default. Passinclude_system_apps=Truewhen a full inventory is needed and supported.device.open_deep_link(...),device.get_clipboard(),device.set_clipboard(...),device.grant_permission(...)anddevice.stop_app(..., clear_data=True)depend on the backend; checkdevice.supports(...)first.device.ui(filter=False)returns the unfiltered tree, including off-screen nodes.
Cloud devices use the same Mobilerun facade:
export MOBILERUN_CLOUD_API_KEY="..."
export MOBILERUN_API_BASE_URL="https://api.mobilerun.ai/v1"from mobilerun_core import Mobilerun
m = Mobilerun()
device = m.connect("<cloud-device-id>", backend="cloud")
device.ui()
device.screenshot()
device.start_app("com.android.settings")Skill-based runtimes can load SKILL.md; all runtimes should start with
AGENTS.md. It routes agents to the smallest needed file:
platforms/android/GUIDE.mdfor Android work.platforms/ios/GUIDE.mdfor iOS work.platforms/<platform>/recovery/GUIDE.mdonly when control fails.- the credentials guide under
core/credentialsonly when a credential or human-gated screen appears. core/memory/GUIDE.mdonly when reading or writing local agent-owned memory.apps/android/<package>/CARD.mdorapps/ios/<bundle-id>/CARD.mdonly for the foreground app.- the same path under
local/apps/after the shipped card, and prefer it where the two disagree. UPDATE.mdwhen an update is requested or a confirmed version incompatibility blocks the task.
| ADB | Android Portal HTTP | Mode |
|---|---|---|
| yes | yes | backend="local-android-adb": core uses ADB and automatically uses Portal features when available. |
| yes | no | backend="local-android-adb": core uses ADB-native control, UI, text input, screenshots, and app lifecycle. |
| no | yes | backend="local-android-http" with the user-provided Portal base URL and bearer token. |
| no | no | Blocked: ask the user to enable ADB or provide reachable Portal HTTP access. |
Android Portal HTTP-only means the agent already has both:
- a base URL such as
http://127.0.0.1:18080 - a bearer token for
Authorization: Bearer <token>
Without ADB, the harness cannot install, enable, port-forward, or fetch a token for Portal. Android Mobilerun Portal: https://github.com/droidrun/mobilerun-portal
| Target | Server | Mode |
|---|---|---|
| iPhone or iPad over USB | mobilerun-ios --local <udid> |
backend="local-ios-http", default http://127.0.0.1:8080. |
| iOS Simulator | ios-portal app |
backend="local-ios-http", default http://127.0.0.1:6643. |
backend="local-ios-http" detects which server answers. stop_app,
list_apps, install_app, and the back, enter, delete, and
app_switch keys need mobilerun-ios.
mobilerun-ios cannot drive Simulators.
- mobilerun-ios local mode: https://docs.mobilerun.ai/framework/guides/device-setup (iOS tab)
- iPhone preparation and WDA signing: https://docs.mobilerun.ai/guides/connect-iphone
- ios-portal: https://github.com/droidrun/ios-portal
Everything the user or the agent owns lives under one git-ignored root, local/: local/memory/ for agent-written memory, local/credentials/ for optional credential notes, and local/apps/ for your own app cards or overrides of shipped ones. The repository tracks only their rules and templates, keeping your own content separate from git pull and plugin updates. In a Claude Code plugin install, local/ lives in the plugin's data directory instead. See local/README.md. Agents may write operational memory after reading core/memory/GUIDE.md.

