Skills for controlling Android, iOS and cloud phones.
> /plugin marketplace add droidrun/mobile-harness> /plugin install mobile-harness@mobile-harness
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`.
mobilerun-core using local ADB with optional Portal, Portal HTTP-only, or cloud.mobilerun-core using ios-portal HTTP or cloud.Install the harness as an agent skill with the skills CLI:
npx skills add droidrun/mobile-harness
When 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
python -m venv .venv
.venv/bin/python -m pip install "mobilerun-core[local]"
.venv/bin/python -c "from mobilerun_core import Mobilerun"
Use Python 3.11, 3.12, or 3.13 to create the venv.
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. Agents should still import only
mobilerun_core.
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:6643")
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 inspect device.capabilities and 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 carry offscreen: True
(outside the viewport; scroll to reach it) and hidden: 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=True returns 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 or None. It stops
early with None when 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
local Android ADB and local iOS Portal HTTP.device.list_apps() excludes system apps by default. Pass
include_system_apps=True when a full inventory is needed and supported.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.md for Android work.platforms/ios/GUIDE.md for iOS work.platforms/<platform>/recovery/GUIDE.md only when control fails.core/credentials only when a credential or human-gated screen appears.core/memory/GUIDE.md only when reading or writing local agent-owned memory.apps/android/<package>/CARD.md or apps/ios/<bundle-id>/CARD.md only for the foreground app.local/apps/ after the shipped card, and prefer it where the two disagree.UPDATE.md when 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:
http://127.0.0.1:18080Authorization: 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
Local iOS has one active capability mode:
iOS Portal HTTP: backend="local-ios-http" with MOBILERUN_IOS_PORTAL_URL or an explicit URL. GET /device/date, GET /state, and GET /vision/screenshot must work.Blocked: no reachable iOS Portal. Start ios-portal check info: https://github.com/droidrun/ios-portalThe default local iOS Portal example is http://127.0.0.1:6643.
A second local iOS server exists: mobilerun-ios --local <udid>, default
http://127.0.0.1:8080, setup guide:
https://docs.mobilerun.ai/guides/connect-iphone. backend="local-ios-http"
speaks only the ios-portal contract and cannot connect to that server; do
not point it at port 8080. platforms/ios/GUIDE.md explains how to tell the
two apart.
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.
mobile-harness is a Claude Code plugin with 1 hand-picked skill, indexed on Flowy in the Mobile category. Install it with the command on its page. It includes mobile-harness. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it