CLI
Every warden command and flag. All commands accept --json.
This is the exhaustive list of warden commands and flags; use it to look up an option, and warden <command> --help for the version you have installed.
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 specs 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 |
| look at / drive a leased device | warden ui snapshot -i --udid <udid> · warden ui press @e3 --settle (guide) |
| record a session as a spec | warden ui record start <id> → drive → warden ui record stop (guide) |
| 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) |
| is the agent loop fast enough? | warden ui bench [--runs n] [--json] |
| what profiling needs on this host / device | warden perf doctor |
| app CPU / memory now, or live | warden perf metrics [--watch] |
| JS hotspots (source-mapped) | warden perf js --duration 10s |
| cold start time | warden perf startup --cold --runs 5 --claim |
| why does this screen re-render? | warden perf react --duration 10s |
| native CPU / hangs / leaks | warden perf native --duration 10s |
| the boot queue and host budget | warden capacity · warden queue |
| a dev client per worktree | warden dev (guide) |
| set an Expo app up for warden | warden init |
| install warden + agent hooks, tab completion | warden install · warden completion zsh |
| the agent skill | warden skill show · warden skill install |
| setup problems | warden doctor |
| update warden | warden 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.
| 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> | what the machine's capacity allows | max warden devices of this profile (pool size; machine-wide boots are capped by capacity) |
--wait <duration> | 30m | how long to wait for a free device or a slot in the boot queue, e.g. 10m |
--no-wait | fail fast instead of queueing | |
--priority <interactive|high|normal|low|N> | WARDEN_PRIORITY, else per command | boot queue priority; see Capacity & queue |
--ttl <duration> | 30m (bare claim by an agent: 5m) | lease TTL without heartbeat |
--label <label> | shown in warden ls | |
--adopt | allow allocating foreign (non-warden) devices | |
--headless / --no-headless | WARDEN_HEADLESS, else off | iOS: close the claimed sims' Simulator.app windows instead of tiling them; --no-headless overrides config and env. See Headless e2e |
warden capacity / warden queue
warden capacity [--json] prints the host budget (RAM, performance cores), what is booted (warden and foreign), per-device cost, the binding dimension and memory pressure. warden queue [--json] lists claims waiting for a boot slot: position, effective priority, age, owner. See Capacity & queue.
warden release [leaseIds...]
Release leases, selected by id, --udid <udid...>, --mine or --session <id>. --label <label> narrows --mine to leases with that label (what subagents should use). --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 |
--app | install the project's app on each device first |
--project <dir> | --app: project directory (default: cwd) |
--variant <name> | --app: install this projects[].variants build (dev, e2e…) |
--bundle-id <id> | --app: override the bundle id / package |
--variant <name> | --app: install this projects[].variants build (dev, e2e…) |
--no-eas / --no-build | --app: skip EAS downloads / local builds |
--install-mode <clean|reinstall> | --app: clean = fresh app data each run without reinstalling an unchanged build (falls back to reinstall if the reset fails); reinstall = uninstall + install each run. Default: install only when the build differs |
--reset-keychain | --install-mode: also reset the iOS simulator's keychain (not cleared by uninstall either) |
--clean | deprecated alias of --install-mode reinstall |
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}, {seq} and {port} (the first leased port) 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; {port} works here and in --serve (tcp:{port}) |
--serve-timeout <duration> | give up waiting for --serve-ready (default 10m) |
--job-timeout <duration> | a job still running after this is screenshotted, killed and failed (exit 124); off by default |
--record <dir> | iOS: record every simulator and the TUI (batch.json, tui.cast, dev-<i>.mp4) |
--logs <dir> | per-job logs (default: <record dir>/logs, else $WARDEN_HOME/batches/<id>/logs) |
--no-tui | plain log lines instead of the live grid |
With --headless (or the preset's headless), the leased sims' windows are closed at claim and again before the first job, serve and jobs get WARDEN_HEADLESS=1, and batch.json has "headless": true. --label also labels the port leases (default warden batch). With fewer jobs than --count, only one device per job is claimed.
Each batch.json job entry (one per attempt) may carry logDrainMs: how long warden waited, after the job exited, for its log pipes to drain, summed over its --passes runs.
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-fromreplace the preset'sjobs/jobsFrom.-- <cmd>replaces the preset'scmd; without it the preset'scmdruns.- Relative paths in the preset resolve against the preset's cwd (its
projectroot, else the config file's directory); paths passed on the command line resolve against your cwd. - Serve, jobs and a
jobsFrom.commandrun 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 specs 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 specs |
--explain | why each spec was picked, with the import chain |
--strict | exit 3 when a changed file reaches no spec |
--json | { suite, base?, changes, platforms: [{ platform, selected (each spec with its reasons), skipped, unreached, warnings }], warnings } |
Following entries through the import graph needs TypeScript 5.0 or later: the compiled binary uses the project's own, the nearest node_modules/typescript from the config's directory upward (Yarn Plug'n'Play is not supported); without one the run exits 1 naming that path. See Affected e2e.
warden e2e [suite]
Select like warden affected, then run the specs like warden batch — each spec as a bun test spec process (@delacour/warden-e2e, configured by the suite's spec) — and gate: exit 0 when every required spec passed, 1 otherwise, 130 when interrupted. Takes every batch flag except the job flags (--jobs, --jobs-from), plus the affected selection flags (--base, --platform, --files, --required-only, --explain) and:
With no e2e suite configured (and none named), the Expo app around the cwd gets a default suite: every *.e2e.ts spec under e2e/, one @delacour/warden-e2e spec process per device (isolate: "device") over a pool of 2 devices (--count), on iOS unless you pass --platform android, the dev client (the dev variant when the project has one) on a Metro warden starts, opened with the Expo dev menu off. No git diff is involved. warden init writes this suite into warden.config.ts.
--install-mode, --reset-keychain and --clean act on the installed app, so a suite that installs none (no app in its config) refuses them up front unless you also pass --app.
| Flag | |
|---|---|
--dry-run | select and explain, run nothing |
--all | run every spec of the suite (after include / exclude), no git diff |
--specs <list> | run exactly these spec ids / globs (comma-separated), no git diff; a list matching nothing fails |
--report <file> | also write e2e-report.json here |
--devices <legs> | which devices legs run: default (the default), all, or profile names (comma-separated) |
--job-timeout <duration> | overrides the suite's jobTimeoutMs (default 10 min) |
--no-shutdown | keep the devices running after the gate passes (the default locally; under CI they are shut down. Suite shutdown sets it, the flag wins) |
--perf[=captures] | profile every attempt: a perf session wraps each test and its report is copied to the attempt's perf/ dir (report.json, report.md, session.json). Bare --perf records metrics, frames and logs; --perf=react,js picks captures (js, react, native, metrics, frames, logs, or all). perf.budgets apply, and a breach fails the test with a PERF_BUDGET error (kind perf_budget in e2e-report.json); e2e-report.json links each test's perf dir |
--no-warm | skip the iOS driver-helper warm-up (the suite's warmDrivers: false) |
The suite's count, profile, retry, passes, app, ports, env, serve, serveReady and serveTimeout apply unless the flag is passed; project sets the cwd. With metro warden starts and verifies its own Metro before any spec and stops it last. With devices, every picked leg claims its own pool and runs the specs concurrently (--profile / --count replace the legs with one claim); the report then has one verdict per spec and leg. By default (isolate: "device") one spec process per device runs its share of the specs (round-robin by spec id) and every spec still gets its own verdict; --retry reruns only the specs that did not pass, and --job-timeout / jobTimeoutMs is per spec. With isolate: "spec" every spec is its own process.
While it runs, the grid (or, with --no-tui, one line per finished test, ✓ todos › adds a todo (3.21s) / ✗ …) ticks specs and tests off as the spec processes append to their results.jsonl. The grid's header bar has one cell per spec (one per job with isolate: "spec"): green passed, red failed, cyan running, dim pending; each device row has its own bar of the jobs it ran. With isolate: "device" every leased device gets a row, cyan while its spec process works; when the run ends warden reads each process's results.jsonl and fills the device's bar with the specs it ran there. Specs run in id order (sorted file path), so count: 1 is a stable serial run. headless (suite, else top-level) closes the leased sims' windows at claim and again before the first spec, unless --no-headless; e2e-report.json records "headless" and "isolate".
Nothing affected → exit 0 without claiming. Every failed spec is named at the end with its device, spec log and failure screenshot (also device, log, screenshot and a tests[] entry per test on the spec in e2e-report.json), then each failing test with the first line of its error and its artifact directory.
Before the specs, an iOS suite warms the leased devices' driver helpers (warmDrivers, on by default) so the first spec does not pay the cold start. Leases are always released at the end.
Under CI (CI set to anything but empty, 0 or false), or with shutdown: "always", a passed gate shuts down the devices warden created (or booted for the run) before releasing them; locally (shutdown: "idle", the default) they stay booted for the next run and warden gc shuts them down once idle. 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.
Device health: release --bad, sim unquarantine
After booting or reusing an iOS sim, warden claim checks that simctl spawn <udid> launchctl print system answers within 5 s and that SpringBoard is running. A sim this claim just booted (bootstatus -b saw the boot finish) only needs SpringBoard running. A device that fails is quarantined (recorded in the store with a reason starting claim:, shut down if warden created it) and the claim picks another one; after 2 retries it fails with a clear error. A probe that times out while the 1-min load average is at or above the core count is asked once more after 5 s, and a second timeout only skips the device for the next round: a busy host is not a hung sim. It is not quarantined, and if this claim booted it, it is shut down again. With --wait it is asked again on a later round; when it was the only candidate the claim fails with host overloaded (load X ≥ N cores); retry later. A claim's quarantine expires after 24 h, so the device gets another chance, but only while the pool has room under --max (a replacement may have taken its slot). A foreign device that fails is skipped, never touched. Android has no probe yet. warden switches Play Protect's adb-install verification off (and pre-answers its consent prompt) on every emulator it boots; the change lives in the emulator's throwaway overlay.
warden release <leaseId…> --bad [reason]releases and quarantines the devices by hand (warden devices only). Quarantined devices are never allocated and free their pool slot; a quarantine set by hand does not expire.warden sim unquarantine <udid|serial>clears it.warden devicesshowsquarantinedandslimnext to the warden column.- A claim that has to build the golden image prints
warden ... prewarm with: warden golden ensure --profile <profile>first. warden app ensureno longer trusts its install record alone: on iOS it also compares the installed app'sCFBundleVersion, executable size andmain.jsbundlesize with the cached build, and reinstalls on a difference.
warden sim slim [udid]
iOS. Switches off the launchd jobs inside a booted simulator that UI specs 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 spec 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 disabled inside the simulator (remembered across its reboots) and booted out so it stops now. Idempotent. Siri's assistantd and corespeechd are deliberately left running: the software keyboard asks them about dictation, and with them down an app that shows the keyboard spins its main thread until XCTest can no longer read it. A simulator slimmed by an older warden gets them re-enabled on the next slim or --restore (effective after its next boot). The disabled state is not copied by warden clone, so slim every device you use. A simulator leased by another session is refused. slim and --restore record the state per device; a plain warden claim re-enables a device left slimmed (jobs already booted out stay down until its next reboot). 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):
| 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 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.
| 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
macOS only. Lines up the open Simulator.app (Xcode 27: Device Hub) 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 and Device Hub 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 neither viewer is ever launched. Every iOS claim (claim, run, batch, e2e) runs this automatically unless it is headless; set WARDEN_ARRANGE=0 to turn that off. Moving windows uses System Events, so your terminal needs Accessibility access (System Settings → Privacy & Security → Accessibility). Xcode 27 replaced Simulator.app with Device Hub, which opens no window when a sim boots: a non-headless claim asks for one per sim it booted (only while Device Hub already runs), then tiles them. --open <udid…> does that for those sims on demand (the e2e session uses it after a HID-loss reboot). --json: "viewer" is device-hub / simulator / none, or { "opened": [...] } with --open. See Device Hub.
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 / Android snapshots new devices start from. All three take --platform ios|android (default ios). ensure [--profile] [--runtime] builds once or reuses (profile default iphone-17; Android: first AVD); ls lists them; prune deletes stale goldens (--all every golden, -y skips the prompt before --all). 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 devices — use warden sims prune to free sim disk. Then prunes the build cache, least-recently-used first, down to --max-cache-size (else WARDEN_MAX_CACHE_SIZE, default 20G; the same cap as warden builds prune --max-size); builds used in the last 30 min and builds being fetched or built stay, and the notes list what was pruned. A bad WARDEN_MAX_CACHE_SIZE only skips the prune, with a note; a bad --max-cache-size exits 1. Background auto-gc also runs when the cache is over that cap. --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 ls
List port leases.
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; --install-mode clean|reinstall resets the installed app first (clean: fresh data, no reinstall of an unchanged build; reinstall: uninstall + install), --reset-keychain also resets the iOS sim keychain, --clean is a deprecated alias of --install-mode reinstall; --no-eas, --no-build, --project, --bundle-id, --variant <name>. See App builds.
warden dev [platform] [-- <expo start args>]
The worktree-friendly expo run:*; the platform defaults to ios. Before leasing anything it checks that the project has a bundle id for the platform and lists expo-dev-client, and names the fix. One device only: --count other than 1 fails with warden dev runs on one device (drop --count). 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) and runs bunx expo start --dev-client --port <p> in the project root.
Metro counts as ready (--ready-timeout, default 2m) only once /status on that port is proven to serve this project root (the X-React-Native-Project-Root header, else the listening process's cwd). Another checkout's Metro on the port fails the command with port <p> is served by <dir>, not this worktree instead of being attached to.
Then the dev client is opened on the device: iOS relaunches the app with --initialUrl <metroUrl> (no "Open in …?" alert); Android runs adb reverse and opens exp+<slug>://expo-development-client/?url=… (--scheme to override). --no-open skips this. --json prints one line { udid, port, metroUrl, leaseIds } on stdout once Metro is ready (Metro's own output goes to stderr). The device and port stay leased (heartbeat every 30 s) until Metro exits; Ctrl-C stops Metro and releases what it leased. --no-eas, --no-build, --install-mode, --reset-keychain, --project, --bundle-id. --headless closes the claimed sim's window and gives Metro WARDEN_HEADLESS=1; dev is headless only with the flag (config headless and WARDEN_HEADLESS don't apply).
warden init
Set an Expo app up for warden dev / warden e2e, in the app's root (--project <dir>). Writes a starter warden.config.ts (the project and a dev-client e2e suite) and an @delacour/warden-e2e spec e2e/smoke.e2e.ts when the app has no specs. --bundle-id <id> fills in expo.ios.bundleIdentifier / expo.android.package in app.json when they are missing (required for an app with no id). Existing files are kept unless --force; a warden config in another format is never replaced. Missing packages (expo-dev-client, @delacour/warden-e2e pinned to this warden's version) are named, not installed. --dry-run, --json ({ root, written, skipped, install }).
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.--max-sizedefaults toWARDEN_MAX_CACHE_SIZE, else20G(the capwarden gc --max-cache-sizeuses).import <path.app|path.apk> --hash <h> [--platform] [--project] [--profile] [--bundle-id]— copy a local build into the cache.
Drive a device
warden ui <subcommand>
The agent CLI over the e2e driver: read the screen, act on it, wait. Full guide: Drive a device. Every subcommand takes the options below, and every call checks the device lease (exit 2 on a foreign or missing lease).
| Option | Effect |
|---|---|
--udid <udid> · --serial <serial> · --lease <id> | the device (else WARDEN_UDID, a single WARDEN_UDIDS, your only device lease) |
--claim | take a device when none is leased, or take over a stale lease; --platform ios|android, --profile <slug>, --runtime <runtime> pick it |
--timeout <ms> | per-command deadline (default 5000) |
| --force | drive the device even while a live warden process (warden e2e, batch, run) holds your lease on it; without it the call exits 2 (device_busy) naming the owner, pid and label. Your own warden dev device needs no --force: the call shares it ("attachedTo": "<leaseId>" in --json; close --release keeps that lease). Another owner's live lease still refuses |
| --no-record | while a record is active, do not append this call to it |
| --json | one JSON document on stdout: {ok, command, device, generation?, durationMs, result, warnings?} or {ok:false, command, error:{code, message, hint?}} |
Exit codes: 0 ok · 1 failed or usage (also the record errors no_recording, recording_active, target_exists, empty_recording, verify_failed, verify_unavailable) · 2 lease · 3 engine unavailable (engine_unavailable, devtools_unavailable) · 4 stale ref · 130 interrupted.
| Subcommand | Notes |
|---|---|
status | device, your lease and the helper |
open <bundleId|url> | --relaunch, --metro <url>, --args <args...>, --settle [ms]. Without --relaunch an app that is already running is attached to, not restarted: the output says attached to <id> and --json has attached: true |
close | stops this device's helper; --app [bundleId] quits the app, --release frees the lease |
snapshot | -i/--interactive interactive only, --all no line cap, --depth <n>, --settle [ms] quiet-screen diff |
find <selector...> | elements matching id=… label=… text=… role=… value=… editable=… nth=…, with refs |
press <target> (alias tap) | target = @eN | selector | x y; --verify, --settle [ms] |
fill <target> <text> · type <text> · key <name> | fill: --secure, --no-verify; type: --submit |
scroll <dir> [amount] | --until <selector> --in <target> --max <n> |
wait · is · get | poll the live screen: wait <selector>|absent <selector>|text "…"|stable [ms], is <selector> visible|hittable|enabled|checked|selected|focused (exit 0/1), get <selector> text|value|label|id|role|frame|count |
alert accept|dismiss|get · home · back | back is Android only. alert get with no alert shows an empty button list (exit 0); alert accept|dismiss then fail with no_alert (exit 1) |
screen rotate [orientation] · screen appearance [light|dark] | the device's orientation (portrait, portrait-upside-down, landscape-left, landscape-right) and light / dark mode; no argument reads the current value |
screen screenshot [path] · screen video start [path]|stop | default under $WARDEN_HOME/ui/<udid>/ |
gesture swipe <dir|x1 y1 x2 y2> · gesture longpress <target> [ms] | swipe --from <target>; longpress: --verify, --settle [ms] |
gesture pinch [target] <scale> · gesture rotate [target] <degrees> | two fingers around the target or the screen centre; --duration <ms> (default 500), --radius <px>, --verify, --settle [ms]; scale above 1 zooms in (0.1 to 10, not 1), positive degrees is clockwise (negative works; not 0, at most 720) |
gesture path --path 'x,y x,y …' [--path …] | 1-2 fingers (each path at least 2 points) that go down and lift together; --duration <ms> (default 600), --settle [ms]; coordinates in points (iOS) or px (Android). Two fingers on Android need warden-uiad |
record start <name> | --to <spec>, --helper <module#fn> --prerequisite <text>, --ids <module>, --options <module#name> (shared test.use options, or none), --title <title>, --suite <name>, --overwrite. Journals every later action; stop writes <specsDir>/<name>.e2e.ts (or the helper). Guide |
record use <module#fn> | --suite <name>. Runs the exported helper once on this device (a generated one-test spec through the spec process stop verifies with); on a pass appends a use step, which the spec calls as await fn(fixtures) imported relative to its path. Exit 1 helper_missing (module or export not found), use_failed (helper failed; details.evidence), verify_unavailable (no suite), no_recording; nothing is recorded on failure |
record note <text> · record undo [n] · record status · record cancel | note: a comment above the next action; undo: drop the last n steps (notes count); status: target, step count, last 5 steps, lints; cancel: drop the journal (a written spec stays) |
record stop | --no-verify, --suite <name>, --overwrite. Writes the spec or helper, merges ids, prints lints and the specs.<id> entry for warden.config.ts (--json: result.config { id, suite?, snippet, entries, routes, unresolved, captured, hints }, null for a helper; routes are read on dev clients only, need the suite's routerRoot, and capture stops after 3 failed reads), then runs the spec once on this device (helpers are not run). Exit 1 (verify_failed, verify_unavailable, target_exists) keeps the journal; --force is the busy-device option, not this one |
eval <expr> · tree · logs · network · reload | dev client only (--metro <url>): eval --await; tree --all --max <n> --screen <name>; logs [n]|start [--bodies]|stop|mark <label>|clear; network [n] (default 50) --include headers,body --id <id>; eval, tree, logs and reload take --metro <url> (default: WARDEN_METRO_URL, state.json, then this owner's leased ports) |
The old top-level rotate, appearance, screenshot, video, swipe and longpress are removed (unknown command, no aliases).
The old top-level rotate, appearance, screenshot, video, swipe and longpress are removed (unknown command, no aliases).
warden ui bench
warden ui bench [--runs <n>] [--metro <url>] [--udid|--serial|--lease] [--json] times the agent-loop primitives on a leased device and compares each with its target. --runs (default 20) is the sample count; the slow metrics are capped (cold start 3, settle and driver open 5). Run it on a device nobody else is using: the cold-start sample stops the device's helper first, and the press samples tap the middle of the screen.
| Metric | What is timed | Target (p50) |
|---|---|---|
helperColdToSnapshot | helper stopped, then connect (spawn) to the first snapshot -i (iOS) | < 1500 ms |
snapshotWarm | snapshot -i against the resident helper | iOS p50 < 60 ms, p95 < 120 ms; Android < 800 ms |
pressHid | a tap at the screen centre, HID only | iOS < 30 ms; Android < 120 ms |
pressSettle | tap, then --settle (wait for a quiet screen, diff) on a static screen (iOS) | < 600 ms |
tree | warden ui tree against the dev client (iOS); skipped: "devtools_unavailable" without one | < 250 ms |
openDriver | a second driver client opened on the resident helper socket, as a spec does (iOS) | < 500 ms |
A metric passes up to its target times 1.5 (ok in the JSON; a loaded machine shows up as slow, past the tolerance as FAIL). --json prints the usual warden ui envelope with result.metrics as { name: { p50, p95, samples, targetMs?, targetP95Ms?, ok? } } in milliseconds (or { skipped, samples: 0 }), plus result.passed. warden ui bench itself exits 0; bun run smoke:ui-bench (scripts/smoke/ui-bench.ts) claims a device, opens the example app, runs it and exits 1 when anything is over target x1.5.
Profile an app
warden perf
Profile an app on a warden simulator / emulator. Sessions live in $WARDEN_HOME/perf/<id>/ (session.json plus what each capturer writes); id is <yyyymmdd-hhmmss>-<hash6> in UTC. Simulator and emulator numbers run on host CPU and GPU: compare relative, not absolute. Dev builds (__DEV__) inflate JS and React costs. One-shot and start/stop subcommands (metrics, memory, frames, startup, js, heap, react, native, logs, crashes, tools) work on their own; start, stop and record run several of them as one session whose report merges them, and query and diff read what it left behind.
Device-bound subcommands take the warden ui options (--udid, --serial, --lease, --claim, --platform, --timeout, --json) plus:
| Option | Effect |
|---|---|
--app <id> | bundle id (iOS) / package (Android). Default: the foreground app (iOS: the one running non-Apple app), then the app in warden.config.ts |
--allow-mutate-foreign | let a mutating step act on a device someone else holds. Refused (device_busy) while a live warden process (e2e, batch, run, dev) holds the lease |
Which device: --udid\|--serial\|--lease, WARDEN_UDID, a single WARDEN_UDIDS, your only device lease, then the only booted device warden knows (created by warden, or leased). Several candidates exit 2 with ambiguous_device and list them (udid, name, holder, foreground app). A device warden does not know exits 2 with device_unknown: perf never attaches to foreign devices.
Observe-only work (metrics, attach-only traces, Metro / Hermes profiling, logs, crashes, frame reads) runs on any warden device and never takes or heartbeats a lease. Mutating work (kill / launch, heap dumps, gfxinfo reset) needs your own lease (--claim takes a free device) or --allow-mutate-foreign; otherwise it exits 2 with device_foreign / device_not_leased.
One active session per device, whoever holds the lease (session_active names the first). A session stops itself after 30 s (--max-duration, hard max 5 m, longer is clamped with a warning); a dead collector's or overdue session is swept the next time a session starts.
| Subcommand | Notes |
|---|---|
ls | recorded sessions, newest first (id, device, app, status running|stopped|aborted|overdue, age, cap, marks); --device <udid> filters, --devices lists the warden devices a session could attach to; --json |
mark <label...> | annotate the device's running session (session_not_found, exit 1, when none): commits, hangs and jank are tagged with the last mark before them |
doctor | read-only checks, one line each: xctrace (macOS; fails when missing), xcode (warns on 26.4+, where xctrace --device deadlocks), adb, device-tools (perfetto and simpleperf on an Android device), trace-processor (cached under $WARDEN_HOME/tools), metro, app (Android: debuggable or <profileable android:shell="true"/>). Exit 1 only on a fail; with no resolvable device the device checks are skip |
Errors carry {code, message, hint?}; the codes added for perf are device_unknown, session_active, session_not_found, app_not_found, unsupported, not_profileable (exit 1) and tool_missing (exit 3) (plus the warden ui lease codes). --json prints the warden ui envelope (command is perf <sub>); ls and doctor print their own document ({sessions} / {devices} / {ok, device?, checks}).
warden perf metrics
CPU % and memory of the app over time. Observe-only: works on any warden device and never takes a lease.
warden perf metrics [--interval 1s] [--duration 3s] [--watch] [--app <id>]- iOS simulator:
psmatched on the app's executable; CPU % is the delta of cumulative CPU time between samples (not the decaying%cpu), memory is RSS. - Android:
top -n 1 -b -p <pid>for CPU anddumpsys meminfo <package>for PSS and RSS. - The first sample is the CPU baseline. Output is min/avg/p95/max per metric plus the memory trend; a steadily rising series (at least 6 samples over 5s, +1 MB, +50 KB/s) adds a
memory-growthleak hint. - Samples go to the running session's
metrics/samples.jsonl(timed from the session start), else a one-shot directory under$WARDEN_HOME/perf. --watchredraws CPU, memory (and fps when available) as unicode sparklines with current/avg/max on stderr. Piped, or with--json, it prints one NDJSON line per sample on stdout, then the result envelope. Ctrl-C prints the summary and exits 130. Without--durationit stops after 5 minutes.--intervalis at least 200ms. Numbers from simulators and emulators are for comparing relative changes, not absolute values.
warden perf memory
Where the app's memory sits right now, or a heap snapshot.
warden perf memory [--snapshot] [--app <id>]- Without
--snapshot(observe-only): Android prints TOTAL PSS/RSS and the top 5dumpsys meminfocategories; iOS prints the physical footprint (and peak) and top regions fromvmmap --summary. - With
--snapshot(pauses the app; needs your own lease, so pass--claim):- iOS:
leaks --outputGraphwrites a.memgraph(open it in Xcode's Memory Graph debugger) and the command reports the leak count, bytes and leaked types. - Android:
am dumpheapwrites an.hprof, which is pulled and removed from the device. It is raw only; open it in Android Studio (Profiler, Load). The app must be debuggable or profileable.
- iOS:
- Artifacts go to the running session's
native/directory, else a one-shot directory under$WARDEN_HOME/perf.
warden perf frames
fps, janky %, dropped and frozen frames and the worst jank windows for the app on an Android device,
read from dumpsys gfxinfo <pkg> framestats.
warden perf frames [--app <pkg>] [--reset] [--json]- Reports
fpsandrefreshHz. The frame deadline is the median vsync gap, clamped to 4-50ms. - Reports
jankPct(the OS "Janky frames" summary since the last reset),droppedFrames,frozenFrames(frames over 700ms) andworstFrameMs. worstWindows: the 3 worst jank clusters. Clusters split on gaps over 500ms. Each window hasstartOffsetMs,endOffsetMs,missedDeadlineFrameCountandworstFrameMs.- With a running session (
warden perf start), offsets are measured from the session start and each window carries the lastwarden perf markbefore it. The report is also written to the session'sframes.json. - A
jankfinding is emitted at 2% or more janky frames, or any frozen frame. - A plain read is observe-only. It takes no lease and changes nothing on the device.
--resetclears the OS frame counters after reading, so the next read is a fresh window. This mutates the device and needs your own lease (--claimor a lease you hold).- Window offsets come from the OS's last ~120 frames (about 2s at 60Hz). The jank % and totals cover
everything since the last reset. For a long scenario, run
perf startwith marks and read at the end of the interaction. - iOS simulator: returns
status: "unsupported"with a hint. The simulator has no frame API. Use hangs fromwarden perf start --nativeinstead.
warden perf startup
Cold or warm app start time, median and p90 over several launches.
warden perf startup [--cold] [--warm] [--runs <n>] [--method approx|xctrace] [--app <id>] [--claim]Kills and relaunches the app, so it needs your own device lease (or --claim on a free device). Without a lease it fails with device_foreign and never touches the app.
| Flag | Meaning |
|---|---|
--cold | Stop the app before each start (default). |
--warm | Send the app to the background first, then start it. With both flags, cold runs first. |
--runs <n> | Launches to time, 1-20 (default 5). |
--method <m> | iOS only: approx (default) or xctrace (App Launch template, cold only, spike). |
- Android:
am start -W(-Sfor cold). Time isTotalTime, the time to the first frame. If the app callsreportFullyDrawn(), the fully-drawn time is reported too. - iOS simulator:
approxis the wall time ofsimctl launch --terminate-running-process. It is a lower bound (time to process spawn); compare relative, not absolute.xctracerecords the App Launch template and reads the launch periods from the trace. - React Native: when Metro is reachable, the app's
performancemarks are read after the last launch and listed. A missing Metro is noted instartup.json, not an error. - Output: median, p90, min and max per kind. A
startupfinding is raised when the cold median is over 2s (warn) or 5s (critical). The full result is written tometrics/startup.jsonin the active session, or in a one-shot dir under$WARDEN_HOME/perf/.
warden perf js
Hermes JS CPU profile of the running dev client, source-mapped to file:line hotspots. Observe only: it never leases the device.
warden perf js [--duration 10s] [--sampling-interval 100] [--metro <url>] [--app <id>] [--json]- Samples for
--duration(default 10s, max 5m) at--sampling-intervalmicroseconds (default 100). - Writes
js/cpu.cpuprofile(opens in Chrome DevTools or speedscope) andjs/hotspots.jsoninto the running session, otherwise a one-shot directory under$WARDEN_HOME/perf. - Source maps are fetched from Metro only (64MiB cap). The report lists top self and total time per function with
file:line, groupsnode_modulesframes by package, reports idle share separately, and raisesjs-hotspotfindings for functions above 10% of active time. - Dev builds (
__DEV__) get a banner: unminified JS and the DevTools hook inflate costs, so confirm on a release or profileable build. - A JS reload during the capture loses the profile. Release builds have no Metro, so the command exits 3 (
devtools_unavailable). - If a running session already profiles JS, the command fails with
session_active.
warden perf heap
Hermes heap snapshots of the running dev client.
warden perf heap snapshot [--metro <url>] [--app <id>] [--claim] [--json]
warden perf heap diff <before> <after> [--json]snapshotfreezes the JS thread while it runs, so it needs your lease on the device (or--claim). It streamsjs/heap-<ts>.heapsnapshot(opens in Chrome DevTools, Memory tab) and prints the top 15 constructors by self size and by count. Retained sizes are not computed; open the file for them.diffcompares two snapshot files: per-constructor count and self-size deltas, largest change first. It only reads files.- If the runtime lacks the needed Profiler or HeapProfiler method, the command exits 3 with
unsupported: ...and a hint.
warden perf logs
The app's device logs, secrets redacted. An observe op: needs no lease, and works on any warden device.
warden perf logs --app com.acme.shop --level W --since 5m
warden perf logs --follow --json --grep "api|cart"| Flag | Meaning |
|---|---|
-f, --follow | Stream live: text lines, or one JSON entry per line with --json. Ctrl-C exits 130. |
--level <V|D|I|W|E|F> | Minimum level (words like error, warn work too). |
--grep <regex> | Keep lines whose tag or message matches (case-insensitive). |
--since <duration> | Only lines from the last 30s, 5m, 1h. |
-n, --lines <n> | Lines to show, newest last (default 200). |
- Android:
logcat -v threadtime --pid <pid>. While following, the pid is re-resolved every 2s and the stream restarts when the app does.--sinceis measured against the device clock. - iOS simulator:
log stream|show --style compactwith a predicate matching the app's subsystem, sub-subsystems and executable. A one-shot read looks back 5m unless--sinceis given. - Redaction:
Authorization/Bearertokens,api_key=, JWTs,password=and emails become[redacted]before anything is printed or written. - In a session: the collector writes the same redacted stream to
logs/device.log.
warden perf crashes
Crashes and ANRs of the app, newest first (up to 10), with crash / anr findings. An observe op.
warden perf crashes --since 1h
warden perf crashes --symbolicate --dsym build/ShopApp.app.dSYM| Flag | Meaning |
|---|---|
--since <duration> | Only crashes from the last 30s, 5m, 1h. |
--symbolicate | iOS only: resolve app frames through a dSYM (needs --dsym). |
--dsym <path> | The .dSYM (or DWARF file) of the build that crashed. |
- iOS: reads
~/Library/Logs/DiagnosticReports/*.ipsfor this app on this simulator. Each crash shows exception type, signal, crashed thread, top 10 frames and the first app frame.--symbolicatematches the dSYM bydwarfdump --uuid, then runsatoswith the image's load address; a dSYM from another build is reported as a warning and the crash is still listed. - Android:
logcat -b crash -dplusdumpsys dropbox --print data_app_crashanddata_app_anr, de-duplicated. Native tombstones are not read. - In a session: raw reports are copied into the session's
crashes/directory.
warden perf react
React render profile of the running dev client (observe-only; never takes the lease): which commits were hot, which components cost the most, and ranked findings with a fix.
warden perf react [--duration 10s] # record, then analyse
warden perf react start # begin and return (profile lives in the app)
warden perf react stop # end, analyse, write| Flag | Meaning |
|---|---|
--duration <time> | how long to record (default 10s, max 5m) |
--metro <url> | Metro to attach through (default: the one serving the device) |
--take-over | take over (or stop) a React profile another agent started |
Output goes to react/commits.json (every commit: durations, per-fiber actual/self time, change descriptions) and react/findings.json (hot commits, ranked components, findings) in the running session, else a one-shot dir under $WARDEN_HOME/perf.
- Hot commits: under 16ms is fine, 16-50ms warm, over 50ms hot; heat is the cube root of duration over the slowest commit.
- Findings (
kind):slow-commit,slow-render,memo-defeated(memoized but props change identity),context-cascade,parent-cascade,render-storm,eager-mount,effect-chain. Each hascostMs,evidence,fix,next. Components under 2ms or 3% of render time are not reported; animation and transition components are tagged and not blamed. - Where: components are located by the runtime's
_debugSourcewhen it exposes one, else by grepping the project forfunction Name/const Name =/class Name. - JS correlation: with a
js/cpu.cpuprofilefrom the same session (perf start --js --react), each hot commit lists the JS functions that ran inside it. - Errors:
rn_too_old(React Native below 0.74),release_build(no DevTools hook or__DEV__false),devtools_backend_missing(noreact-devtools-corein the bundle),session_active(another agent holds the profile;--take-over).
Dev builds inflate React costs: confirm on a profileable build. The simulator banner applies.
warden perf native
warden perf native [start|stop] [--duration 10s] [--leaks] [--analyze <trace>] [--skip-analysis] [--flavor perfetto|simpleperf] [--yes]Native CPU, hangs and (iOS) leaks from the platform profiler. iOS simulators use xcrun xctrace record with the Time Profiler template plus the Hangs instrument; --leaks adds Leaks and Allocations. The trace lands in native/ios.trace of the device's running perf session, or of a one-shot dir under $WARDEN_HOME/perf/native-<id>/. With no action it records for --duration (default 10s, max 5m), stops and analyzes; start begins a detached recording, stop ends it (SIGINT, waits for the trace to be finalised, SIGKILL only on timeout) and exports it.
-
Attach-only recording is observe-only: it works on any warden device and never takes or heartbeats a lease. On Xcode 26.4+ (where
xctrace record --devicedeadlocks) it records--all-processesand filters the rows by the app's pid. -
--leaksis a mutation: it terminates the app and cold-launches it withMallocStackLogging=1under the profiler, so it needs your own lease (--claim). -
The analysis names the dominant frame of each sample (first app or third-party frame, else a React Native frame such as
RCT*, Yoga, Hermes, JSI; system-only samples are skipped), groups threads (Main Thread, JS Thread, Shadow Thread) and reports frames over 3% of samples (over 15% is critical). Each hang lists its duration, the top 5 functions and top 3 call chains on the main thread during it. Leaks are grouped by type and responsible frame with bytes and counts. -
--analyze <trace>re-analyzes an existing.tracewithout a device.--skip-analysisonstopleaves the export for later.--yes(Android) downloadstrace_processorwithout asking. -
Simulator numbers are for comparing runs, not absolutes. Open the trace in Instruments for the full picture. Android:
-
Default is a Perfetto trace of the app: 100 Hz callstack sampling scoped to the package, scheduler/binder ftrace, atrace (
view gfx wm am input dalvik+ the app), SurfaceFlinger frame timeline and 1 s process stats. The config goes toperfettoon stdin; the trace is pulled tonative/android.perfetto-traceand removed from the device. The trace stops at--max-durationat the latest. -
--flavor simpleperf(in a session:perf start --native=simpleperf) samples the running app pid withsimpleperf record -e cpu-clock:uand reports the top functions from the on-devicesimpleperf report(native/simpleperf.data,native/simpleperf.data.report.txt). It needs no trace_processor. -
Analysis runs PerfettoSQL through a pinned
trace_processor_shell($WARDEN_HOME/tools/trace_processor_shell-<version>, orWARDEN_TRACE_PROCESSOR): CPU hotspots (critical over 15% of samples, warn 3-15%), missed-deadline frames with the main-thread state split (running / runnable / sleeping / io) and GC overlap, per-thread time, RSS growth. Raw SQL is written tonative/queries/. -
The trace is always kept: if trace_processor is missing the analysis is skipped with a warning naming the install command.
-
Zero samples fail with
not_profileable: the app must be debuggable or<profileable android:shell="true"/>in its manifest. -
Observe-only: attaching never leases or restarts the device or app.
warden perf start | stop | record
warden perf start [--js] [--react] [--native [flavor]] [--metrics] [--frames] [--logs] [--all]
[--relaunch] [--reset] [--max-duration 30s] [--yes] [--join]
warden perf stop [--no-analyze] [--yes]
warden perf record [--duration 10s] [same capture flags as start] [--no-analyze]One combined session. start sweeps overdue sessions, opens the session, writes plan.json, records a clock anchor ({wallMs, deviceWallMs}; Android reads adb shell date +%s%N and halves the round trip) and spawns one detached warden perf-collect --session <id> that hosts metrics, logs, the JS and React capture (one Hermes socket) and the auto-stop timer. With no capture flag you get metrics, frames and logs; --all adds JS, React and native.
--relaunchterminates the app, arms what can be armed before launch (Android perfetto), launches, waits for the Metro target and then arms JS and React. iOS native is armed right after launch (it needs the pid).--resetclears app data first and implies--relaunch. Both mutate the device, so they need your lease; a free warden device gets a shortwarden perflease thatstopreleases. A live foreign lease fails withdevice_foreign.--native [flavor]isperfetto(default) orsimpleperfon Android; give the flavor attached,--native=simpleperf, so it is not read as a positional argument.--max-durationis capped; at the cap the collector runs the same stop path asperf stop.--joinattaches read-only to a running session.stopends the capturers in reverse order, gives back a leasestarttook and writesreport.md+report.jsonunder the session dir.--no-analyzekeeps native traces unanalyzed.recordisstart, wait--duration(default 10s, max 5m),stop.--no-analyzestops without building the report. Ctrl-C stops early, still reports and exits 130.warden releaseandwarden ui close --releasestop a running session on the released device.
warden perf report
warden perf report [id|latest] [--full] [--rebuild] [--json]Prints the merged report: Summary, Startup, Frames, JS CPU, React, Native CPU, Hangs, Memory, Logs/Crashes and next: hints. Findings are sorted by severity then cost; overlaps are correlated (hang 412ms @ 3.2s overlaps commit #14 (FlatList, 180ms) <- mark 'scroll feed'). --json is report.json (PerfReportJson). Output is capped; --full lifts the cap, --rebuild regenerates from the session files.
warden perf query
warden perf query <id> commits [--by component|window|index|cascade] [--size 1s] [--index N] [--limit N]
warden perf query <id> commits --by index <n>
warden perf query <id> cpu [--top | --tree | --window 2s-5s] [--limit N]
warden perf query <id> stacks hang | callers <fn> | thread | leaks [--rerun --yes] [--limit N]
warden perf query <id> frames [--worst]<id> is a session id, a unique prefix or latest. Reads the session's files; no device needed.
| Topic | Options |
|---|---|
commits | --by component (default) | window | index | cascade. --by window takes --size <time> (bucket, default 1s). --by index takes the commit as a positional <n> or --index <n>: one commit in depth |
cpu | --top (the hottest functions, default), --tree (call tree), --window <a-b> (self time between two points on the session timeline, e.g. 2s-5s) |
stacks | hang, callers <fn>, thread, leaks; --rerun re-runs the Perfetto SQL (Android), --yes downloads trace_processor for it |
frames | --worst: the worst jank windows and hangs |
--limit <n> caps the rows of commits and cpu (cpu defaults to 15). --json prints the warden ui envelope.
warden perf diff
warden perf diff <a> <b> [--fail-on regress] [--threshold 10%] [--json]Reads two sessions' reports, so it takes no device options.
Compares two reports metric by metric (lower is better, except fps) with a relative threshold plus absolute floors, and lists function self-time deltas. With --fail-on regress exits 1 when any metric regressed.
warden perf tools
warden perf tools install trace-processor [--yes] [--json]Downloads the pinned Perfetto trace_processor_shell (about 14 MB; sha256-verified, installed atomically) into $WARDEN_HOME/tools/. On a terminal it asks first; without a terminal it fails with tool_missing (exit 3) unless --yes is given. A progress bar (bytes, percent, MB/s) is drawn on stderr only, so --json stdout stays one document. Re-running reports already installed.
Setup
warden install
Install warden to ~/.local/bin plus hooks for detected agents (default: every detected agent). --claude, --codex, -y/--yes (apply agent-config changes without asking), --dry-run (show diffs, write nothing), --shim (running from source: link ~/.local/bin/warden to this checkout), --no-completion (skip tab completion), --json (diffs go to stderr).
warden completion [zsh|bash|fish]
Print the tab-completion script for a shell (default: $SHELL). warden install sets it up for $SHELL: zsh / bash get $WARDEN_HOME/completion/warden.<shell> sourced from .zshrc / .bashrc (one line, diff + confirm), fish gets ~/.config/fish/completions/warden.fish. By hand: source <(warden completion zsh) after compinit.
Every tab asks warden __complete -- <words…>, which walks the same command tree as --help: subcommands at every depth, options after -, .choices() and a | b help hints as values (warden claim <tab> → android ios), files for free-form arguments. zsh: tab once lists with descriptions, tab twice picks the first, further tabs cycle (menu select, unless you set your own menu style). fish cycles by default. bash cycles only with readline's bind 'TAB:menu-complete', bind 'set show-all-if-ambiguous on' and bind 'set menu-complete-display-prefix on', which warden leaves to you.
warden skill show | install
show [--file <name>] [--json] prints SKILL.md, or one file of the skill folder such as references/ui.md. warden install --claude stages the embedded skill in $WARDEN_HOME/skill/warden/ and installs it with the same skills add command.
install runs bunx skills add (npx fallback): --from local|github (default local: this binary's embedded copy; github is the updatable release repo), --project (default: global), -a/--agent <agent...>, --copy (copy instead of symlink), -y, --dry-run (print the command, run nothing), --json.
warden hook session-end
The SessionEnd hook entrypoint: shuts down what the session booted and releases its leases. Called by Claude Code / Codex, not by hand. There is no PreToolUse hook any more: warden install removes the PreToolUse hook an older warden wrote, and a hidden warden hook pretool no-op keeps a PreToolUse hook written by an older warden from erroring.
warden doctor
Check home, db, device tools, the iOS simulator helper, install method (and its upgrade command), PATH, Claude/Codex hooks + skill, rule, and stale leases. --fetch-helper first downloads warden-simd for this version into $WARDEN_HOME/simd/<version>/. See iOS driver helper.
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).