# Command line

> The zeptap command drives your iPhone from any shell - screenshots saved as files, OCR text with refs, leases between agents, batched steps, and exit codes agents can act on.

Source: https://zeptap.com/docs/agents/cli



`zeptap` is a command line tool that ships inside the Mac app. It drives the same iPhones as the [MCP server](https://zeptap.com/docs/agents/connect), through the running app, so anything with a shell can use it: Claude Code, Codex, Gemini CLI, Aider, scripts, cron jobs, CI.

```sh
zeptap phones                  # which iPhones are paired, and whether they're ready
zeptap look                    # the screen's text, one line per label, each with a @t ref
zeptap tap @t4 --look          # tap it, wait for the screen to settle, print the new screen's text
zeptap screenshot              # save the screen as a JPEG and print its path
```

## When to use it instead of MCP [#when-to-use-it-instead-of-mcp]

Both drive the phone the same way and share one set of tools. Pick by agent:

|                    | Command line                                                           | MCP                                                                                 |
| ------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Works in           | Any agent or script with a shell                                       | Clients with MCP support, including Claude Desktop and ChatGPT, which have no shell |
| Screenshots        | Saved as files: the agent opens one only when text isn't enough        | An image with every action                                                          |
| Reading the screen | `look` prints text with `@t` refs, which is much cheaper than an image | `find_text`                                                                         |
| Many steps         | `zeptap run` does a whole sequence in one call                         | One call per action                                                                 |
| Setup              | On PATH automatically; agents learn it from a skill                    | One click in **Settings › Connectors**                                              |

Agents that only need to know what's on screen use far fewer tokens with `look` than with an image per action. For a model that relies on seeing each screen, MCP is just as good.

## Install [#install]

There's nothing to install separately. When Zeptap runs from your Applications folder, it links `~/.local/bin/zeptap` to the tool inside the app and keeps the link current across updates. If `~/.local/bin` isn't on your shell's `PATH`, it adds one guarded line to `~/.zshenv` (or `~/.bash_profile` for bash), once. **Settings › Connectors › Command line** shows the status and has an **Install** button if the link is missing.

With [Homebrew](https://zeptap.com/docs/get-started/install#homebrew), `brew install --cask zeptap/tap/zeptap` links the same tool.

The Beta build installs `zeptap-beta`, which talks only to Zeptap Beta. It also takes `zeptap` while no other copy of Zeptap has it.

Check everything with:

```sh
zeptap doctor
```

`doctor` checks the command, `PATH`, the app, the connection, the license, the phones and the agent skills, and says how to fix whatever is wrong.

## Teach your agents [#teach-your-agents]

**Settings › Connectors › Command line** lists the agents found on this Mac. **Add** does two things for each one:

| Agent        | Skill                                                          | Also allows `zeptap` to run                                                                                                             |
| ------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code  | `~/.claude/skills/zeptap/SKILL.md`                             | `Bash(zeptap:*)` in `permissions.allow`, and Zeptap's socket folder in `sandbox.network.allowUnixSockets`, in `~/.claude/settings.json` |
| Codex        | `~/.agents/skills/zeptap/SKILL.md`                             | `~/.codex/rules/zeptap.rules`: `prefix_rule(pattern = ["zeptap"], decision = "allow")`                                                  |
| Cursor       | `~/.cursor/skills/zeptap/SKILL.md`                             |                                                                                                                                         |
| Other agents | `~/.agents/skills/zeptap/SKILL.md` (Gemini CLI, OpenCode, ...) |                                                                                                                                         |

The skill is short. It tells the agent to run `zeptap --help` first, so the manual always matches the version installed. `zeptap skill install` writes the same skill files from a terminal, without the permission rules.

Use either the command line or MCP for an agent, not both: two sets of instructions for one phone tend to confuse it.

## Agent sandboxes [#agent-sandboxes]

Codex's sandbox, and Claude Code's when it's turned on, block every way a shell command could reach a local app: TCP, Unix sockets and launching apps. `zeptap` talks to Zeptap over a Unix socket in `~/Library/Application Support/Zeptap/run/`, so one exception covers it:

* **Claude Code**: the socket folder in `sandbox.network.allowUnixSockets`. **Add** in Settings writes it.
* **Codex**: the `allow` rule above. Codex then runs `zeptap` without asking, outside its sandbox.

When a sandbox blocks it, `zeptap` exits with code 8 and prints the setting for the agent it's running in. Agents should call it by its bare name, `zeptap`, not a full path: permission and sandbox rules match the command as typed.

## The loop [#the-loop]

```sh
zeptap phones
zeptap acquire --reason "warm up @sara_runs on Instagram"   # prints a lease id, e.g. 7
zeptap open Instagram -l 7
zeptap look -l 7
zeptap tap -l 7 @t3 --look
zeptap release -l 7
```

Every action waits for the screen to stop moving, then prints what happened and saves a screenshot:

```
[Test iPhone] tap done; settled in 540 ms
target: "Reels" (412,1188)
screenshot: /Users/you/project/.zeptap/shots/0042.jpg (591x1280 px; tap coordinates are pixels in it)
```

Coordinates are pixels in that screenshot, origin top-left: the same space as the MCP tools.

## Seeing the screen [#seeing-the-screen]

| Command                    | What it does                                                                                                         |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `look [<query>] [--exact]` | Runs OCR and prints `@tN  x,y  text` per line, top to bottom. `x,y` is the center. With a query, only matching lines |
| `screenshot [-o <path>]`   | Saves the settled screen as a JPEG (or PNG for a `.png` path) and prints its path and size                           |
| `screenshot --if-changed`  | Prints the previous file instead of saving a new one when the screen hasn't changed                                  |
| `screenshot --annotate`    | Draws each text line's box and its `@t` number on the image                                                          |
| `--look` (on any action)   | Prints the screen's text after the action, in the same call                                                          |

Screenshots go to `./.zeptap/shots/` in the working directory, numbered and limited to the last 50. The folder ignores itself in git. Set `ZEPTAP_OUT_DIR` to use another folder, or pass `-o <path>` for one file. If the working directory isn't writable, they go to `$TMPDIR/zeptap/`.

### Refs [#refs]

`look` numbers the lines it reads as `@t1`, `@t2`, and so on, and remembers them for that phone. `tap @t4` checks that the text is still where it was before tapping. If the text moved a little it taps the new spot. If it went away, the tap is refused with exit code 3, and the agent runs `look` again instead of tapping a guess.

## Touch, keys and apps [#touch-keys-and-apps]

| Command                                             | Notes                                                                                                                                                                          |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tap <x> <y>` \| `@tN` \| `--text <t>`              | `--text` picks an exact line first, then a line containing the text. With several matches it lists them and taps nothing: pass `--index <n>` (1-based, top to bottom) or a ref |
| `tap <x> <y> --expect <t>`                          | Refuses the tap unless that text is at the point. Use it where a mis-tap posts, sends or deletes                                                                               |
| `double-tap`, `long-press [--ms 800]`               | Same targets as `tap`                                                                                                                                                          |
| `drag <x1> <y1> <x2> <y2> [--ms 800]`               | Precise scrolling, sliders, pickers (about 50 px per step). No momentum                                                                                                        |
| `flick up\|down\|left\|right [--from <x>,<y>]`      | A fast swipe with momentum. `up` scrolls far down a list                                                                                                                       |
| `type <text>` \| `--stdin`                          | Types into the focused field. US-keyboard ASCII only                                                                                                                           |
| `key <name> [--mod cmd,shift] [--repeat <n>]`       | `enter`, `escape`, `backspace`, `tab`, `space`, arrows, `a` `c` `v` `x` `z`. Also `key cmd+a`                                                                                  |
| `open <app>`                                        | Opens an app by the name under its icon                                                                                                                                        |
| `home`, `back`, `dismiss`                           | Home Screen, one screen back, and close Spotlight, menus, sheets or the keyboard                                                                                               |
| `lock`, `unlock`                                    | Unlock uses the passcode saved in Zeptap. The agent never sees it                                                                                                              |
| `wait [<ms>]`                                       | Waits, then takes a screenshot. Default 1000 ms, at most 120000                                                                                                                |
| `wait --text <t>` \| `--gone <t>` `[--timeout 10s]` | Polls OCR until the text shows up or goes away. Exit code 6 on timeout                                                                                                         |

The [driving rules](https://zeptap.com/docs/agents/driving) apply as they do over MCP.

## Choosing a phone, and leases [#choosing-a-phone-and-leases]

Without options, commands act on the only paired iPhone, or on the one active in Zeptap. `-p <id or name>` (or `ZEPTAP_PHONE`) picks another phone.

When several agents share a Mac, have each one hold its phone with a lease:

```sh
zeptap acquire --reason "reply to Anna in Messages" --label codex-4411 -p A1B2
```

* stdout is only the lease id, and the message goes to stderr. Pass the id with `-l <id>` (or `ZEPTAP_LEASE`) on every command.
* While a phone is leased, calls from anyone else are refused with exit code 9: other `zeptap` commands, MCP agents and Zeptap's own chat. The refusal says who holds the phone, for what, and when they last used it.
* `--wait <seconds>` waits for a busy phone instead of failing. `--force` takes over a lease. Use it only when that lease is stale.
* A lease ends with `zeptap release -l <id>`, or after `--idle-timeout` seconds without a command (default 600), so a crashed agent can't block a phone for long. Leases also end when Zeptap quits.
* `zeptap phones` shows each phone's lease.

## Many steps in one call [#many-steps-in-one-call]

```sh
zeptap run -l 7 "open Instagram" "tap --text Reels" "wait 3000" "flick up" "wait 3000"
zeptap run -l 7 -f warmup.steps --continue-on-error
```

`run` takes steps as arguments, one per line from a file (`-f <file>`), or from stdin (`-f -`). Blank lines and lines starting with `#` are skipped. The steps run in order and stop at the first failure unless `--continue-on-error` is set. Only the last step saves a screenshot unless `--shots all` is set. The output has one line per step, then the last step's result. Use `run` for sequences the agent has already worked out. It still decides what to do between runs.

## Output for programs [#output-for-programs]

`--json` prints one object instead of text, on stdout, whether the command succeeds or fails:

```json
{"ok":true,"message":"[Test iPhone] tap done; settled in 540 ms","phone":{"id":"A1B2","name":"Test iPhone"},
 "screenshot":{"path":"/…/.zeptap/shots/0042.jpg","width":591,"height":1280},"details":["target: \"Reels\" (412,1188)"]}
```

Failures add `"code"`. `look` and `--look` add `"lines"`: each has a ref, its text, its center and its box. `phones`, `acquire` and `doctor` add `"data"`.

### Exit codes [#exit-codes]

| Code | Meaning                                                                           |
| ---- | --------------------------------------------------------------------------------- |
| 0    | Done                                                                              |
| 1    | Failed: read the message                                                          |
| 2    | Bad arguments, or the target was ambiguous (several text matches, several phones) |
| 3    | Not found: text, a ref (stale or missing), or an app. Nothing was done            |
| 4    | Zeptap isn't running and couldn't be opened                                       |
| 5    | The phone isn't ready: Bluetooth, USB, or locked                                  |
| 6    | Timed out                                                                         |
| 7    | Needs the user: the license, pairing, a permission                                |
| 8    | An agent sandbox blocked the connection to Zeptap                                 |
| 9    | The phone is leased to another agent                                              |

## Everything else [#everything-else]

| Command                                             | What it does                                                                                                                                                                |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `zeptap --help`                                     | The full manual, written for agents                                                                                                                                         |
| `zeptap call <tool> --args '<json>'`                | Calls any MCP tool by name. `zeptap tools` lists them                                                                                                                       |
| `zeptap skill list` \| `get <name> [--file <path>]` | The [skills](https://zeptap.com/docs/agents/tools#skills) turned on in **Settings › Skills**, built in and your own. Load the matching one before a job; `--file` reads one of its reference files |
| `zeptap skill print` \| `install`                   | The SKILL.md for agents, and putting it in their skills folders                                                                                                             |
| `zeptap version`                                    | Versions of the command and of the running app                                                                                                                              |
| `zeptap mcp`                                        | The MCP server over stdio, for clients that only launch commands. `zeptap --stdio` does the same                                                                            |

If Zeptap isn't running, `zeptap` opens it in the background and waits for it (up to 20 s). Inside a sandbox it can't, so the user has to open Zeptap first.

## How it connects [#how-it-connects]

The command finds the running app through `~/Library/Application Support/Zeptap/run/server.json`. The app rewrites that file on every launch: process id, TCP port, socket path, version. The command then sends MCP requests over the Unix socket `zeptap.sock` in the same folder. That folder is readable only by your user (mode 0700), so only your own processes can connect. If the app is too old to have the socket, the command falls back to `http://127.0.0.1:47801/mcp`.
