// signals
A layout that passes every check in CI and still breaks on an iPhone isn’t a rare bug; it’s a structural one. Your CI runs Playwright. Playwright renders Chromium. The iPhone renders WebKit. Those are different engines, so the class of bug that only shows up on iOS Safari never gets a chance to fail where you’d catch it. I got tired of shipping those and then finding them by hand on a phone.
The gap nobody tests
Playwright and Chromium cannot reproduce real iOS WebKit: 100dvh that isn’t what you think, -webkit-fill-available, the Safari toolbar that slides away and eats your footer, MSE/HLS playback. The obvious fix is Apple’s own Simulator, except it has a hole. xcrun simctl, the tool that boots and controls simulators, has no gesture API at all. You can launch an app. You cannot tap a button. So iPhone-only bugs never appear in CI, and the one device that would show them can’t be driven. That is the whole problem: no clean path from an agent to the thing a user’s thumb actually touches.
What I built
ios-sim-driver closes that path. It drives Mobile Safari and native apps on a genuine iOS device (real WebKit, not a stand-in) through Appium and XCUITest plus simctl. Native touch is real: tap, double-tap, long-press, swipe, scroll, pinch, type, hardware buttons. Because the gestures are real, a native swipe moves the Safari toolbar the way a thumb would; a JavaScript scrollBy doesn’t. It dumps the live accessibility tree, so an agent finds a control by its label instead of guessing pixel coordinates, and it runs iOS WebKit JS eval against the actual engine. Simulators, and (since v0.4.0) physical iPhones.
It started small. The first version was a ~140-line one-shot Safari-driving script I wrote during the RanSynTV IPTV hardening campaign, still shipped as a back-compat shim. Then I generalized it into a platform instead of copy-pasting it into the next project.
The tool surface
One engine, three front doors: a CLI (bin/ios), an MCP server that exposes 46 ios_* tools, and a Claude Code plugin with a /ios command. The MCP tools and the CLI verbs map 1:1 over the same code path, so what I run by hand and what an agent runs are the same thing. The whole engine is ~1,250 lines of JavaScript with zero npm dependencies: no package.json, no node_modules, just Node shelling out to simctl, appium, and devicectl. Prerequisites are macOS, Xcode, and a one-time global Appium install.
The capabilities I lean on most: GPS spoofing, push-notification simulation, photo and video library injection, permission grant and revoke, light/dark and Dynamic Type, Face ID / Touch ID enroll-and-match, the App Store “clean” status bar (9:41, full bars, 100%), screenshots returned as real images, screen recording, live console streaming, and an XCUITest accessibility audit. Physical-device support is verified live on an iPhone 15 Pro running iOS 26.5: devicectl operations, a real screenshot, a 1.78 MB accessibility-tree dump, and native input, all confirmed on hardware.
How my agents use it
The MCP server is registered globally on my build machine, so all 46 ios_* tools are present in every Claude Code project I open. An agent opens the URL, evals against real WebKit, screenshots the result, reads the accessibility tree, taps the actual button, and asserts with real exit codes. It is fast because it is lazy: verbs that don’t need UI automation (screenshots, installs, permissions, GPS) go straight through simctl with no session, while the input and web-eval verbs reuse one cached Appium session instead of paying startup on every call. The default target is an iPhone 17 Pro on iOS 26.5, overridable per project.
The takeaway
The method point is small, and it outlasts iOS: to test what a user actually touches, you have to drive what a user actually touches: the real engine, the real gesture, the real device. A convenient-but-wrong emulator doesn’t remove the bug; it moves it downstream to a human holding a phone. That’s the same instinct behind everything I ship at RandomSynergy: build the capability once against reality, prove it cheaply on my own projects, then graduate what survives.
