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

List iPhones with their setup step

/v1/phonesGET
For the ghosthand app (setup window and control plane). Unlike /v1/devices it answers at once, from USB lockdown, and also lists iPhones that screen capture does not show yet: not trusted, or locked. Each iPhone has one step, the next thing a person must do (ready when agents can use it), so a client needs no logic of its own. phones is sorted by id, so the order is stable. macName is the name the iPhone lists under Settings > Bluetooth.
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 · iPhones on USB
account *object
Sign-in state only; no plan fields and no server request
Show properties
state *"signed-out" | "signing-in" | "signed-in"
signed-out: no account on this Mac; every lease fails with login-required. signing-in: a browser sign-in runs; open verificationUrl. signed-in: each acquire checks the iPhone and this Mac against the plan.
server *string
The ghosthand.dev server the daemon uses (env GHOSTHAND_SERVER at daemon start)
email?string
Signed-in user
userCode?string
While signing in, the code the browser page shows, to compare
verificationUrl?string
While signing in, the page to open in a browser
lastError?string
Why the last sign-in did not finish (denied or expired), until the next one starts
plan?"free" | "pro"
Only from GET /v1/account. free: one iPhone, on any Mac. pro: every iPhone on each paid Mac, plus the free iPhone anywhere. Acquire on another iPhone fails with subscription-required.
paidMacs?integer
Only from GET /v1/account. Macs the Pro subscription pays for; 0 on free
sourceAccess?boolean
Only from GET /v1/account. True when the account can read the daemon source on GitHub
notice?string
Note about the plan to show the user
subscribeUrl?string
Checkout, or the billing portal when subscribed
dashboardUrl?string
serverError?string
Only from GET /v1/account, when ghosthand.dev could not be reached
macName *string
The name this Mac shows under Settings > Bluetooth on the iPhone
camera *"authorized" | "notDetermined" | "denied" | "restricted"
macOS Camera permission of the daemon, needed for screen capture. Until it is authorized, phones have no captureId. notDetermined: the user clicks Allow Camera in setup. denied: the user opens Camera privacy settings from setup. restricted: blocked by macOS policy. Commands and previews never ask by themselves.
bluetooth *object
Show properties
state *"not-started" | "starting" | "on" | "permission-missing" | "permission-denied" | "permission-restricted" | "powered-off" | "unsupported" | "error"
not-started: allowed, but no USB iPhone uses Bluetooth mode yet; starts on the first Bluetooth command or when list or setup sees one. starting: waiting for Bluetooth or HID publication. on: the HID service is published. Permission states are checked before hardware state. Missing or denied permission tells the agent to run setup and poll setup status until ready. Only the setup app's buttons request permissions or open privacy settings. powered-off: turn Bluetooth on. unsupported: authorized, but Classic is unavailable. error: service failure.
error?string
Why Bluetooth is not available, e.g. it is off or not allowed for the daemon
photos?"authorized" | "notDetermined" | "denied"
Optional macOS removable volumes access, used only to delete photos on the iPhone (photo delete, screenshot --native). The user clicks Allow Photos in setup.
phones *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
developerMode?boolean
Settings > Privacy & Security > Developer Mode, read over USB. XCTest mode needs it. Absent on iOS 15 and when unknown.
id *string
USB lockdown id (UDID). Not the id of /v1/devices; that is captureId.
captureId?string
Screen capture id, the one list prints and acquire -d takes. Absent until screen capture shows the iPhone and it is matched to its USB device.
name *string
model?string
Product type, e.g. iPhone11,8
modelName?string
Marketing name, e.g. iPhone XR. Absent for unknown product types
iosVersion?string
phoneNumber?string
Phone number of the SIM, e.g. +39 333 123 4567. Absent without a SIM, when the carrier did not write it, or when the iPhone is not trusted.
step *"trust" | "unlock" | "usbUnreadable" | "bluetoothUnavailable" | "pair" | "connecting" | "developerMode" | "xctestStarting" | "xctestFailed" | "ready"
The next thing a person must do, in this order: trust: unlock the iPhone and tap Trust. unlock: trusted, needs its passcode. usbUnreadable: trusted, but the lock state or Bluetooth address could not be read over USB. Bluetooth mode: bluetoothUnavailable: Bluetooth is not ready on this Mac (see bluetooth). pair: pair the Mac from iPhone Settings > Bluetooth. connecting: paired, the link is down; the Mac reconnects. XCTest mode: developerMode: turn on Developer Mode on the iPhone. xctestStarting: WebDriverAgent builds or starts. xctestFailed: its start failed (xctest.error). ready: agents can use it.
assistiveTouch?boolean
True while AssistiveTouch is on, read over USB. 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
waiting *object[]
Clients running ghosthand wait for this iPhone, oldest first
Show item properties
label?string
Display text the client gave, e.g. an agent session id
reason *string
The task the client waits to do
since *string (date-time)
When it started waiting
mac?string
Only in remotePhones. Slug of the Mac the iPhone is plugged into, e.g. studio
macName?string
Only in remotePhones. Name of that Mac, e.g. Tommy's Mac Studio
remotePhones?object[]
Only on the unix socket: iPhones of the account's other online Macs, each with mac and macName, for the control plane. Each Mac pushes its list through ghosthand.dev when it changes, so the response makes no request. Lease times (lastUsedAt, lastCommand, expiresAt) can be old. Absent when there are none.
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
developerMode?boolean
Settings > Privacy & Security > Developer Mode, read over USB. XCTest mode needs it. Absent on iOS 15 and when unknown.
id *string
USB lockdown id (UDID). Not the id of /v1/devices; that is captureId.
captureId?string
Screen capture id, the one list prints and acquire -d takes. Absent until screen capture shows the iPhone and it is matched to its USB device.
name *string
model?string
Product type, e.g. iPhone11,8
modelName?string
Marketing name, e.g. iPhone XR. Absent for unknown product types
iosVersion?string
phoneNumber?string
Phone number of the SIM, e.g. +39 333 123 4567. Absent without a SIM, when the carrier did not write it, or when the iPhone is not trusted.
step *"trust" | "unlock" | "usbUnreadable" | "bluetoothUnavailable" | "pair" | "connecting" | "developerMode" | "xctestStarting" | "xctestFailed" | "ready"
The next thing a person must do, in this order: trust: unlock the iPhone and tap Trust. unlock: trusted, needs its passcode. usbUnreadable: trusted, but the lock state or Bluetooth address could not be read over USB. Bluetooth mode: bluetoothUnavailable: Bluetooth is not ready on this Mac (see bluetooth). pair: pair the Mac from iPhone Settings > Bluetooth. connecting: paired, the link is down; the Mac reconnects. XCTest mode: developerMode: turn on Developer Mode on the iPhone. xctestStarting: WebDriverAgent builds or starts. xctestFailed: its start failed (xctest.error). ready: agents can use it.
assistiveTouch?boolean
True while AssistiveTouch is on, read over USB. 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
waiting *object[]
Clients running ghosthand wait for this iPhone, oldest first
Show item properties
label?string
Display text the client gave, e.g. an agent session id
reason *string
The task the client waits to do
since *string (date-time)
When it started waiting
mac?string
Only in remotePhones. Slug of the Mac the iPhone is plugged into, e.g. studio
macName?string
Only in remotePhones. Name of that Mac, e.g. Tommy's Mac Studio
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/phones" \ -H "Authorization: Bearer <token>"
Response example
{ "macName": "Tommy's MacBook Pro", "camera": "authorized", "bluetooth": { "state": "on" }, "phones": [ { "id": "00008020-001A2B3C4D5E002E", "captureId": "7EF219AD-69B4-423E-B8C4-4F4A6140822F", "name": "Tommy's iPhone", "model": "iPhone11,8", "modelName": "iPhone XR", "iosVersion": "17.6.1", "step": "ready", "assistiveTouch": true, "bluetooth": { "address": "A4:83:E7:12:34:56", "state": "connected" }, "waiting": [] } ] }