# CLI reference

> phone-harness entry points: stdin Python exec with helpers in scope, --doctor and doctor, skill, help flags, usage errors when args or TTY lack a script, and the ./phone-harness checkout launcher.

- Repository: ShawnPana/phone-harness
- GitHub: https://github.com/ShawnPana/phone-harness
- Human docs: https://grok-wiki.com/public/docs/shawnpana-phone-harness-bf80173a2a2e
- Complete Markdown: https://grok-wiki.com/public/docs/shawnpana-phone-harness-bf80173a2a2e/llms-full.txt

## Source Files

- `src/phone_harness/run.py`
- `pyproject.toml`
- `phone-harness`
- `src/phone_harness/admin.py`
- `src/phone_harness/helpers.py`
- `SKILL.md`

---

---
title: "CLI reference"
description: "phone-harness entry points: stdin Python exec with helpers in scope, --doctor and doctor, skill, help flags, usage errors when args or TTY lack a script, and the ./phone-harness checkout launcher."
---

`phone-harness` is a single console entry point (`phone_harness.run:main` in `pyproject.toml`) that either runs a named subcommand or `exec`s a Python script from stdin with the public `helpers` namespace already in scope. There is no daemon, REPL, or file-path argument mode: every control session is one process, one stdin body (or one doctor/skill invocation), then exit.

## Entry points

| Surface | How it is invoked | Implementation |
| --- | --- | --- |
| Installed command | `phone-harness …` on `PATH` | `[project.scripts]` → `phone_harness.run:main` |
| Checkout launcher | `./phone-harness …` from the repo root | Shell wrapper: `PYTHONPATH=$DIR/src exec python3 -m phone_harness.run "$@"` |
| Module form | `python3 -m phone_harness.run …` with `src` on `PYTHONPATH` | Same `main()` as the console script |

Both the installed command and the checkout launcher share argument parsing and exit behavior. The launcher exists so a working tree can be used without `pip install`.

## Command dispatch

`main()` reads `sys.argv[1:]` and branches in fixed order:

1. `-h` / `--help` → print usage text, return (exit `0`)
2. `--doctor` / `doctor` → `admin.run_doctor()`, then `sys.exit(code)`
3. `skill` → print repo-root `SKILL.md` to stdout, return (exit `0`)
4. Otherwise → stdin script mode (see below)

```text
argv[0]
  ├─ -h | --help     → print USAGE, exit 0
  ├─ --doctor|doctor → run_doctor(), exit 0|1
  ├─ skill           → print SKILL.md, exit 0
  └─ (no args)       → read stdin Python and exec
                       if TTY / extra args / empty → print USAGE, exit 1
```

There are no long-option parsers, subcommand groups, or positional script paths. Unknown first tokens fall through to the stdin gate and become usage errors.

## Usage text

Printed by help and by usage failures:

```text
Usage:
  phone-harness <<'PY'
  print(screen_info())
  PY

Commands:
  phone-harness --doctor    diagnose permissions, app, and session state
  phone-harness skill       print the phone-harness skill text
```

## Stdin Python exec

Default mode when the process is not a TTY and stdin has non-empty code.

### Preconditions

| Condition | Result |
| --- | --- |
| Any argv after the program name (except the known commands above) | Usage error |
| `sys.stdin.isatty()` is true (interactive terminal, no pipe/heredoc) | Usage error |
| Stdin is empty or only whitespace | Usage error |
| Non-empty stdin and no extra args | Script runs |

Typical invocation:

```bash
phone-harness <<'PY'
print(connection_state())
print(screen_info())
PY
```

Pipes work the same way:

```bash
echo 'print(screen_info())' | phone-harness
```

### Namespace

On success, `run.py`:

1. Imports `phone_harness.helpers` (which may load agent helpers at import time)
2. Builds `g = {k: v for k, v in vars(helpers).items() if not k.startswith("_")}`
3. Sets `g["__name__"] = "__main__"`
4. Calls `exec(code, g)`

So scripts see public helpers as bare names (`screen_info`, `ocr`, `tap`, …), not as `phone_harness.helpers.ocr`. Names starting with `_` are omitted. Modules and constants that do not start with `_` (for example `mirror`, `tap`, `REPO_ROOT`, `AGENT_WORKSPACE`) are also present because they live on the helpers module.

Agent-editable symbols from `agent-workspace/agent_helpers.py` (or `PH_AGENT_WORKSPACE/agent_helpers.py`) are merged into the helpers module at import via `_load_agent_helpers()`, so they appear in the same stdin namespace without an extra import.

Raw Quartz remains available inside the script via a normal import:

```python
import Quartz  # escape hatch when helpers are not enough
```

### Runtime characteristics

- **One shot.** No REPL loop; the process ends when `exec` returns or raises.
- **No built-in top-level exception wrapper.** Uncaught exceptions propagate to the Python process exit (typically non-zero).
- **Backend selection** for taps/capture happens when helpers is imported (`PHONE_HARNESS_BACKGROUND`, SkyLight fallback). It is not a CLI flag.
- **No session daemon.** Window bounds and captures are re-queried inside helpers; each CLI invocation is self-contained.

### Minimal script examples

```bash
# Connection gate before work
phone-harness <<'PY'
print(connection_state())
PY
```

```bash
# Capture ground truth
phone-harness <<'PY'
info = screen_info()
print(info)
print(ocr()[:5])
PY
```

```bash
# Gesture after ensure_mirroring
phone-harness <<'PY'
ensure_mirroring()
home()
wait_stable()
print([o["text"] for o in ocr()][:20])
PY
```

## Help flags

| Flag | Behavior | Exit |
| --- | --- | --- |
| `-h` | Print `USAGE` to stdout | `0` |
| `--help` | Same | `0` |

Only the first argument is checked. Combined forms such as `phone-harness --help --doctor` still match help and do not run doctor.

## Doctor

| Form | Equivalent |
| --- | --- |
| `phone-harness --doctor` | Canonical |
| `phone-harness doctor` | Same path |

Both call `admin.run_doctor()` and exit with its return code.

Doctor prints a headed ladder and per-check `[PASS]` / `[FAIL]` lines. Summary:

- Success: `all clear` → exit `0`
- Failure: `fix the FAILs above, then re-run` → exit `1`

Check order (early hard-stop only on missing pyobjc):

| Order | Check | Fatal to overall `ok` |
| --- | --- | --- |
| 1 | pyobjc frameworks (Quartz, Vision, AppKit) | Yes — returns `1` immediately if import fails |
| 2 | Accessibility (`AXIsProcessTrusted`) | Yes |
| 3 | Screen Recording (`CGPreflightScreenCaptureAccess`) | Yes |
| 4 | iPhone Mirroring app installed at known path | Yes |
| 5 | App running | Reported; not fatal by itself |
| 6 | Mirroring window found | Reported |
| 7 | Window capture size `> 20_000` bytes | Yes when a window exists |
| 8 | Vision OCR on that capture | Reported when capture is large enough |

Doctor always ends with a note that a fresh machine may still prompt for extra permissions on first real action even when the ladder passes.

Full ladder semantics and recovery live on the doctor diagnostics page; the CLI only needs the flags, exit codes, and that output is human-oriented text on stdout (not JSON).

## Skill

```bash
phone-harness skill
```

- Resolves the package file to the repository root (`run.py` → `src/phone_harness` → `src` → repo)
- Prints the full contents of `SKILL.md` to stdout (UTF-8, no trailing rewrite)
- Exit `0`

Used to install or re-sync agent skill bodies:

```bash
mkdir -p ~/.claude/skills/phone-harness
phone-harness skill > ~/.claude/skills/phone-harness/SKILL.md

mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills/phone-harness"
phone-harness skill > "${CODEX_HOME:-$HOME/.codex}/skills/phone-harness/SKILL.md"
```

The skill body describes when to use the harness, stdin usage, consent, connection rules, and helper patterns. Re-run the redirect after `git pull` so installed copies stay current.

## Usage errors

`sys.exit(USAGE)` is used when stdin mode cannot start. That prints the usage string and exits with code `1`.

| Trigger | Example |
| --- | --- |
| Extra / unknown argv | `phone-harness foo` |
| Interactive TTY, no script | typing `phone-harness` at a shell prompt |
| Empty stdin | `phone-harness </dev/null` or a zero-length pipe |
| Known commands misspelled as extra args | `phone-harness --doc` (falls through to stdin gate) |

Help (`-h` / `--help`) is not a usage error: it prints the same text with exit `0`.

## Checkout launcher

Repository root file `phone-harness` (executable shell script):

```sh
#!/bin/sh
# Dev launcher: run the working tree without installing.
DIR="$(cd "$(dirname "$0")" && pwd)"
PYTHONPATH="$DIR/src" exec python3 -m phone_harness.run "$@"
```

| Property | Value |
| --- | --- |
| Purpose | Run the checkout without an editable install |
| `PYTHONPATH` | `$DIR/src` so `phone_harness` resolves from the tree |
| Process model | `exec` replaces the shell with `python3 -m phone_harness.run` |
| Args | Forwarded with `"$@"` |

Examples:

```bash
./phone-harness --doctor
./phone-harness skill
./phone-harness <<'PY'
print(screen_info())
PY
```

Behavior matches the installed command for flags and stdin mode. Prefer the PATH install for agent skills and automation that must work from any cwd; prefer `./phone-harness` while developing against an uninstalled tree.

## Exit codes

| Situation | Code |
| --- | --- |
| `-h` / `--help` | `0` |
| `skill` | `0` |
| `--doctor` / `doctor` all required checks pass | `0` |
| Doctor failure (including early pyobjc import fail) | `1` |
| Usage error (TTY, extra args, empty stdin) | `1` |
| Stdin script completes without raising | `0` (normal Python exit) |
| Stdin script raises | Non-zero (Python traceback / uncaught exception) |

## What the CLI does not provide

These surfaces are intentionally absent from `run.py`:

- Path arguments to a `.py` file (`phone-harness script.py` is a usage error)
- Interactive REPL
- Flags for backend selection, workspace path, or capture directory (use environment variables instead)
- JSON/machine output for doctor
- Background service or long-lived session process

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Clone, pyobjc, editable install, PATH command, and first `--doctor` verification.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First stdin script: connection state, `screen_info`, and success signal.
  </Card>
  <Card title="Register as an agent skill" href="/register-agent-skill">
    `phone-harness skill` install paths for Claude Code and Codex, and re-sync after pulls.
  </Card>
  <Card title="Helpers API" href="/helpers-api">
    Public names available in the stdin `exec` namespace and their return shapes.
  </Card>
  <Card title="Doctor diagnostics" href="/doctor-diagnostics">
    Ordered ladder, per-check meaning, and exit behavior beyond the CLI flag.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    `PHONE_HARNESS_BACKGROUND`, `PH_AGENT_WORKSPACE`, and backend fallback.
  </Card>
</CardGroup>
