warden

CLI

Every warden command and flag. All commands accept --json.

Quick reference

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

Devices

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.

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

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...>

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
--appinstall 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.

warden batch [platform|preset] [-- <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-tuiplain 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 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.
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.

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-onlyonly required flows
--explainwhy each flow was picked, with the import chain
--strictexit 3 when a changed file reaches no flow
--jsonselections, reasons, skipped and unreached files

See Affected e2e.

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-runselect and explain, run nothing
--allrun 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-shutdownkeep 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

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

warden devices [platform] (alias list)

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

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
--bootedevery booted simulator instead of one <udid>
--dry-runlist the jobs, change nothing
--restorere-enable them (fully effective on the next boot)

Each job is launchctl disabled 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.

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

ReasonOwnerWhen
runtime removedanythe sim's runtime is no longer installed (isAvailable: false)
warden-named, no warden recordwardenwarden-<profile>-N that the store has no record of (orphan)
warden sim unused Nwardennot claimed or booted (the later of the store's lastUsedAt and lastBootedAt) for --idle (default 7d)
over --max-sizewardenwith --max-size 40G, the least-recently-used unblocked warden sims until all sims fit (budget)
not booted in Nforeignlast booted more than --stale ago (default 30d). Never-booted sims are left alone.
older runtimeforeignolder than the newest runtime installed for the same device type and platform
duplicateforeignsame 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.

SubcommandActs on
audit (the default)nothing; read-only. VERDICT is keep (leased|booted|golden|recent), delete: <reasons> (prune will delete it), or foreign[: <reasons>]
prunewarden sims with a reason and no blockers
deleteany 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 deleted, forgotten from the store, and released. A racing warden claim either wins (the sim is skipped) or waits.

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

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)

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.

FlagMeaning
--suggesteddelete every suggestion instead of showing the menu
--stale <duration> / --idle <duration>rule thresholds (same as audit)
--dry-runonly show what would be deleted, with reasons (works off a terminal)
-y, --yesdon'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

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>

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

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.

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...]

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

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

warden port claim

--from (8091), --span (20), --count (1), --ttl (30m), --label. Prints one port per line. See Ports.

warden port release [ports...]

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

Builds

warden app fingerprint [platform]

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

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.

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

  • 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

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

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

warden hook pretool | session-end

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

warden doctor

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

warden update

--check, --release, --force, --to <path>. npm / bun installs print their package manager's upgrade command instead. See Updating.

warden version

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

On this page