DEV Community

Honey Kiran
Honey Kiran

Posted on Originally published at honeykiran.substack.com on

My AI Agent Drove My iPhone for a Day. Then I Parked It.

Saturday, coffee in hand, the dangerous kind of simple idea in my head.

On my phone sits an app with no API. Agents that can only talk are half an agent.

So I gave Muse, my AI agent, hands on my real iPhone: open the app, read its screens, tap with me in the loop.

A terminal panel sends "GET /source" to a generic phone. The phone's screen rows light up one by one as the screen tree comes back as XML, including a tab's center at 67, 904. Then "POST /wda/tap" with those coordinates fires, a tap ripple lands on a tab, and the phone switches screens. Caption: Eyes and fingers, under orders.

The tool, in plain words

The standard tool is WebDriverAgent (WDA): a test-runner app that moonlights as a remote control, signed with your own Apple ID.

Once it runs, the phone becomes a tiny web API on port 8100: GET the screen as XML, POST a tap.

The morning: a "successful" test that never ran

The rig: a current iPhone, an Apple-silicon Mac, Xcode 27.1. The clone was branch 3.17 — vintage code, brand-new iOS 27. The first build hit three walls fast.

Wall 1: deployment target 10.0. Both targets went to 15.0.

Wall 2: warnings-as-errors. Switched OFF.

Wall 3: XCTAutomationSupport links only into approved clients. Fix: a static library, and the client name "XCTest".

By midday the build was green and the test was "running" — 20,301.4 seconds on the record: 5.6 hours of "testing." Nothing was listening on port 8100.

It had never loaded.

A build panel turns green with "Build succeeded" and a test-session timer climbs to 20,301.4 seconds, 5.6 hours. Beside it, a port 8100 listener shows a flat red line and "nothing listening" while every status probe is refused. Caption: It had never loaded.

The evening: two gates and a missing framework

That evening we stopped trusting the GUI and asked the phone directly.

Gate one: Developer Mode was OFF, so the Developer Disk Image could not mount — a cryptex error, CoreDeviceError 12040/12052. Gate two: trust the developer certificate, or iOS refuses to launch the runner.

Then the real one: a detached launch died instantly (exit code 83). The framework was linked but never embedded.

Two LD_RUNPATH_SEARCH_PATHS lines fixed it, and ServerURLHere printed the phone's address.

iOS 27 removed two methods. We patched both.

iOS 27 had quietly removed two private methods WDA stood on, and WDA called both unguarded.

The first crash came minutes after the server came up: the protocol-version call. Muse read the source, found the send site, and patched it: check first, wrap in @try/@catch, fall back to 0, never crash.

The second surfaced on the first real screen read: the quiescence (app-idle) wait. A settings knob, waitForIdleTimeout, sounds exactly like the fix. We set it to 0. Nothing changed. The knob is decorative. Same patch at the second send site: guard it, catch it, skip the wait if the method is gone.

// The pattern that fixes both iOS 27 crashes: never call a private
// method you have not checked for, and never let the check kill you.
if ([object respondsToSelector:@selector(thePrivateMethod:)]) {
  @try {
    [object thePrivateMethod:];
  } @catch (NSException *exception) {
    // Selector vanished mid-flight — fall back. Do not crash the runner.
  }
}
Enter fullscreen mode Exit fullscreen mode

Two lanes, one per missing method. In each, an unguarded call reaches a daemon where the method is marked missing and the runner crashes and goes dark. Then the guarded version checks first, catches the failure and falls back, and the runner stays alive: first for the protocol-version call, then for the wait-for-idle call. Caption: Check first. Catch. Fall back. Never crash.

The recipe

  • Clone the current Appium WebDriverAgent repo, not an old branch; Personal Team signing on both targets, unique bundle IDs (com.yourname.*).
  • Deployment target 15.0 on both targets; GCC_TREAT_WARNINGS_AS_ERRORS = NO.
  • WebDriverAgentLib: MACH_O_TYPE = staticlib. Runner: link XCTAutomationSupport, add PrivateFrameworks to FRAMEWORK_SEARCH_PATHS, and OTHER_LDFLAGS += "-Wl,-client_name,XCTest".
  • Runner: both PrivateFrameworks paths in LD_RUNPATH_SEARCH_PATHS.
  • On the phone: Developer Mode ON, then trust the developer certificate.
  • Patch both iOS 27 send sites: guard, @try/@catch, fall back.
  • Start with xcodebuild test from Terminal: ~90 seconds to server-up. Once, while cabled, tick "Connect via network" in Xcode's Devices window so later starts run over Wi-Fi.

A 15-second animation. An agent reads a phone's screen over HTTP and taps a tab. A build turns green while a timer runs 5.6 hours over a port where nothing is listening. Two missing methods crash the runner until guarded calls check, catch and fall back. Then the cable comes out, a status check returns ready, one wireless tap and a screen read succeed, and the rig is stamped parked.

The proof: no cable, one tap

Earlier that evening the patched runner had already proved itself: /status answered ready, carrying the patch's fingerprint: "testmanagerdVersion": 0, and a session survived creation. One landmine, learned then: a session bound to SpringBoard, the home screen, timed out and wedged the server solid. Attach to normal apps.

Then the cable came OUT. Muse drove the phone with plain curl over Wi-Fi and Tailscale, a private mesh VPN, and /status answered ready.

Muse attached to a target app and read its screen live — every label and figure exactly as displayed. Element search by ID was broken on this tree, so Muse took a tab's center, (67, 904), straight from the screen XML and sent one tap; the tab switched on the phone in my hand. The next screen read clean: a list, rows and counts intact.

A laptop and a phone joined by a cable. The cable is crossed out and removed, and a dotted Wi-Fi and Tailscale path arcs from the laptop's terminal to the phone. A status call returns "ready, testmanagerdVersion 0", one tap ripples onto the phone, and a list screen reads back with its rows checking in one by one. Caption: Read plus tap. Zero cables.

Why I parked it the same day

The control never missed — a flawless demo. That same evening I told the agent to switch it off. The bill:

While a session runs, the automation screen owns the display. You are carrying a test rig, not a phone.

Every start needs the Mac awake and a human pasting a Terminal command. Muse's attempts to start it by driving Xcode's GUI failed twice; the paste is the only start that ever worked.

Free signing dies every 7 days; $99 a year stretches it to a year.

And the stack is an old branch on two patches.

A dark checklist card titled "The Park-It Checklist." Four ticked items: the display is owned; a human paste to start; signing dies in 7 days; two patches from the next update. Footer: It worked perfectly. That was not the problem.

Two verdicts

Technology verdict and ownership verdict are different verdicts.

If it returns: a spare phone on a dock, next to an always-on Mac — never the daily driver.

Running an agent against your own phone? Tell me where yours broke.


© 2026 Honey Kiran. All rights reserved.

Honey Kiran · DevOps & Platform Engineering

I write about DevOps, platform engineering, Kubernetes, and putting AI agents to work — with guardrails, receipts, and the occasional parked project.

Disclaimer

  • This post describes my personal experience with my own systems and accounts, in my own words. It is a first-person account of what I observed and what I do in my own work — not a statement of any company’s intentions, policies, or internal decisions. Where I do not know something, the post says so.
  • I am not affiliated with, endorsed by, or speaking for any company, product, or vendor named in this post. All product and company names are trademarks of their respective owners and are mentioned only to describe the experience accurately.
  • Any error messages, documentation excerpts, or figures quoted here are reproduced from my own records or from the public sources linked in the post, believed accurate at the time of writing. Product behavior changes over time; verify against current official documentation before relying on any detail.
  • Nothing in this post is professional, financial, legal, or security advice for your specific situation. Examples are illustrative — validate in your own environment before running anything in production.
  • Corrections are welcome. If any company named here publishes an official statement, or if any detail is wrong, I will update this post and note the correction. The goal is an accurate public record, not a verdict.

Examples are illustrative — validate in your own environment before running in production.

Top comments (0)