Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

List connected iPhones

/v1/devicesGET
iPhones that USB screen capture shows, with their model, lock state, Bluetooth link and lease. Waits up to 25 s for a trusted USB iPhone to appear after a daemon start; fails in about 0.1 s when USB has no trusted iPhone. With none, returns a no-device problem that says what USB lockdown sees (not on USB, not trusted, locked). On the unix socket of a signed-in Mac, the list also has the iPhones of the account's other online Macs (mac says where); a Mac that cannot answer adds a warning.
Authorizationoptional
bearerAuth?Bearer <token>
Required on the tunnel. An API token of the account, from ghosthand tunnel url --new-token. The unix socket needs no token
Response
200 · Connected iPhones
warnings?string[]
Bluetooth problems, and capture devices that cannot be matched to a USB iPhone yet (what to do).
devices *object[]
Show item properties
control *"bluetooth" | "xctest"
How input reaches the iPhone. bluetooth (default): the Mac is a Bluetooth mouse and keyboard; screenshots come from USB screen capture. xctest: WebDriverAgent, an XCUITest runner, taps, types and takes screenshots (they show dialogs that USB capture hides). Needs Xcode, Developer Mode and UI Automation. No passcode unlock, mute or keyboard shortcuts.
xctest?object
WebDriverAgent on this iPhone. Present only in XCTest mode.
Show properties
team *string
Apple team id that signs the runner, e.g. 92MF9DG2LR
state *"stopped" | "building" | "starting" | "running" | "failed"
stopped: not started since the daemon started; the next command starts it (about 15 s). building: Xcode builds it (1-3 min the first time). starting: launched, waiting for its HTTP server. running: input works. failed: see error.
error?string
Why the last start failed and what to do
id *string
USB capture device id. Pass it as device to POST /v1/leases.
mac?string
Slug of the Mac the iPhone is plugged into, e.g. studio. Absent while this Mac is signed out or not connected to ghosthand.dev.
name *string
Name of the iPhone, from Settings > General > About > Name
model?string
Product type read over USB lockdown, e.g. iPhone8,1
modelName?string
Marketing name for model, e.g. iPhone 6s. Absent for product types ghosthand does not know yet
iosVersion?string
iOS version read over USB lockdown, e.g. 15.8.3
phoneNumber?string
Phone number of the SIM, read over USB lockdown (PhoneNumber), e.g. +39 333 123 4567. Absent without a SIM, or when the carrier did not write the number to it.
locked?boolean
True while the iPhone needs its passcode, read over USB (lockdown PasswordProtected). Absent when unknown, e.g. two iPhones with the same name.
assistiveTouch?boolean
True while AssistiveTouch is on, read over USB. acquire turns it on by itself. Absent when unknown.
bluetooth?object
Show properties
address *string
The iPhone's Bluetooth address, read over USB. Its HID link to the Mac uses the same address.
Example: "A4:83:E7:12:34:56"Pattern: ^[0-9A-F]{2}(:[0-9A-F]{2}){5}$
state *"connected" | "disconnected" | "notPaired"
connected: input works. disconnected: paired, the Mac is reconnecting; Bluetooth is off on the iPhone or it is out of range if this lasts. notPaired: pair from iPhone Settings > Bluetooth (the next input command waits for it).
pointer?"absolute" | "relative"
How taps move the pointer, once known. absolute (iOS 17+) goes straight to the spot. relative (iOS 15, which ignores absolute positions) pushes the pointer into the top-left corner, then moves it in measured steps
lease?object
Who holds the iPhone. Present while a lease is active. Never has the lease id.
Show properties
label?string
Display text the holder gave, e.g. an agent session id
reason *string
The task the iPhone is used for
acquiredAt *string (date-time)
lastUsedAt *string (date-time)
The last call on the lease (or acquiredAt before the first)
lastCommand?string
CLI name of the last call on the lease, e.g. tap or screenshot. Absent before the first
expiresAt *string (date-time)
When the lease ends if no call uses it before
Default · Error (RFC 9457 problem details). `code` is stable; switch on it, not on `title` or `detail`.
code *"invalid-request" | "unauthorized" | "not-found" | "device-not-found" | "no-device" | "multiple-devices" | "permission-denied" | "bluetooth-unavailable" | "bluetooth-disconnected" | "no-frame" | "device-locked" | "lock-failed" | "pairing-timeout" | "pointer-not-found" | "lock-state-unavailable" | "lockdown-unavailable" | "unlock-failed" | "mute-unconfirmed" | "text-not-found" | "wait-text-timeout" | "cancelled" | "lease-invalid" | "device-leased" | "recording-active" | "no-recording" | "recording-failed" | "daemon-busy" | "tunnel-offline" | "login-required" | "subscription-required" | "unsupported-in-mode" | "xctest-unavailable" | "task-failed" | "internal-error"
Stable error code. Switch on it; title and detail are for people.
title *string
Short summary of the code, the same for every error with this code
status *integer
HTTP status of the error. In stream mode the response status stays 200.
detail *string
What happened and what to do next. Agents should read it.
Request example
curl -X GET "https://ghosthand.dev/macs/studio/v1/devices" \ -H "Authorization: Bearer <token>"
Response example
{ "devices": [ { "id": "7EF219AD-69B4-423E-B8C4-4F4A6140822F", "name": "Tommy's iPhone", "model": "iPhone11,8", "modelName": "iPhone XR", "iosVersion": "17.6.1", "locked": false, "assistiveTouch": true, "bluetooth": { "address": "A4:83:E7:12:34:56", "state": "connected", "pointer": "absolute" }, "lease": { "label": "ses_123", "reason": "reply to Anna in Messages", "acquiredAt": "2026-10-07T09:00:00.000Z", "lastUsedAt": "2026-10-07T09:04:12.000Z", "lastCommand": "tap", "expiresAt": "2026-10-07T09:14:12.000Z" } } ] }