# CLI (/docs/reference/cli)



## Quick reference [#quick-reference]

| Need                                   | Command                                                                 |
| -------------------------------------- | ----------------------------------------------------------------------- |
| one sim / emulator                     | `warden claim ios --json` · `warden claim android --json`               |
| N devices + ports for a script         | `warden run ios --count N --port 8091:20 -- <cmd>`                      |
| One job per device, queued             | `warden batch ios --count N --jobs-from jobs.txt -- <cmd {job} {udid}>` |
| a saved batch from `warden.config.ts`  | `warden batch <preset> [flags] [-- <cmd>]`                              |
| which e2e flows a change needs         | `warden affected [suite] --base main --explain`                         |
| run them and gate on the required ones | `warden e2e [suite] --base main --count N`                              |
| a free port                            | `warden port claim --json`                                              |
| dev build installed on the device      | `warden app ensure ios --udid <udid> --json`                            |
| duplicate a shut-down sim              | `warden clone <udid\|name> [--name x]`                                  |
| pre-build the golden image             | `warden golden ensure --profile iphone-17`                              |
| who holds what                         | `warden ls`                                                             |
| every sim / emulator + who leases it   | `warden devices` (alias `warden list`)                                  |
| lighten a booted sim (fewer daemons)   | `warden sim slim <udid>\|--booted [--dry-run] [--restore]`              |
| tidy Simulator.app windows             | `warden arrange`                                                        |
| is this device free / mine?            | `warden check --udid <udid>` (exit 2 = someone else's)                  |
| keep a long lease alive                | `warden heartbeat --mine`                                               |
| reclaim dead leases                    | `warden gc`                                                             |
| sim + runtime disk usage, what can go  | `warden sims` · `warden sims prune --dry-run`                           |
| pick sims to delete (any owner)        | `warden sims delete` (`--suggested -y` off a terminal)                  |
| setup problems                         | `warden doctor`                                                         |
| update warden                          | `warden update` (`--check` to only look)                                |

## Devices [#devices]

### `warden claim [platform]` [#warden-claim-platform]

Lease iOS sims / Android emulators — reuse, boot or create — booted and ready. `platform` is `ios | android`, asked for when omitted in a terminal.

| Flag                  | Default                 |                                               |
| --------------------- | ----------------------- | --------------------------------------------- |
| `--profile <slug>`    | `iphone-17` / first AVD | device profile, e.g. `iphone-17`, `pixel-10`  |
| `--runtime <runtime>` | `latest`                | `latest`, `iOS-26-5`, `26.5` …                |
| `--count <n>`         | `1`                     | how many devices                              |
| `--max <n>`           | cores/4, cap 4          | max warden devices of this profile            |
| `--wait <duration>`   |                         | wait this long for a free device, e.g. `10m`  |
| `--ttl <duration>`    | `30m`                   | lease TTL without heartbeat                   |
| `--label <label>`     |                         | shown in `warden ls`                          |
| `--adopt`             |                         | allow allocating foreign (non-warden) devices |

### `warden release [leaseIds...]` [#warden-release-leaseids]

Release leases, selected by id, `--udid <udid...>`, `--mine` or `--session <id>`. `--shutdown` also shuts down devices warden created (or the lease owner booted).

### `warden run [platform] -- <cmd...>` [#warden-run-platform----cmd]

Claim devices, run a command with `WARDEN_UDIDS` set, release on exit. Takes every `claim` flag, plus:

| Flag                      |                                                                                                                   |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `--port <from:span>`      | lease a port for the child (`WARDEN_PORT_<i>`); repeatable                                                        |
| `--app`                   | install the project's app on each device first                                                                    |
| `--project <dir>`         | `--app`: project directory (default: cwd)                                                                         |
| `--bundle-id <id>`        | `--app`: override the bundle id / package                                                                         |
| `--no-eas` / `--no-build` | `--app`: skip EAS downloads / local builds                                                                        |
| `--clean`                 | `--app`: uninstall the app first, so every run starts from a fresh container (even when the hash already matches) |

See [e2e scripts](/docs/guides/e2e).

### `warden batch [platform|preset] [-- <cmd...>]` [#warden-batch-platformpreset----cmd]

Claim devices and fan a job queue out over them, one worker per device; the command runs once per job with `{job}`, `{udid}`, `{worker}` and `{seq}` substituted. Takes every `run` flag, plus:

| Flag                                      |                                                                                   |
| ----------------------------------------- | --------------------------------------------------------------------------------- |
| `--jobs <list>` / `--jobs-from <file\|->` | the jobs: comma-separated, or one per line                                        |
| `--retry <n>`                             | re-run a failed job up to N times on the same device                              |
| `--passes <n>`                            | a job passes only after N consecutive green runs on its device (`WARDEN_PASS`)    |
| `--serve <sh-cmd>`                        | a long-lived process (e.g. Metro) started before the jobs, killed at the end      |
| `--serve-ready <probe>`                   | wait for `http://…`, `tcp:PORT` or `file:PATH` first                              |
| `--serve-timeout <duration>`              | give up waiting for `--serve-ready` (default `10m`)                               |
| `--record <dir>`                          | iOS: record every simulator and the TUI (`batch.json`, `tui.cast`, `dev-<i>.mp4`) |
| `--logs <dir>`                            | per-job logs                                                                      |
| `--no-tui`                                | plain log lines instead of the live grid                                          |

`--label` also labels the port leases (default `warden batch`). With fewer jobs than `--count`, only one device per job is claimed.

If the first operand is the name of a preset in [`warden.config.ts`](/docs/reference/configuration#batches) `batches` (found by walking up from the cwd to the git root), that preset supplies the platform and defaults:

* Flags you actually pass override the preset; commander defaults (`--count 1`, `--retry 0`, `--serve-timeout 10m`…) do not.
* `--jobs` / `--jobs-from` replace the preset's `jobs` / `jobsFrom`.
* `-- <cmd>` replaces the preset's `cmd`; without it the preset's `cmd` runs.
* Relative paths in the preset resolve against the preset's cwd (its `project` root, else the config file's directory); paths passed on the command line resolve against your cwd.
* Serve, jobs and a `jobsFrom.command` run in the preset's cwd.
* The label defaults to the preset name.

```bash
warden batch salient-e2e                # everything from the preset
warden batch salient-e2e --count 2 --jobs qa-login -- bun e2e {job} --device {udid}
```

An operand that is neither `ios`, `android` nor a preset fails with the list of presets.

Exits 0 only when every job passed. See [Batches](/docs/guides/e2e#batches-one-job-per-device).

### `warden affected [suite]` [#warden-affected-suite]

List the flows of `e2e.<suite>` (optional with one suite) that the changes since `merge-base(--base, HEAD)` need: one id per line.

| Flag                    |                                                                |
| ----------------------- | -------------------------------------------------------------- |
| `--base <ref>`          | compare against this ref (default: the suite's `base`, `main`) |
| `--platform <platform>` | `ios \| android` (default: the suite's `platform`, else both)  |
| `--files <list>`        | comma-separated changed files (relative to cwd) instead of git |
| `--required-only`       | only `required` flows                                          |
| `--explain`             | why each flow was picked, with the import chain                |
| `--strict`              | exit 3 when a changed file reaches no flow                     |
| `--json`                | selections, reasons, skipped and unreached files               |

See [Affected e2e](/docs/guides/affected-e2e).

### `warden e2e [suite]` [#warden-e2e-suite]

Select like `warden affected`, then run the flows like `warden batch` — the suite's `runner` once per flow — and gate: exit 0 when every required flow passed, 1 otherwise, 130 when interrupted. Takes every `batch` flag except the job flags, the `affected` selection flags, plus:

| Flag              |                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| `--dry-run`       | select and explain, run nothing                                                                  |
| `--all`           | run every flow of the suite (after `include` / `exclude`), no git diff                           |
| `--flows <list>`  | run exactly these flow ids / globs (comma-separated), no git diff; a list matching nothing fails |
| `--report <file>` | also write `e2e-report.json` here                                                                |
| `--no-shutdown`   | keep the devices running after the gate passes                                                   |

The suite's `count`, `profile`, `retry`, `passes`, `app`, `ports`, `env`, `serve`, `serveReady` and `serveTimeout` apply unless the flag is passed; `project` sets the cwd. Flows run in id order (sorted file path), so `count: 1` is a stable serial run. Nothing affected → exit 0 without claiming. Every failed flow is named at the end with its device, runner log and failure screenshot (also `device`, `log`, `screenshot` on the flow in `e2e-report.json`). Leases are always released at the end; when the gate passes, the devices warden created (or booted for the run) are shut down first — a failed or interrupted run leaves them up to inspect, and a sim that was already running when leased is never touched.

### `warden ls` [#warden-ls]

List leases: resource, state, owner, repo/worktree, age, heartbeat.

### `warden devices [platform]` (alias `list`) [#warden-devices-platform-alias-list]

Every simulator / emulator, booted or not, plus Android AVDs — with warden ownership and leases.

### `warden sim slim [udid]` [#warden-sim-slim-udid]

iOS. Switches off the launchd jobs inside a booted simulator that UI flows never need — Siri, Apple Intelligence, Health, News, Mail, Photos analysis, Game Center, Wallet and friends (a conservative denylist; SpringBoard, installd, XCTest, the accessibility tree and the keyboard are never touched). A booted simulator runs 200+ such processes; with several simulators side by side, that is the RAM and CPU a flow suite is starved of.

| Flag        |                                                   |
| ----------- | ------------------------------------------------- |
| `--booted`  | every booted simulator instead of one `<udid>`    |
| `--dry-run` | list the jobs, change nothing                     |
| `--restore` | re-enable them (fully effective on the next boot) |

Each job is `launchctl disable`d inside the simulator (remembered across its reboots) and booted out so it stops now. Idempotent. The disabled state is not copied by `warden clone`, so slim every device you use. A simulator leased by another session is refused. In an e2e suite, `"slim": true` does this on every leased device before `setup` — see [Affected e2e](/docs/guides/affected-e2e#per-device-setup).

### `warden sims audit | prune | delete` [#warden-sims-audit--prune--delete]

`audit`, `prune` and `delete` share one listing (`simctl list devices -j`: every platform, unavailable runtimes included) and one set of rules, so they always agree about a sim. The only difference is which sims each one acts on.

**Owner.** `warden` (in warden's store, or named `warden-<profile>-N`), `golden` (`warden-golden-…`), or `foreign` (anything else).

**Blockers.** A sim is `leased` (any lease, live or stale; a stale one shows `run warden gc`), `booted`, or `golden`.

**Reasons** (the same text appears in the audit's VERDICT column and in the delete menu hint):

| Reason                         | Owner   | When                                                                                                         |
| ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------ |
| runtime removed                | any     | the sim's runtime is no longer installed (`isAvailable: false`)                                              |
| warden-named, no warden record | warden  | `warden-<profile>-N` that the store has no record of (`orphan`)                                              |
| warden sim unused N            | warden  | not claimed or booted (the later of the store's `lastUsedAt` and `lastBootedAt`) for `--idle` (default `7d`) |
| over --max-size                | warden  | with `--max-size 40G`, the least-recently-used unblocked warden sims until all sims fit (`budget`)           |
| not booted in N                | foreign | last booted more than `--stale` ago (default `30d`). Never-booted sims are left alone.                       |
| older runtime                  | foreign | older than the newest runtime installed for the same device type and platform                                |
| duplicate                      | foreign | same name and runtime as a more recently booted sim                                                          |

Warden sims never get `older runtime` or `duplicate`: warden manages its own pool. Goldens never get a reason; they belong to `warden golden prune`.

| Subcommand            | Acts on                                                                                                                                       |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `audit` (the default) | nothing; read-only. VERDICT is `keep (leased\|booted\|golden\|recent)`, `delete: <reasons>` (prune will delete it), or `foreign[: <reasons>]` |
| `prune`               | warden sims with a reason and no blockers                                                                                                     |
| `delete`              | any sim you pick except leased ones and goldens. Unblocked sims with a reason start ticked.                                                   |

Sizes are simctl's `dataPathSize` (the device's `data/` dir, nearly all of it), with `du` as a fallback.

**Deleting.** `prune` and `delete` use the same delete path. Each sim is leased to the caller under the store lock, and skipped if anyone holds a lease on it, even a stale one. It is then shut down (if its runtime still exists), `simctl delete`d, forgotten from the store, and released. A racing `warden claim` either wins (the sim is skipped) or waits.

#### `warden sims audit` [#warden-sims-audit]

Lists every sim with its size, owner, lease, last use and verdict, then every downloaded runtime (`xcrun simctl runtime list`, Xcode 15+). For each runtime it shows the size (7–9 GB each), last use, how many sims use it, and a verdict: `in use`, `unused`, `unused after prune` (only sims prune would delete use it) or `unused (not deletable)`. Runtimes are machine-wide, so warden only reports them; free one with `xcrun simctl runtime delete <identifier>`.

Flags: `--idle`, `--stale`, `--max-size`, `--owner warden|golden|foreign|all` and `--json`. In the JSON, each entry has `blockers`, `reasons` and `verdict`, and runtimes sit under `runtimes` (`null` plus `runtimesError` when simctl can't list them). The audit also says if you'd still be over `--max-size` after pruning.

#### `warden sims prune` [#warden-sims-prune]

Deletes the sims `audit` marks `delete` (`--idle`, `--max-size`). Use `--dry-run` to preview. Off a terminal it needs `--yes`. It never deletes foreign sims or goldens.

#### `warden sims delete [udids...]` (alias `sims rm`) [#warden-sims-delete-udids-alias-sims-rm]

Permanently deletes simulators. With no udids it opens a multi-select menu (space toggles, enter submits) of every sim. Suggested sims come first and start ticked; the hint shows their reasons, state, runtime and size. Leased sims and goldens are shown but can't be picked. It lists what will go and asks before deleting unless you pass `-y`.

| Flag                                       | Meaning                                                                                                      |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `--suggested`                              | delete every suggestion instead of showing the menu                                                          |
| `--stale <duration>` / `--idle <duration>` | rule thresholds (same as `audit`)                                                                            |
| `--dry-run`                                | only show what would be deleted, with reasons (works off a terminal)                                         |
| `-y, --yes`                                | don't ask; required off a terminal (with udids or `--suggested`)                                             |
| `--json`                                   | `{ deleted, failed }`, or `{ dryRun, wouldDelete: [{ id, name, reasons }] }`; exits 1 if any sim was skipped |

### `warden arrange` [#warden-arrange]

macOS only. Lines up the open Simulator.app windows of warden sims in name order (`warden-iphone-17-2` before `-10`), left to right from the top-left of the main screen. Simulator windows can't be resized, so a row wider than the screen overlaps evenly, with every title bar left visible; windows wrap to a second row only if the screen is tall enough. Other simulators are left where they are, and Simulator.app is never launched. Every iOS claim (`claim`, `run`, `batch`, `e2e`) runs this automatically; set `WARDEN_ARRANGE=0` to turn that off. Moving windows uses System Events, so your terminal needs Accessibility access (System Settings → Privacy & Security → Accessibility).

### `warden clone <source>` [#warden-clone-source]

Duplicate a shut-down iOS simulator (udid or name) into warden's pool. `--name` defaults to the next free `warden-<profile>-N`.

### `warden golden ensure | ls | prune` [#warden-golden-ensure--ls--prune]

Golden iOS images new sims are cloned from. `ensure [--profile] [--runtime]` builds once or reuses; `prune` deletes stale goldens (`--all` every golden, `-y` skips the prompt). See [Golden images](/docs/guides/golden-images).

### `warden check` [#warden-check]

Exit 0 if a device (`--udid`) is free or yours, **2** if another owner holds it. `--session <id>` checks against that agent session instead of the caller.

### `warden heartbeat [leaseIds...]` [#warden-heartbeat-leaseids]

Refresh heartbeats so leases don't go stale. Same selectors as `release`.

### `warden gc` [#warden-gc]

Reclaim stale leases and shut down unleased warden devices idle for `--idle` (default `20m`). Never deletes — use `warden sims prune` to free disk. `--quiet` prints nothing (used by background auto-gc).

## Ports [#ports]

### `warden port claim` [#warden-port-claim]

`--from` (8091), `--span` (20), `--count` (1), `--ttl` (30m), `--label`. Prints one port per line. See [Ports](/docs/guides/ports).

### `warden port release [ports...]` [#warden-port-release-ports]

By port, `--lease <id...>` or `--mine`. `--force` releases leases held by someone else.

## Builds [#builds]

### `warden app fingerprint [platform]` [#warden-app-fingerprint-platform]

Print the project's native fingerprint (both platforms by default). `--project`, `--bundle-id`, `--variant`.

### `warden app ensure [platform]` [#warden-app-ensure-platform]

Install the build matching the fingerprint on a device (`--lease <id>` or `--udid`). `--no-install` only resolves into the cache; `--clean` uninstalls the app first even when the hash matches; `--no-eas`, `--no-build`, `--project`, `--bundle-id`, `--variant <name>`. See [App builds](/docs/guides/app-builds).

### `warden dev [platform] [-- <expo start args>]` [#warden-dev-platform----expo-start-args]

The worktree-friendly `expo run:*`. Uses the device of `--udid` / `--lease` / your single lease, else claims one (claim options apply). Ensures the `dev` variant's build (or the project's own when it has no `dev`; `--variant` to pick, which must exist), leases a Metro port (`--port`, default `8081:100`), runs `bunx expo start --dev-client --port <p>` in the project root, waits for `/status` (`--ready-timeout`, default `2m`), then opens `exp+<slug>://expo-development-client/?url=…` on the device (`--scheme` to override; Android runs `adb reverse` first). Ctrl-C stops Metro and releases what it leased. `--no-eas`, `--no-build`, `--clean`, `--project`, `--bundle-id`.

### `warden builds ls | prune | import` [#warden-builds-ls--prune--import]

* `prune [--max-size 20G] [--dry-run] [--yes]` — remove least-recently-used builds until the cache fits; build-locked ones stay.
* `import <path.app|path.apk> --hash <h> [--platform] [--project] [--profile] [--bundle-id]` — copy a local build into the cache.

## Setup [#setup]

### `warden install` [#warden-install]

Install warden to `~/.local/bin` plus hooks for detected agents. `--claude`, `--codex`, `-y/--yes`, `--dry-run`, `--shim` (running from source: link `~/.local/bin/warden` to this checkout).

### `warden skill show | install` [#warden-skill-show--install]

`install --from local|github`, `--project`, `-a/--agent <agent...>`, `--copy`, `-y`, `--dry-run`.

### `warden hook pretool | session-end` [#warden-hook-pretool--session-end]

The agent hook entrypoints. Called by Claude Code / Codex, not by hand.

### `warden doctor` [#warden-doctor]

Check home, db, device tools, install method (and its upgrade command), `PATH`, Claude/Codex hooks + skill, and stale leases.

### `warden update` [#warden-update]

`--check`, `--release`, `--force`, `--to <path>`. npm / bun installs print their package manager's upgrade command instead. See [Updating](/docs/updating).

### `warden version` [#warden-version]

Print the version and build channel (`dev` / `local` / `release`).
