Mobilerun device control — you are the agent
mobilerun is your hands and eyes on a real phone. You are the brain. You read the
screen with mobilerun device ui / device screenshot, decide the next move with your own
reasoning, and act with deterministic mobilerun device commands. No cloud, no Gemini, no
mobilerun run, no API key — those are explicitly out of scope here.
Scripts live in scripts/ next to this file; reference docs in references/. Set two
variables once per session, before the loop — the skill is invoked with the project cwd,
not this folder, so a bare scripts/mr-preflight.sh will No such file or directory. And because
each Bash call is a fresh shell (one command per call), no cd persists between calls to rescue a
relative path — so address scripts absolutely:
export MR_DEVICE=<serial> # find it with: mobilerun devices
export SKILL=<absolute path to this skill dir> # the folder holding this SKILL.md
Then call every script as bash "$SKILL/scripts/<name>" — that's how the runnable blocks below
are written, so they work from any cwd. (Prose and tables name a script by its bare filename,
e.g. mr-tap.sh, purely for brevity.)
The mental model
Six ideas make every other instruction in this skill obvious. Hold them and the right move usually
follows on its own.
- You are the agent. The cloud path (
mobilerun run / Gemini) needs an API key this setup
doesn't have, so the intelligence is you: you read the screen, choose the next move, and issue
one deterministic mobilerun device command at a time. There's nothing else deciding — every tap
is yours.
- Act on the snapshot you just took. The element list is rebuilt by a pre-order walk and
renumbered on every UI change, so index 9 in an old capture is a different element now. The tap
you're about to make has to come from the capture you're looking at: snap → act on that snap →
snap again to see what happened.
- One command per call, and read its printed line. Each Bash call runs a single
mobilerun
command so you can read its result — Tapped (x,y), Typed:, the tree. Piping (cmd | tail)
hands you tail's exit code instead of mobilerun's, so a real error reads as success; trust the
printed words, or ${PIPESTATUS[0]} if you must pipe.
- Move like a person. Pause between actions and aim a few pixels off dead-center. This reads
human, and it's also more correct: the dwell lets animations and lazy lists settle before your
next snap, and the offset avoids landing on an overlapping child element.
mr-wait.sh and
mr-tap.sh's jitter do this for you.
- Compose outward steps, show them, then commit. Anything other people or the account will
see — posting, sending, commenting, buying, installing/uninstalling, signing out, a factory
reset — you build up to and then stop, show the user, and submit only on their yes. A
reversible setting you're just testing is different: change it, verify, and restore it yourself.
- Begin ready.
mr-preflight.sh resolves the device, rides out the USB settle, and re-asserts
accessibility in the form the version check expects — the things that otherwise make the first
command of a session fail. Run it once up front and the loop starts on solid ground.
The through-line: perception before action. Hold the six ideas above and one truth explains
them all — almost every failure on this device is a perception failure, not an action failure.
You rarely fail because you tapped the wrong pixel; you fail because you acted on a belief about the
screen that a fresh snap would have corrected. That belief takes four shapes, and naming the one
you're in hands you the fix:
- Stale snapshot — you act on a capture the screen already moved past; any scroll renumbers the
tree. Snap → act on that snap → snap again.
- Assumed location — you act on where you think you are, not what the header says:
start
resumes a leftover screen, and a stray back can drop you into another app entirely. Read the
phone-state activityName before you act.
- Unconfirmed blank — you read "no output" as an empty tree without checking the snap itself
was empty; a swallowed pipe looks identical to a wedged a11y service. Confirm the missing
Clickable UI elements line before you believe it.
- Trusted doc — you act on a documented shape, flag, or schema you never ran; docs drift, the
device does not. Run it once and read the real bytes.
So when something doesn't work, your first move is not a different tap — it's a fresh snap, because a
surprising result is data about your perception before it is data about the device. The lone
exception is a tool-contract gap: when the command itself cannot express what you need (clearing a
field to empty), you fix the tool, not your looking — see references/text-input.md.
The loop
0. SETUP (once): export MR_DEVICE=<serial> ; export SKILL=<abs path to this skill dir>
PREFLIGHT (once): bash "$SKILL/scripts/mr-preflight.sh" # ready + heals a11y/version/USB
── then repeat ──
1. CAPTURE: bash "$SKILL/scripts/mr-snap.sh" [--boxes] # indexed UI tree (+ screenshot, +numbered boxes)
2. DECIDE (you): read the tree/screenshot; pick the target element by its TEXT/resourceId;
copy its bounds (x1,y1,x2,y2). When unsure of layout, Read the SHOT= PNG.
3. ACT: bash "$SKILL/scripts/mr-tap.sh" --bounds "x1,y1,x2,y2" # jittered box-center tap
bash "$SKILL/scripts/mr-type.sh" "text" [--clear|--human] (focus the field first)
mobilerun device swipe … / press back|home|enter / start <pkg>
bash "$SKILL/scripts/mr-wait.sh" [read|think|MIN MAX] # human pause between steps
4. VERIFY: bash "$SKILL/scripts/mr-snap.sh" ; confirm the expected change appeared.
If nothing changed, retry the tap once; if it's still unchanged, re-plan from
the new snap rather than repeating the same tap.
Give the loop a budget: if the goal state isn't reached in ~15–25 capture→act cycles, stop and
report what you saw — a stalled plan is information, so re-plan instead of looping on.
The box-select click system (the core mental model)
mobilerun device ui prints every on-screen element as:
Current Clickable UI elements:
'index. className: resourceId; checkedState, text - bounds(x1,y1,x2,y2)':
9. TextView: "android:id/title", "Network & internet" - (221,462,566,519)
34. Switch: "…/switchWidget", "Dark theme" ; isChecked=False - (859,1912,996,2038)
- The bounds
(x1,y1,x2,y2) are absolute device pixels: left,top,right,bottom.
- The Portal can draw these as numbered boxes on screen (
mr-snap.sh --boxes, or
adb shell content insert --uri content://com.mobilerun.portal/overlay_visible --bind visible:b:true).
The number in each box's top-right corner matches the device ui index — this is your
set-of-marks. Use it when you (or the user) want to see what's clickable.
- To click element N:
center = ((x1+x2)//2, (y1+y2)//2), then tap (with jitter). mr-tap.sh --bounds "…" does the math + jitter — the same center mobilerun computes internally.
- Identify a target by its text / resourceId, not by a remembered number: the index is only
meaningful in the snap it came from, so re-snap and re-find the element each time before you tap.
Full mechanics, overlap handling, raw JSON: references/box-select-and-clicking.md.
Capture: which signal to use
| Need |
Use |
Why |
| Find tappable targets + exact bounds |
mobilerun device ui (default) |
cheap, text, precise pixels — your primary sense |
| Understand layout / images / a screen with no a11y tree |
mobilerun device screenshot → Read the PNG |
you are multimodal; Read renders the PNG visually |
| See which elements are clickable, visually |
mr-snap.sh --boxes → Read the BOXES png |
numbered overlay = set-of-marks |
| Programmatic raw tree |
adb … content query --uri …/state_full |
JSON with index/bounds/flags |
How to choose between them, and how to handle the temp-path screenshot: references/capture-state.md.
Acting: primitives
| Action |
Command |
Notes |
| Tap element |
mr-tap.sh --bounds "x1,y1,x2,y2" |
jittered center; re-snap first |
| Tap raw point |
mr-tap.sh --xy X Y |
when you have pixels (e.g. from a screenshot) |
| Long-press |
mobilerun device long-press X Y |
context menus; not on iOS |
| Type / paste |
mr-type.sh "text" [--clear] [--human] |
focus the field first (tap, ~0.5s settle) |
| Scroll |
mobilerun device swipe 540 1700 540 700 --duration 0.4 |
up-swipe scrolls down; re-snap after |
| Set a slider |
tap its Decrease/Increase buttons, or a point along the track |
SeekBar has no isChecked; verify by thumb-bounds shift |
| System buttons |
mobilerun device press back|home|enter |
back is 2-stage if keyboard is up; only these three |
| Launch app |
mobilerun device start <package> |
get packages from device apps |
Gestures, scrolling a list to find an item, paging: references/navigation-and-gestures.md.
Text entry, how the keyboard works on this device, atomic paste vs human typing: references/text-input.md.
Stealth & looking human
Default to human cadence: mr-wait.sh between actions, mr-wait.sh read while "reading" a post,
jittered taps, natural scroll distances, and --human typing for anything a person authored
(comments, messages). See references/stealth-and-human-pacing.md. When you compose text a human
will be seen to have written, also read references/writing-like-a-human.md so it doesn't read
like an LLM.
Planning any task
Decompose the goal into the loop above, expressed as concrete CLI steps before you touch the
device. Worked end-to-end plans (Reddit "read N posts then comment", Settings toggles, search-and-
open, multi-app flows) are in references/task-planning-playbook.md. Start there for any non-trivial request.
Reference routing
| File |
Read when |
references/task-planning-playbook.md |
Planning ANY multi-step task into CLI steps; worked examples. Start here. |
references/box-select-and-clicking.md |
Tapping elements: indices, bounds, center math, overlap, numbered boxes, raw JSON. |
references/capture-state.md |
Choosing/using ui vs screenshot vs raw state; reading the PNG; temp-path handling. |
references/text-input.md |
Typing/pasting; how the keyboard behaves on this device; clearing fields; special characters. |
references/navigation-and-gestures.md |
Scrolling, paging a list to find an off-screen item, back/home, switching apps. |
references/stealth-and-human-pacing.md |
Pacing, jitter, natural scrolling/typing, anti-bot-pattern hygiene. |
references/writing-like-a-human.md |
Composing comments/messages/posts that read human, not LLM. |
references/command-reference.md |
Exact CLI flags, Portal content-provider endpoints (query vs insert), bash discipline. |
references/device-health-and-recovery.md |
What preflight does and why; re-asserting accessibility, the version pin, USB-blip and empty-tree recovery, GrapheneOS specifics. Read when something degrades mid-session. |
1---2name: mobilerun-control3description: Use if controlling or testing a connected Android phone via the mobilerun CLI — tap, type, swipe.4---56# Mobilerun device control — you are the agent78`mobilerun` is your **hands and eyes** on a real phone. **You** are the brain. You read the9screen with `mobilerun device ui` / `device screenshot`, decide the next move with your own10reasoning, and act with deterministic `mobilerun device` commands. No cloud, no Gemini, no11`mobilerun run`, no API key — those are explicitly out of scope here.1213Scripts live in `scripts/` next to this file; reference docs in `references/`. **Set two14variables once per session, before the loop** — the skill is invoked with the *project* cwd,15not this folder, so a bare `scripts/mr-preflight.sh` will `No such file or directory`. And because16each Bash call is a fresh shell (one command per call), no `cd` persists between calls to rescue a17relative path — so address scripts absolutely:1819```bash20export MR_DEVICE=<serial> # find it with: mobilerun devices21export SKILL=<absolute path to this skill dir> # the folder holding this SKILL.md22```2324Then call every script as `bash "$SKILL/scripts/<name>"` — that's how the runnable blocks below25are written, so they work from any cwd. (Prose and tables name a script by its bare filename,26e.g. `mr-tap.sh`, purely for brevity.)2728## The mental model2930Six ideas make every other instruction in this skill obvious. Hold them and the right move usually31follows on its own.32331. **You are the agent.** The cloud path (`mobilerun run` / Gemini) needs an API key this setup34 doesn't have, so the intelligence is *you*: you read the screen, choose the next move, and issue35 one deterministic `mobilerun device` command at a time. There's nothing else deciding — every tap36 is yours.372. **Act on the snapshot you just took.** The element list is rebuilt by a pre-order walk and38 renumbered on every UI change, so index 9 in an old capture is a different element now. The tap39 you're about to make has to come from the capture you're looking at: snap → act on *that* snap →40 snap again to see what happened.413. **One command per call, and read its printed line.** Each Bash call runs a single `mobilerun`42 command so you can read its result — `Tapped (x,y)`, `Typed:`, the tree. Piping (`cmd | tail`)43 hands you `tail`'s exit code instead of mobilerun's, so a real error reads as success; trust the44 printed words, or `${PIPESTATUS[0]}` if you must pipe.454. **Move like a person.** Pause between actions and aim a few pixels off dead-center. This reads46 human, and it's also more *correct*: the dwell lets animations and lazy lists settle before your47 next snap, and the offset avoids landing on an overlapping child element. `mr-wait.sh` and48 `mr-tap.sh`'s jitter do this for you.495. **Compose outward steps, show them, then commit.** Anything other people or the account will50 see — posting, sending, commenting, buying, installing/uninstalling, signing out, a factory51 reset — you build up to and then *stop*, show the user, and submit only on their yes. A52 reversible setting you're just testing is different: change it, verify, and restore it yourself.536. **Begin ready.** `mr-preflight.sh` resolves the device, rides out the USB settle, and re-asserts54 accessibility in the form the version check expects — the things that otherwise make the first55 command of a session fail. Run it once up front and the loop starts on solid ground.5657**The through-line: perception before action.** Hold the six ideas above and one truth explains58them all — *almost every failure on this device is a perception failure, not an action failure.*59You rarely fail because you tapped the wrong pixel; you fail because you acted on a belief about the60screen that a fresh snap would have corrected. That belief takes four shapes, and naming the one61you're in hands you the fix:6263- **Stale snapshot** — you act on a capture the screen already moved past; any scroll renumbers the64 tree. *Snap → act on that snap → snap again.*65- **Assumed location** — you act on where you *think* you are, not what the header says: `start`66 resumes a leftover screen, and a stray `back` can drop you into another app entirely. *Read the67 phone-state `activityName` before you act.*68- **Unconfirmed blank** — you read "no output" as an empty tree without checking the *snap itself*69 was empty; a swallowed pipe looks identical to a wedged a11y service. *Confirm the missing70 `Clickable UI elements` line before you believe it.*71- **Trusted doc** — you act on a documented shape, flag, or schema you never ran; docs drift, the72 device does not. *Run it once and read the real bytes.*7374So when something doesn't work, your first move is not a different tap — it's a fresh snap, because a75surprising result is data about your perception before it is data about the device. The lone76exception is a *tool-contract gap*: when the command itself cannot express what you need (clearing a77field to empty), you fix the tool, not your looking — see `references/text-input.md`.7879## The loop8081```820. SETUP (once): export MR_DEVICE=<serial> ; export SKILL=<abs path to this skill dir>83 PREFLIGHT (once): bash "$SKILL/scripts/mr-preflight.sh" # ready + heals a11y/version/USB84 ── then repeat ──851. CAPTURE: bash "$SKILL/scripts/mr-snap.sh" [--boxes] # indexed UI tree (+ screenshot, +numbered boxes)862. DECIDE (you): read the tree/screenshot; pick the target element by its TEXT/resourceId;87 copy its bounds (x1,y1,x2,y2). When unsure of layout, Read the SHOT= PNG.883. ACT: bash "$SKILL/scripts/mr-tap.sh" --bounds "x1,y1,x2,y2" # jittered box-center tap89 bash "$SKILL/scripts/mr-type.sh" "text" [--clear|--human] (focus the field first)90 mobilerun device swipe … / press back|home|enter / start <pkg>91 bash "$SKILL/scripts/mr-wait.sh" [read|think|MIN MAX] # human pause between steps924. VERIFY: bash "$SKILL/scripts/mr-snap.sh" ; confirm the expected change appeared.93 If nothing changed, retry the tap once; if it's still unchanged, re-plan from94 the new snap rather than repeating the same tap.95```9697Give the loop a budget: if the goal state isn't reached in ~15–25 capture→act cycles, stop and98report what you saw — a stalled plan is information, so re-plan instead of looping on.99100## The box-select click system (the core mental model)101102`mobilerun device ui` prints every on-screen element as:103104```105Current Clickable UI elements:106'index. className: resourceId; checkedState, text - bounds(x1,y1,x2,y2)':1079. TextView: "android:id/title", "Network & internet" - (221,462,566,519)10834. Switch: "…/switchWidget", "Dark theme" ; isChecked=False - (859,1912,996,2038)109```110111- The **bounds `(x1,y1,x2,y2)`** are absolute device pixels: `left,top,right,bottom`.112- The Portal can **draw these as numbered boxes** on screen (`mr-snap.sh --boxes`, or113 `adb shell content insert --uri content://com.mobilerun.portal/overlay_visible --bind visible:b:true`).114 The number in each box's **top-right corner matches the `device ui` index** — this is your115 set-of-marks. Use it when you (or the user) want to *see* what's clickable.116- To **click element N**: `center = ((x1+x2)//2, (y1+y2)//2)`, then tap (with jitter). `mr-tap.sh117 --bounds "…"` does the math + jitter — the same center mobilerun computes internally.118- Identify a target by its **text / resourceId**, not by a remembered number: the index is only119 meaningful in the snap it came from, so re-snap and re-find the element each time before you tap.120121Full mechanics, overlap handling, raw JSON: `references/box-select-and-clicking.md`.122123## Capture: which signal to use124125| Need | Use | Why |126|---|---|---|127| Find tappable targets + exact bounds | `mobilerun device ui` (default) | cheap, text, precise pixels — your primary sense |128| Understand layout / images / a screen with no a11y tree | `mobilerun device screenshot` → **Read** the PNG | you are multimodal; Read renders the PNG visually |129| See *which* elements are clickable, visually | `mr-snap.sh --boxes` → Read the BOXES png | numbered overlay = set-of-marks |130| Programmatic raw tree | `adb … content query --uri …/state_full` | JSON with index/bounds/flags |131132How to choose between them, and how to handle the temp-path screenshot: `references/capture-state.md`.133134## Acting: primitives135136| Action | Command | Notes |137|---|---|---|138| Tap element | `mr-tap.sh --bounds "x1,y1,x2,y2"` | jittered center; re-snap first |139| Tap raw point | `mr-tap.sh --xy X Y` | when you have pixels (e.g. from a screenshot) |140| Long-press | `mobilerun device long-press X Y` | context menus; not on iOS |141| Type / paste | `mr-type.sh "text" [--clear] [--human]` | **focus the field first** (tap, ~0.5s settle) |142| Scroll | `mobilerun device swipe 540 1700 540 700 --duration 0.4` | up-swipe scrolls down; re-snap after |143| Set a slider | tap its `Decrease`/`Increase` buttons, or a point along the track | `SeekBar` has no `isChecked`; verify by thumb-bounds shift |144| System buttons | `mobilerun device press back\|home\|enter` | back is 2-stage if keyboard is up; only these three |145| Launch app | `mobilerun device start <package>` | get packages from `device apps` |146147Gestures, scrolling a list to find an item, paging: `references/navigation-and-gestures.md`.148Text entry, how the keyboard works on this device, atomic paste vs human typing: `references/text-input.md`.149150## Stealth & looking human151152Default to human cadence: `mr-wait.sh` between actions, `mr-wait.sh read` while "reading" a post,153jittered taps, natural scroll distances, and `--human` typing for anything a person authored154(comments, messages). See `references/stealth-and-human-pacing.md`. When you compose text a human155will be seen to have written, also read `references/writing-like-a-human.md` so it doesn't read156like an LLM.157158## Planning any task159160Decompose the goal into the loop above, expressed as concrete CLI steps before you touch the161device. Worked end-to-end plans (Reddit "read N posts then comment", Settings toggles, search-and-162open, multi-app flows) are in `references/task-planning-playbook.md`. Start there for any non-trivial request.163164## Reference routing165166| File | Read when |167|---|---|168| `references/task-planning-playbook.md` | Planning ANY multi-step task into CLI steps; worked examples. **Start here.** |169| `references/box-select-and-clicking.md` | Tapping elements: indices, bounds, center math, overlap, numbered boxes, raw JSON. |170| `references/capture-state.md` | Choosing/using `ui` vs `screenshot` vs raw state; reading the PNG; temp-path handling. |171| `references/text-input.md` | Typing/pasting; how the keyboard behaves on this device; clearing fields; special characters. |172| `references/navigation-and-gestures.md` | Scrolling, paging a list to find an off-screen item, back/home, switching apps. |173| `references/stealth-and-human-pacing.md` | Pacing, jitter, natural scrolling/typing, anti-bot-pattern hygiene. |174| `references/writing-like-a-human.md` | Composing comments/messages/posts that read human, not LLM. |175| `references/command-reference.md` | Exact CLI flags, Portal content-provider endpoints (query vs insert), bash discipline. |176| `references/device-health-and-recovery.md` | What preflight does and why; re-asserting accessibility, the version pin, USB-blip and empty-tree recovery, GrapheneOS specifics. Read when something degrades mid-session. |