Package Android-phone control (over adb) as an installable agent skill, modeled on how EgoBrowser (ego lite) packages browser control for AI agents. Any skill-capable host (Claude Code first) becomes the brain; this repo ships only the eyes and hands.
This plan synthesizes three investigations: (a) how ego lite is actually built, (b) what is
reusable in LightGUIAgent (our grid-overlay adb agent), and (c) an integration
architecture for this repo. A working first draft of the skill already exists at
LightGUIAgent/skills/android-use/ (uncommitted there) and migrates here as v0.1.
ego lite is a Chromium fork. ego-browser on PATH is a thin native CLI that forwards a
heredoc script over Mach/Mojo IPC to a Node runtime compiled into the browser; helper
functions (snapshotText, click, …) are injected as AsyncFunction parameters around the
script. The browser process owns all state (task spaces, tabs, login sessions). The skill
directory ships read-only inside the app bundle and reaches ~/.claude/skills/ego-browser
through a chain of symlinks (~/.local/share/ego/active_version_dir pins the CLI+skill to
the browser build that last completed onboarding). Errors carry stable codes
(EGO_TASK_SPACE_USER_IN_CONTROL, …) and docs say "branch on the code, not the wording".
A learnings/ subsystem (per-site manifest.json + notes + host-side/page-side tools)
ships fully wired.
LightGUIAgent is a standalone autonomous agent: screenshot → chess-grid overlay (PIL) →
Claude Opus picks a cell like "E5" → adb shell input tap. It carries its own LLM loop,
prompt, cost tracking, and logging — exactly the layer a host agent replaces. Its draft
skill skills/android-use/ (stdlib-only cli.py: devices / snapshot / tap / scroll / swipe / type / key / screenshot / app, refmap cached per-serial, yadb pushed lazily for CJK
input) is already a superset of the old agent's action surface.
| ego concept | Verdict | Why |
|---|---|---|
| Semantic-first, visual-fallback doctrine (+ "write-probe then verify") | Adopt (already in SKILL.md) | uiautomator dump is our semantic tier; screenshot + tap x y the visual tier |
| Observe-after-every-action; refs invalidated per snapshot | Adopt (already in SKILL.md) | Same staleness physics as @N refs |
| Stable error-code contract | Adopt (v0.2) | cli.py exits with AU_NO_DEVICE, AU_UNAUTHORIZED, AU_MULTIPLE_DEVICES, AU_KEYGUARD_LOCKED, AU_STALE_REF, AU_DUMP_FAILED; SKILL.md tells the agent to branch on the code |
Idempotent install.sh, verify-at-end, "return to the original task" framing |
Adopt | Far simpler payload: no DMG/Gatekeeper — check adb → symlink → selftest → adb devices |
learnings/ per-target knowledge |
Adopt simplified (v0.3) | learnings/<package>/notes.md, markdown only. No manifest/tool-injection: there is no runtime to load tools into. ego's own SKILL.md never mentions its learnings system — ship ours documented from day one |
| Control handoff / ownership | Adapt | A phone has one screen, so agent/user isolation is time-slicing. Prose policy (lock screen, payments, FLAG_SECURE → stop and ask) + one mechanical guard: refuse actions while the keyguard is up (v0.2) |
| Persistent runtime daemon | Reject | The adb server is already a daemon and the phone holds all UI state; a daemon adds lifecycle/version-skew/debugging cost and buys nothing. See §4 |
| Heredoc Node scripting surface | Reject | adb actions are atomic; there is no in-process object graph. A subcommand CLI is the right grain |
| Task spaces | Reject permanently | One physical foreground screen; isolation is impossible. Analog of completeTaskSpace is key home — a convention, not an API |
Symlink version-pinning (active_version_dir), Omaha updater |
Reject | The repo is the payload; git pull is the updater |
help() introspection |
Reject | 9 subcommands fit in one SKILL.md table + argparse --help |
| Asset | Verdict |
|---|---|
adb action recipes (agent.py:163-293) |
Already ported into cli.py, improved (exec-out screencap, override-aware wm size) |
| yadb binary + push protocol | Port as-is. Also exposes unused -layout (accessibility UI dump — fallback when uiautomator dump fails) and clipboard read/write (third text-entry route) |
Grid overlay + converter (grid_overlay.py, grid_converter.py) |
Drop for v0.x. The host reads raw screenshots and emits pixels directly. If coordinate taps prove unreliable, rewrite as an optional ~60-line grid.py behind a Pillow import guard — do not port (import-time adb calls, hardcoded macOS font, square-resize distortion) |
Settings auto-detect (settings.py) |
Drop. cli.py's wm size handling is already better (override beats physical; hard error over silent 1080×2400 fallback) |
Stuck-detection heuristics (claude_client.py:320-336) |
Drop the code, port the wording into SKILL.md: "same screen twice → key back or re-enter from home". Lift the common-package table (WeChat/Taobao/Alipay/Meituan) into references/apps.md |
| Logger, LLM loop, cost tracking | Drop entirely — that layer is the host agent |
Relationship going forward: two products, one substrate. LightGUIAgent stays the
autonomous-agent research artifact in its own repo. android-use is the tool surface for host
agents. The draft skill was never committed to LightGUIAgent, so migration is a clean copy
here (initial commit), delete the draft there, retarget ~/.claude/skills/android-use.
yadb (13 KB) is vendored independently in both repos — a shared submodule is pure overhead.
Every piece of state a daemon would hold already has a home:
| State | Where it lives |
|---|---|
| Device connection (USB/Wi-Fi) | adb server — already a daemon |
| UI state | the phone itself |
| Refmap from last snapshot | ~/.cache/android-use/refs-<serial>.json |
| yadb-pushed flag | checked on-device (~50 ms) |
| Device selection | ANDROID_SERIAL env (adb-native) + -d override |
| scrcpy live view | the one true daemon candidate — deferred to a v1.0 decision point |
Per-invocation overhead is one adb round trip (~100–200 ms), dwarfed by uiautomator dump
(~1 s) which no daemon can avoid.
android-use/
├── README.md # humans: what/why, install one-liner, demo
├── PLAN.md # this plan
├── LICENSE # Apache-2.0 (see §8)
├── NOTICE # yadb attribution (upstream ysbing/YADB, license, version)
├── install.sh # idempotent: adb check → symlink skill → selftest → verify
├── skills/android-use/ # ← the distributable payload; symlink target
│ ├── SKILL.md # agent-facing manual
│ ├── scripts/cli.py # the entire runtime, stdlib-only, any python3
│ ├── bin/yadb # vendored CJK-input dex (13 KB)
│ ├── references/setup.md # USB/wireless onboarding + troubleshooting
│ ├── references/apps.md # common package names (from LightGUIAgent prompt)
│ └── learnings/ # v0.3: per-app notes keyed by package name
└── tests/test_cli.py # parse_tree/refmap unit tests, no device needed
Nesting under skills/ (not SKILL.md at repo root) keeps the agent-readable payload minimal
and is exactly the layout a Claude Code plugin (.claude-plugin/plugin.json + skills/)
requires at v1.0 — no breaking git mv later. No Python packaging: cli.py is invoked
by path; pyproject/pip would add install steps and version skew for zero benefit.
Primary (v0.1): git clone + sh install.sh, matching how every skill on this machine is
already installed (symlinks into ~/.claude/skills/). install.sh is POSIX sh:
command -v adb— missing → printbrew install android-platform-tools/ apt line.ln -sfn "$repo/skills/android-use" ~/.claude/skills/android-use(--target <dir>for other hosts).- Verify:
python3 …/cli.py selftest; thenadb devices— no authorized device → point atreferences/setup.md, exit 0 (device onboarding is the user's GUI step, like ego's).
Versioning: metadata.version in SKILL.md frontmatter + git tags; update = git pull.
MCP server: not in v1 — the CLI is the universal adapter; wrap it only if a bash-less host
actually appears.
v0.1 — it exists as a repo (days)
- Copy the draft skill here with the §9 defect fixes; initial commit; README/LICENSE/NOTICE/install.sh/tests.
- Retarget local symlink; delete the draft from LightGUIAgent.
- Milestone: fresh machine → clone →
install.sh→ Claude Code taps through a real app.
v0.2 — round-trip and safety ergonomics
tap --text "Login"/tap --id send_btn: dump+match+tap in one invocation (kills the stale-ref problem for the common case).- Global
--snapshot-after(act → settle ~800 ms → print fresh snapshot): action + observation in one Bash call. - Keyguard guard: any action while locked →
AU_KEYGUARD_LOCKEDhard error → hand back to user. - Stable error codes throughout (§2);
connectsubcommand wrappingadb pair/adb connect. --clearfallback for Android ≤10 (MOVE_END+ N×DEL); yadb-layoutas dump fallback.- Milestone: a 10-step task completes in ≤10 Bash invocations with no stale-ref retries.
v0.3 — learnings
learnings/<package>/notes.mdconvention + SKILL.md write-back protocol (agent appends discovered navigation paths after tasks).- Seed 2–3 apps already exercised in LightGUIAgent demos (WeChat, Xiaohongshu).
v1.0 — distribution polish
.claude-plugin/plugin.json(plugin/marketplace install);curl | shbootstrap; CI (pytest + shellcheck); tagged releases.- Decision point, not commitment: scrcpy live-view as an optional background process — only if screenshot polling proves insufficient.
Explicitly not building: a daemon/session runtime; heredoc scripting; task spaces; MCP server (until needed); scrcpy streaming in v0.x; on-device accessibility-service APK (uiautomator is slower but zero-install); emulator lifecycle; iOS; any autonomous LLM loop (that's LightGUIAgent's job).
- Apache-2.0 for this repo — matches LightGUIAgent's LICENSE file (its pyproject declares MIT; that conflict should be fixed there) and provides the NOTICE mechanism.
NOTICEnames yadb: upstream ysbing/YADB, its license, and the pinned version the vendored dex was built from (currently recorded nowhere — extractVERSION_NAMEfrom the dex or pin the upstream release). Mention provenance inreferences/setup.md.
yadb multi-word text bugFixed in draft: payload now quoted for the device-side shell,\n/\t→space preprocessing added.ensure_yadbstaleness: presence-onlylscheck trusts any pre-existing/data/local/tmp/yadb. Push once per process, or comparemd5sum.--clearsilently no-ops on Android ≤11 (input keycombinationis Android 12+, not 11+ as first believed) → v0.2 fallback viaMOVE_END+ repeated DEL.- Cosmetic:
snapshotleaves/sdcard/au-dump.xmlbehind (tech review suggestsexec-out uiautomator dump /dev/ttyto remove the temp file and a round trip).
End-to-end task (open Taobao, search for Nikon cameras, read results) succeeded — 8 CLI invocations on the happy path, ~30 total including a proxy detour. Validated: CJK input via yadb (flawless), ref-based taps (100% accurate), screenshot-guided coordinate taps (first-try precision), scroll granularity (~one card row).
Findings, mapped to the tech review's predictions:
- Risk #1 confirmed: the shopping app's results page is semantically opaque — bare clickable FrameLayouts with no titles/prices, or a near-empty WebView mid-load — while an equivalent page rendered natively minutes earlier exposed full text (mixed native/web render paths in one app). Vision fallback was mandatory to read results.
- The predicted
could not get idle stateerror never occurred, but a subtler variant did: early dumps return plausible-but-wrong bare trees on loading screens (a skeleton screen and an error page produced identical-looking trees). - New finding: apps interleave zero-width characters (U+200B) in label text, silently breaking substring matching on snapshot output.
- New finding: a device-wide VPN (v2RayTun) caused the shopping app's search to return generic "system error" pages — server-side risk control of proxy exit IPs is indistinguishable from an automation bug without a screenshot, and nothing in the skill hinted at device network state as a suspect.
- The SKILL.md
$AUalias example broke under zsh (no word-splitting), and shell state doesn't persist across host Bash calls anyway.
Fixes landed in the draft skill in response: snapshot --settle (re-dump until stable;
plus --compressed retry on dump failure), zero-width stripping, foreground activity in
the snapshot header, app launch now verifies the foreground switch, app --list
restricted to launchable apps, yadb quoting fix (§9.1), SKILL.md sections on in-app
errors vs automation errors (VPN guidance) and full-command invocation, --clear
boundary corrected to Android 12+.
Upstream LightGUIAgent bugs found along the way (fix there, not here): agent.py:310 looks
for yadb at the wrong path so it never installs; agent.py:529 logs a dict where
logger.py expects a float.
The skill migrated from LightGUIAgent/skills/android-use/ into this repo
(skills/android-use/), with LICENSE (Apache-2.0), NOTICE (yadb LGPL-3.0
attribution), install.sh (symlink installer), and .claude-plugin/plugin.json
(pulled forward per the tech review). Added since the field test:
mirror [--off]: optional live scrcpy window with the system touch indicator enabled, so a human can watch the agent's taps in real time.- SKILL.md "Evidence for reports" workflow: distinct screenshot paths per step, report composed by the host agent (deliberately no report generator in the CLI). Validated end to end: a sub-agent researched cameras on Taobao and produced a self-contained HTML report with 22 embedded evidence screenshots.
Still open from section 9: yadb re-vendor pinned >= v1.1.1 with checksum,
ensure_yadb staleness check, Android <= 11 --clear fallback.