# Phone Harness πŸ“± **[phone-harness](https://phone-harness.com)** Β· let your agent control your phone. Connect an AI agent β€” Claude Code, Codex, or any LLM β€” directly to your real phone with a thin, editable harness. **iPhone** through the Mac's iPhone Mirroring window, or **Android** over adb (USB or Wi‑Fi). No jailbreak, no Xcode, no WebDriverAgent, no app on the phone. The Mac is the whole transport. iPhone: `screencapture` + Vision-framework OCR for eyes, HID-level CGEvents for hands. Android: `screencap` for eyes plus the phone's own accessibility tree β€” exact text, exact boxes β€” and `input` for hands. Nothing between the agent and the phone. The agent writes what's missing during execution in `agent-workspace/agent_helpers.py`. ``` ● agent: wants to open Weather β”‚ ● ocr() β†’ "Weather" at (400, 468) β”‚ ● tap(400, 468) β†’ wait_stable() β†’ ocr() confirms the forecast βœ“ done ``` **Your phone, driven by an agent.** ## Setup prompt Paste into Claude Code or Codex: ```text Set up phone-harness for me. Clone https://github.com/ShawnPana/phone-harness into ~/.phone-harness (its canonical home), read `install.md` first, install it so `phone-harness` is a command on my PATH, and register it as an agent skill named phone-harness using `phone-harness skill` as the body, so you reach for it automatically. Then read `SKILL.md` for normal usage, and always read `src/phone_harness/helpers.py` because that is where the functions are. Then read `onboarding.md` and walk me through it. ``` The agent then follows [onboarding.md](onboarding.md): it asks one thing β€” which phone is your default, iPhone or Android β€” and walks you through only what needs your hands. **iPhone:** pairing iPhone Mirroring once, and granting the terminal **Accessibility** and **Screen Recording** in System Settings β†’ Privacy & Security (Screen Recording takes effect after the terminal restarts). **Android:** turning on Developer options, then either plugging in and tapping Allow, or Wireless debugging + one 6‑digit pairing code. Then `phone-harness --doctor` verifies the chain, and `phone-harness config set platform ios|android` sets the default (both can be set up). A fresh machine may prompt for more permissions the first time an action runs β€” if `--doctor` passes but taps or capture silently do nothing, watch for a macOS prompt. See [install.md](install.md) for details. ## Why this works **iPhone.** iPhone Mirroring (macOS Sequoia+) renders the phone as a Mac window and forwards real mouse and keyboard input as touches. That gives an agent everything it needs for real-device iOS automation: - **See** β€” capture just the mirroring window, OCR it with Apple's Vision framework: every visible string with a tap-ready coordinate. The poor man's DOM. - **Act** β€” CGEvents posted at the HID tap: taps, long-presses, drags/flicks, scroll gestures, unicode typing, and the app's own shortcuts (Cmd+1 Home, Cmd+2 App Switcher, Cmd+3 Spotlight). For icon-only controls found in a screenshot, use `tap_image_point(x, y, image_size=...)`: screenshot coordinates are image pixels, while `tap(x, y)` expects global Mac screen points. `image_point()` performs the conversion when a raw `tap()` is needed. - **Verify** β€” screenshot again. No DOM means the capture is the ground truth. Things that do NOT work, learned the hard way: AppleScript `click at` (silently ignored β€” the window is a video stream with no accessibility tree), unicode key payloads (mirroring forwards raw HID keycodes, so typing must use keycodes), a slow touch-drag (barely moves an iOS list β€” use wheel scroll for lists, a fast flick for pages), and input while the window isn't frontmost (swallowed). **Android.** adb reaches the phone directly, over USB or Wi‑Fi β€” no window, no focus, nothing on the Mac has to be in front. `screencap` is the capture; the phone's own accessibility tree (`uiautomator`) is the text source, so `ocr()` returns exact strings and boxes and `tap_ui("url_bar")` finds elements OCR never could; `input tap/swipe/text` are the hands; Back exists. Coordinates are device pixels, so a screenshot is 1:1 with `tap(x, y)`. The harness finds the phone itself (a plugged-in one first, else the paired Wi‑Fi phone), refuses to drive a locked one, and can keep it awake for a session without touching a setting. Same helpers, same agent code. ## Usage ```bash ./phone-harness <<'PY' open_app("Notes") tap_text("New Note") type_text("hello from the harness") print([o["text"] for o in ocr()][:10]) PY ``` Day-to-day workflow lives in [SKILL.md](SKILL.md), which [install.md](install.md) registers as an agent skill (`phone-harness skill` prints the body) so the agent reaches for it on its own. ## Architecture - `SKILL.md` β€” day-to-day usage (the agent-facing product surface) - `onboarding.md` β€” the first-run flow the agent walks the user through - `install.md` β€” setup reference and troubleshooting - `src/phone_harness/` β€” protected core: - `transport.py` β€” the one seam: the op vocabulary every device sits behind, and `connect("ios"|"android")` - `helpers.py` β€” the primitives pre-imported into scripts (platform-agnostic) - `ios.py`, `mirror.py`, `background.py`, `ocr.py` β€” the iPhone: window discovery, focus, capture, CGEvent input, Vision OCR - `android.py` β€” the Android: adb, the accessibility tree, USB/Wi‑Fi resolution and pairing, awake sessions, the `android` CLI - `config.py` β€” settings (`phone-harness config`) and remembered devices - `admin.py` β€” `--doctor`; `run.py` β€” the CLI (`exec` stdin with helpers in scope) - `agent-workspace/agent_helpers.py` β€” helper code the agent edits; auto-loaded into every script's namespace Both transports are stateless per call (window bounds and captures are re-queried; adb has its own server), so there is no daemon β€” every invocation is self-contained. State that must persist (the default platform, paired Android phones) lives in `~/.config/phone-harness` and `~/.local/state/phone-harness`. ## Development From a checkout, use `./phone-harness` to run the working tree directly: ```bash ./phone-harness <<'PY' print(screen_info()) PY ``` ## Limits - iPhone: one phone, one session; unlocking the physical phone pauses mirroring. OCR sees text, not semantics β€” unlabeled icons need a screenshot + a vision-capable model. - Android: the tree costs seconds on a slow phone and is unavailable on screens that never go idle (read the screenshot instead); `input text` is ASCII; key chords are not a thing over adb; a PIN-locked phone needs the user. - Both: no multi-touch (no pinch), no camera/Face ID flows, DRM video renders black. Connecting the phone is always the user's job. ## Sponsor phone-harness is free and maintained in my own time. [Sponsoring](https://github.com/sponsors/ShawnPana) keeps it that way.