ghosthand control -l 42 xctest # build once (1-3 min), start, wait until it answers ghosthand tap -l 42 207 448 --screenshot s.png ghosthand control -l 42 bluetooth # back to the default
| Bluetooth (default) | XCTest | |
| Taps | AssistiveTouch mouse pointer | XCTest touch events, like a finger |
| Screenshots | USB screen capture | XCTest screenshots; hidden-dialog support is not verified |
| Text | US-ASCII, as a hardware keyboard | Any text (accents, emoji), as the on-screen keyboard |
| Lock, passcode unlock | Yes | No: keep the iPhone unlocked |
| Mute | Yes | No |
| On the Mac | Nothing to install | Xcode, signed in to an Apple account |
| On the iPhone | Trust + Bluetooth pairing | Trust + Developer Mode + UI Automation |
| Renewal | None | Free Apple account: rebuild every 7 days |
| iPhones per Mac | About 7 (Bluetooth limit) | No Bluetooth limit |
type sends accents and emoji.unlock), and the
input path is the one a person with a mouse and keyboard uses.ghosthand control -l 42 xctest --team 92MF9DG2LR.ghosthand setup --xctest
ghosthand control -l 42 xctest
control xctest again.| Command | In XCTest mode |
tap, double-tap, hold, swipe, hold-drag | Touch events |
scroll | Slow drags of about 100 points per tick |
type | Any text, into the field that has focus |
key | home, volumeup, volumedown, enter, backspace, tab, space, and shortcuts like cmd+space, cmd+c |
screenshot, screentext | XCTest screenshots; --native is not needed |
lock, unlock, mute, unmute | Error unsupported-in-mode: a locked iPhone cannot be unlocked in this mode |
ghosthand list shows (xctest running) instead of the Bluetooth state. After a daemon
restart the runner is stopped; the next command starts it again (about 15 s).| Message | What to do |
No Accounts | Sign in to Xcode: Settings > Accounts |
Developer Mode | Step 2 above |
not been explicitly trusted, invalid code signature | Trust the developer (step 4) |
UI Automation | Settings > Developer > Enable UI Automation |
maximum number of apps | A free account signs 3 apps per iPhone: delete an old one |
| Stopped working after a week | ghosthand control -l 42 xctest --rebuild |
~/.ghosthand/xctest/<udid>-build.log and <udid>-run.log.CLI / MCP ──> daemon ──> Controller (leases, OCR, waitText, settle) ──> PhoneDriver │ ┌─────────────────────────────────┴──────┐ v v BluetoothDriver XCTestDriver HID mouse + keyboard (default) │ │ HTTP/1.1 over │ USB (usbmux), v port 8100 ~/.ghosthand/xctest/ WebDriverAgent source ──> xcodebuild build ──> WebDriverAgent (pinned tag) -for-testing on the iPhone, kept (your team) alive by xcodebuild test-without-building
USBMuxConnectByPort).~/.ghosthand/state.db, keyed by the USB
UDID only: a new or deleted screen capture match keeps it, and setup can choose XCTest
before screen capture lists the iPhone. Leases, OCR and waits do not change.scale from
GET /wda/screen, read once). No USB frame is needed, so input works while capture is
dark or frozen. Every screenshot refreshes the size, so it follows a rotation the agent saw.devmodectl makes. Only pairing is needed. Turning it on stays with you: iOS
allows that only without a passcode.| Command | WebDriverAgent route |
| tap, double-tap, hold, swipe, hold-drag, scroll | POST /session/:id/actions (one touch pointer) |
| key home | POST /wda/homescreen |
| key cmd+space, cmd+c, ... | POST /session/:id/wda/element/0/keyboardInput |
| key volumeup, volumedown | POST /session/:id/wda/pressButton |
| type, key enter / backspace / tab / space | POST /session/:id/wda/keys |
| lock / wake | POST /wda/lock / POST /wda/unlock |
| screenshot | GET /screenshot |
| screen scale (once, for coordinates) | GET /session/:id/wda/screen |
xcodebuild test-without-building, own usbmux client; tested on iOS 26.5.devicectl launch breaks on iOS 27 (appium#22636), so ghosthand keeps xcodebuild.services/amfi.py: the Developer Mode reveal call.Testing failed ... connection was invalidated) about 100 s
after start, with no crash report; not tied to one gesture. The next command restarts it
(5-18 s). The failed command says so (xctest-unavailable).type with no focused field still succeeds; WebDriverAgent does not check focus.