ZeptapDocs

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.

zeptap is a command line tool that ships inside the Mac app. It drives the same iPhones as the MCP server, through the running app, so anything with a shell can use it: Claude Code, Codex, Gemini CLI, Aider, scripts, cron jobs, CI.

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

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

Command lineMCP
Works inAny agent or script with a shellClients with MCP support, including Claude Desktop and ChatGPT, which have no shell
ScreenshotsSaved as files: the agent opens one only when text isn't enoughAn image with every action
Reading the screenlook prints text with @t refs, which is much cheaper than an imagefind_text
Many stepszeptap run does a whole sequence in one callOne call per action
SetupOn PATH automatically; agents learn it from a skillOne 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

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, 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:

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

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

AgentSkillAlso allows zeptap to run
Claude Code~/.claude/skills/zeptap/SKILL.mdBash(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

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

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

CommandWhat 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-changedPrints the previous file instead of saving a new one when the screen hasn't changed
screenshot --annotateDraws 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

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

CommandNotes
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> | --stdinTypes 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, dismissHome Screen, one screen back, and close Spotlight, menus, sheets or the keyboard
lock, unlockUnlock 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 apply as they do over MCP.

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:

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

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

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

{"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

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

Everything else

CommandWhat it does
zeptap --helpThe 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 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 | installThe SKILL.md for agents, and putting it in their skills folders
zeptap versionVersions of the command and of the running app
zeptap mcpThe 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

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.

On this page