# Control an iPhone remotely (/docs/apps/iphone-remote)

See an iPhone's screen live and drive it from the hub, the Android app or any codai agent, through the codai iPhone bridge — no jailbreak, same account only.

The **codai iPhone bridge** runs on the Windows PC your iPhone is paired with and drives the phone over USB or Wi-Fi (pymobiledevice3, developer image, WebDriverAgent-free tools). With **remote access** switched on, the bridge also connects to codai by itself — no desktop app, no inbound port — so you can control the phone from anywhere:

- **Control** goes through the [hosts relay](../sessions/hosts.mdx): the bridge is a host with caps `iphone` and `video`, and every client calls `POST /v1/hosts/{deviceId}/exec` with op `iphone`.
- **Live screen** goes through codai-relay: the bridge is the daemon of its own environment **"iPhone &lt;name&gt;"** and streams the screen as HEVC video.

```text
   hub / Android / agent            codai gateway + relay                PC with the bridge        iPhone
   ─────────────────────           ─────────────────────               ──────────────────        ──────
   POST /v1/hosts/:dev/exec  ───▶  hosts relay (SSE)            ───▶  same tool code as the  ─▶  tap, type,
        { op: 'iphone', … }                                            local MCP                  open app…
   video viewer (WebSocket)  ◀───  relay stream kind `video`    ◀───  DisplayService HEVC    ◀─  screen
```

## Set up the bridge [#set-up-the-bridge]

Install the bridge first (see `apps/iphone-bridge/README.md`; it ships with the codai desktop MCP `codai-iphone`). Then, once:

<Steps>
<Step>

### Give it an API key [#give-it-an-api-key]

Create a key in the hub (**Keys**) and store it in a file next to the bridge's `control.json` — never in the config itself:

```powershell
Set-Content -NoNewline "$env:LOCALAPPDATA\codai\iphone-bridge\api-key.txt" 'codai_…'
'{"relay": {"enabled": true, "api_key_file": "%LOCALAPPDATA%/codai/iphone-bridge/api-key.txt"}}' |
  Set-Content "$env:LOCALAPPDATA\codai\iphone-bridge\config.json"
```

</Step>
<Step>

### Enrol the phone [#enrol-the-phone]

```powershell
& "$env:LOCALAPPDATA\codai\iphone-bridge\venv\Scripts\codai-iphone-bridge.exe" relay enroll
```

`relay enroll` creates (or reuses) the project **"codai iPhone"** and a BYO environment **"iPhone &lt;name&gt;"**, and pins this phone's stable device id to it. It is idempotent; options `--udid`, `--name`, `--base-url`.

</Step>
<Step>

### Start it with remote access [#start-it-with-remote-access]

Restart the bridge, or run it with `--relay` (equivalent to `relay.enabled: true`). It registers with `os=ios`, `caps=iphone,video` and the environment id, and reconnects with backoff (1 → 30 s) on network failures. A rejected key or another bridge taking over the same environment stops the remote side until you restart it.

</Step>
</Steps>

`config.json` keys under `relay`: `enabled`, `api_key_file` (else `CODAI_API_KEY`), `base_url` (default `https://ai.codai.ro`), `relay_url` (default: what the gateway returned at enrolment), `allow_sensitive` (default `false`) and `video` (default `true`; `false` = control only).

## Use it [#use-it]

| Where | How |
| --- | --- |
| **Hub** | [hub.codai.ro/devices/iphone](https://hub.codai.ro/devices/iphone) — *Remote iPhones* lists the phones online; open one for the live screen and controls (tap, scroll, Home, buttons, typing). |
| **Android app** | **Settings → Remote hosts → iPhone**: the same list, a live screen you tap and drag on, and Home / Back / Lock buttons. |
| **Agents** | The `iphone_*` tools in every codai agent (desktop, Android, VS Code, headless): `iphone_hosts`, `iphone_screen`, `iphone_screenshot`, `iphone_find`, `iphone_click`, `iphone_tap`, `iphone_swipe`, `iphone_type`, `iphone_key`, `iphone_button`, `iphone_home`, `iphone_open_app`, `iphone_open_url`, `iphone_list_apps`, `iphone_wait_for`. Each takes an optional `host` (device id); the default is the only online host with cap `iphone`. |
| **API / SDKs** | `POST /v1/hosts/{deviceId}/exec` with `{ "op": "iphone", "args": { "tool": "tap", "args": { "x": 200, "y": 640 } } }`; `client.hosts.iphone(deviceId, 'tap', { x: 200, y: 640 })` in TypeScript, `client.hosts.iphone(device_id, "tap", {"x": 200, "y": 640})` in Python. |

The answer is `{ ok, result: { data, image? } }`: `data` is what the bridge tool returned and `image` an optional JPEG screenshot as a data URL (≤ 1.5 MB, whole result ≤ 2 MB). Tool names and arguments are those of the local `codai-iphone` MCP without the `iphone_` prefix.

## Live screen and fallback [#live-screen-and-fallback]

A viewer opens a `video` stream on the phone's environment. The bridge starts **CoreDevice DisplayService** (HEVC, typically 30–55 fps), shares one session between all viewers and stops it with the last one. When the stream cannot run — another app holds the camera or microphone (error 9022), the phone does not support it, or it failed three restarts — the bridge switches automatically to **JPEG screenshots at about 2 fps** and the Android app shows *Screenshots* instead of *Live*. A slow viewer gets frames dropped until the next keyframe, never a closed connection.

The hub opens the relay WebSocket with a single-use ticket valid for 60 seconds, so your API key never reaches the browser.

## Security [#security]

- **Same user only.** The gateway relays an `iphone` op only between devices of the same account; there is no sharing across users.
- **Sensitive tools are refused remotely.** Install / uninstall and `wifi_setup` run only locally unless you set `relay.allow_sensitive: true` on the bridge.
- **Everything is journaled.** Remote calls run the same tool code as local ones and land in the bridge journal marked `"via": "relay"`.
- **Your key stays on the PC**, in the file you chose; the bridge never prints it.

## Troubleshooting [#troubleshooting]

| Symptom | Fix |
| --- | --- |
| `409 host_offline` | The bridge is not connected with remote access: start it with `--relay` or `relay.enabled: true`. |
| *No iPhone online* in the hub | Same as above; it appears within a few seconds of the bridge connecting. |
| *Screenshots* instead of *Live* | DisplayService is unavailable (camera or microphone in use, unsupported); control keeps working. |
| Phone not found over Wi-Fi | Keep it awake and on the same network, or plug the cable in once and call `wifi_setup` locally. |
| Developer image mount fails | Close tools that hold the image mounter (Impactor, 3uTools) before starting the bridge. |
