Skip to main content

Phone Harness 📱

phone-harness · 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:

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: 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 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).
  • 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

./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, which 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:

./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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

phone_harness-0.2.0.tar.gz (50.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

phone_harness-0.2.0-py3-none-any.whl (55.5 kB view details)

Uploaded Python 3

File details

Details for the file phone_harness-0.2.0.tar.gz.

File metadata

  • Download URL: phone_harness-0.2.0.tar.gz
  • Upload date:
  • Size: 50.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for phone_harness-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a1a26771033cedad27265eeadc65b3e453b1f18f71ac1cd6a2be9746a0cb46b4
MD5 271261da7f1eefface1e809d711b5eea
BLAKE2b-256 9f6d5c858d26aca6fc58ea7e42e373f02aed1d4d8bf0324a795d9a73073ad621

See more details on using hashes here.

Provenance

The following attestation bundles were made for phone_harness-0.2.0.tar.gz:

Publisher: publish.yml on ShawnPana/phone-harness

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file phone_harness-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: phone_harness-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 55.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for phone_harness-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7f52ec3fc899e422e033a4d47a5b21a9fa61fe77766f5b8b4126498bcf160484
MD5 aa7a88cded65801ce1ac007d19225e2f
BLAKE2b-256 01cb071ec0edaf360b60a3bb4b7d16999cc9584de418204299ae5531dda35174

See more details on using hashes here.

Provenance

The following attestation bundles were made for phone_harness-0.2.0-py3-none-any.whl:

Publisher: publish.yml on ShawnPana/phone-harness

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page