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."4allowed-tools: Bash(mobilerun:*) Bash(adb:*) Read5disable-model-invocation: true6metadata:7 source: https://skillmd.com/skills/yigitkonur/mobilerun-control8---910# Mobilerun device control — you are the agent1112`mobilerun` is your **hands and eyes** on a real phone. **You** are the brain. You read the13screen with `mobilerun device ui` / `device screenshot`, decide the next move with your own14reasoning, and act with deterministic `mobilerun device` commands. No cloud, no Gemini, no15`mobilerun run`, no API key — those are explicitly out of scope here.1617Scripts live in `scripts/` next to this file; reference docs in `references/`. **Set two18variables once per session, before the loop** — the skill is invoked with the *project* cwd,19not this folder, so a bare `scripts/mr-preflight.sh` will `No such file or directory`. And because20each Bash call is a fresh shell (one command per call), no `cd` persists between calls to rescue a21relative path — so address scripts absolutely:2223```bash24export MR_DEVICE=<serial> # find it with: mobilerun devices25export SKILL=<absolute path to this skill dir> # the folder holding this SKILL.md26```2728Then call every script as `bash "$SKILL/scripts/<name>"` — that's how the runnable blocks below29are written, so they work from any cwd. (Prose and tables name a script by its bare filename,30e.g. `mr-tap.sh`, purely for brevity.)3132## The mental model3334Six ideas make every other instruction in this skill obvious. Hold them and the right move usually35follows on its own.36371. **You are the agent.** The cloud path (`mobilerun run` / Gemini) needs an API key this setup38 doesn't have, so the intelligence is *you*: you read the screen, choose the next move, and issue39 one deterministic `mobilerun device` command at a time. There's nothing else deciding — every tap40 is yours.412. **Act on the snapshot you just took.** The element list is rebuilt by a pre-order walk and42 renumbered on every UI change, so index 9 in an old capture is a different element now. The tap43 you're about to make has to come from the capture you're looking at: snap → act on *that* snap →44 snap again to see what happened.453. **One command per call, and read its printed line.** Each Bash call runs a single `mobilerun`46 command so you can read its result — `Tapped (x,y)`, `Typed:`, the tree. Piping (`cmd | tail`)47 hands you `tail`'s exit code instead of mobilerun's, so a real error reads as success; trust the48 printed words, or `${PIPESTATUS[0]}` if you must pipe.494. **Move like a person.** Pause between actions and aim a few pixels off dead-center. This reads50 human, and it's also more *correct*: the dwell lets animations and lazy lists settle before your51 next snap, and the offset avoids landing on an overlapping child element. `mr-wait.sh` and52 `mr-tap.sh`'s jitter do this for you.535. **Compose outward steps, show them, then commit.** Anything other people or the account will54 see — posting, sending, commenting, buying, installing/uninstalling, signing out, a factory55 reset — you build up to and then *stop*, show the user, and submit only on their yes. A56 reversible setting you're just testing is different: change it, verify, and restore it yourself.576. **Begin ready.** `mr-preflight.sh` resolves the device, rides out the USB settle, and re-asserts58 accessibility in the form the version check expects — the things that otherwise make the first59 command of a session fail. Run it once up front and the loop starts on solid ground.6061**The through-line: perception before action.** Hold the six ideas above and one truth explains62them all — *almost every failure on this device is a perception failure, not an action failure.*63You rarely fail because you tapped the wrong pixel; you fail because you acted on a belief about the64screen that a fresh snap would have corrected. That belief takes four shapes, and naming the one65you're in hands you the fix:6667- **Stale snapshot** — you act on a capture the screen already moved past; any scroll renumbers the68 tree. *Snap → act on that snap → snap again.*69- **Assumed location** — you act on where you *think* you are, not what the header says: `start`70 resumes a leftover screen, and a stray `back` can drop you into another app entirely. *Read the71 phone-state `activityName` before you act.*72- **Unconfirmed blank** — you read "no output" as an empty tree without checking the *snap itself*73 was empty; a swallowed pipe looks identical to a wedged a11y service. *Confirm the missing74 `Clickable UI elements` line before you believe it.*75- **Trusted doc** — you act on a documented shape, flag, or schema you never ran; docs drift, the76 device does not. *Run it once and read the real bytes.*7778So when something doesn't work, your first move is not a different tap — it's a fresh snap, because a79surprising result is data about your perception before it is data about the device. The lone80exception is a *tool-contract gap*: when the command itself cannot express what you need (clearing a81field to empty), you fix the tool, not your looking — see `references/text-input.md`.8283## The loop8485```860. SETUP (once): export MR_DEVICE=<serial> ; export SKILL=<abs path to this skill dir>87 PREFLIGHT (once): bash "$SKILL/scripts/mr-preflight.sh" # ready + heals a11y/version/USB88 ── then repeat ──891. CAPTURE: bash "$SKILL/scripts/mr-snap.sh" [--boxes] # indexed UI tree (+ screenshot, +numbered boxes)902. DECIDE (you): read the tree/screenshot; pick the target element by its TEXT/resourceId;91 copy its bounds (x1,y1,x2,y2). When unsure of layout, Read the SHOT= PNG.923. ACT: bash "$SKILL/scripts/mr-tap.sh" --bounds "x1,y1,x2,y2" # jittered box-center tap93 bash "$SKILL/scripts/mr-type.sh" "text" [--clear|--human] (focus the field first)94 mobilerun device swipe … / press back|home|enter / start <pkg>95 bash "$SKILL/scripts/mr-wait.sh" [read|think|MIN MAX] # human pause between steps964. VERIFY: bash "$SKILL/scripts/mr-snap.sh" ; confirm the expected change appeared.97 If nothing changed, retry the tap once; if it's still unchanged, re-plan from98 the new snap rather than repeating the same tap.99```100101Give the loop a budget: if the goal state isn't reached in ~15–25 capture→act cycles, stop and102report what you saw — a stalled plan is information, so re-plan instead of looping on.103104## The box-select click system (the core mental model)105106`mobilerun device ui` prints every on-screen element as:107108```109Current Clickable UI elements:110'index. className: resourceId; checkedState, text - bounds(x1,y1,x2,y2)':1119. TextView: "android:id/title", "Network & internet" - (221,462,566,519)11234. Switch: "…/switchWidget", "Dark theme" ; isChecked=False - (859,1912,996,2038)113```114115- The **bounds `(x1,y1,x2,y2)`** are absolute device pixels: `left,top,right,bottom`.116- The Portal can **draw these as numbered boxes** on screen (`mr-snap.sh --boxes`, or117 `adb shell content insert --uri content://com.mobilerun.portal/overlay_visible --bind visible:b:true`).118 The number in each box's **top-right corner matches the `device ui` index** — this is your119 set-of-marks. Use it when you (or the user) want to *see* what's clickable.120- To **click element N**: `center = ((x1+x2)//2, (y1+y2)//2)`, then tap (with jitter). `mr-tap.sh121 --bounds "…"` does the math + jitter — the same center mobilerun computes internally.122- Identify a target by its **text / resourceId**, not by a remembered number: the index is only123 meaningful in the snap it came from, so re-snap and re-find the element each time before you tap.124125Full mechanics, overlap handling, raw JSON: `references/box-select-and-clicking.md`.126127## Capture: which signal to use128129| Need | Use | Why |130|---|---|---|131| Find tappable targets + exact bounds | `mobilerun device ui` (default) | cheap, text, precise pixels — your primary sense |132| Understand layout / images / a screen with no a11y tree | `mobilerun device screenshot` → **Read** the PNG | you are multimodal; Read renders the PNG visually |133| See *which* elements are clickable, visually | `mr-snap.sh --boxes` → Read the BOXES png | numbered overlay = set-of-marks |134| Programmatic raw tree | `adb … content query --uri …/state_full` | JSON with index/bounds/flags |135136How to choose between them, and how to handle the temp-path screenshot: `references/capture-state.md`.137138## Acting: primitives139140| Action | Command | Notes |141|---|---|---|142| Tap element | `mr-tap.sh --bounds "x1,y1,x2,y2"` | jittered center; re-snap first |143| Tap raw point | `mr-tap.sh --xy X Y` | when you have pixels (e.g. from a screenshot) |144| Long-press | `mobilerun device long-press X Y` | context menus; not on iOS |145| Type / paste | `mr-type.sh "text" [--clear] [--human]` | **focus the field first** (tap, ~0.5s settle) |146| Scroll | `mobilerun device swipe 540 1700 540 700 --duration 0.4` | up-swipe scrolls down; re-snap after |147| Set a slider | tap its `Decrease`/`Increase` buttons, or a point along the track | `SeekBar` has no `isChecked`; verify by thumb-bounds shift |148| System buttons | `mobilerun device press back\|home\|enter` | back is 2-stage if keyboard is up; only these three |149| Launch app | `mobilerun device start <package>` | get packages from `device apps` |150151Gestures, scrolling a list to find an item, paging: `references/navigation-and-gestures.md`.152Text entry, how the keyboard works on this device, atomic paste vs human typing: `references/text-input.md`.153154## Stealth & looking human155156Default to human cadence: `mr-wait.sh` between actions, `mr-wait.sh read` while "reading" a post,157jittered taps, natural scroll distances, and `--human` typing for anything a person authored158(comments, messages). See `references/stealth-and-human-pacing.md`. When you compose text a human159will be seen to have written, also read `references/writing-like-a-human.md` so it doesn't read160like an LLM.161162## Planning any task163164Decompose the goal into the loop above, expressed as concrete CLI steps before you touch the165device. Worked end-to-end plans (Reddit "read N posts then comment", Settings toggles, search-and-166open, multi-app flows) are in `references/task-planning-playbook.md`. Start there for any non-trivial request.167168## Reference routing169170| File | Read when |171|---|---|172| `references/task-planning-playbook.md` | Planning ANY multi-step task into CLI steps; worked examples. **Start here.** |173| `references/box-select-and-clicking.md` | Tapping elements: indices, bounds, center math, overlap, numbered boxes, raw JSON. |174| `references/capture-state.md` | Choosing/using `ui` vs `screenshot` vs raw state; reading the PNG; temp-path handling. |175| `references/text-input.md` | Typing/pasting; how the keyboard behaves on this device; clearing fields; special characters. |176| `references/navigation-and-gestures.md` | Scrolling, paging a list to find an off-screen item, back/home, switching apps. |177| `references/stealth-and-human-pacing.md` | Pacing, jitter, natural scrolling/typing, anti-bot-pattern hygiene. |178| `references/writing-like-a-human.md` | Composing comments/messages/posts that read human, not LLM. |179| `references/command-reference.md` | Exact CLI flags, Portal content-provider endpoints (query vs insert), bash discipline. |180| `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. |