# Set up your agent (/docs/agent-setup) Your coding agent needs the warden skill to know the claim, drive, release rules. Run `warden install`, or paste the prompt below into the agent. ```bash warden install # skill + rule + SessionEnd hook for every detected agent; shows a diff, asks first ``` ## Paste this into your agent [#paste-this-into-your-agent] ```text Set up warden for mobile device work in this repo. 1. Run `npm i -g @delacour/warden && warden install` (or `bunx @delacour/warden install`). Approve the diff. 2. Run `warden doctor` and fix anything it reports. 3. Run `warden skill show` and read it. Follow its rules from now on: claim a device with `warden claim ios --json`, pass the returned udid on every call, drive it with `warden ui`, never touch a device you did not claim, and release with `warden ui close --release`. 4. In an Expo app, run `warden init --bundle-id ` and install the packages it names. ``` ## What gets installed [#what-gets-installed] | Piece | Where | Agents | | --------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Skill | `skills/warden/`: `SKILL.md` + `references/{ui,specs,suites,troubleshooting}.md`, embedded in the binary | anything the [`skills`](https://github.com/vercel-labs/skills) CLI installs into | | Rule | `~/.claude/rules/warden.md`, written directly | Claude Code | | SessionEnd hook | `warden hook session-end` | Claude Code, Codex | The skill loads into Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Windsurf, Zed and the other agents the `skills` CLI supports. Hooks exist only for Claude Code and Codex. Agents are detected from `~/.claude` / `claude` on `PATH` and `$CODEX_HOME` (`~/.codex`) / `codex` on `PATH`; with none found, only the binary is installed. The skill is placed by the `skills` CLI, which may prompt (`--yes` is passed through). ## Per agent [#per-agent] ```bash warden install --claude # Claude Code only: skill (via `skills add`) + rule + SessionEnd hook warden install --codex # Codex only: hooks in $CODEX_HOME/hooks.json warden skill install # the skill, into any agent the skills CLI offers ``` Every agent-config change shows a diff and asks first. `--yes` skips the prompt, `--dry-run` only prints, and a non-TTY without `--yes` skips. `warden install --claude` installs the skill, the rule and the SessionEnd hook. It also removes the `PreToolUse` hook (`warden hook pretool`) that older versions installed. Details: [Agent rules](/docs/agents/rules#claude-code). `warden install --codex` merges the SessionEnd hook into `$CODEX_HOME/hooks.json`. Run `/hooks` in Codex once to trust it. Codex does not set a session id, so set `WARDEN_SESSION_ID`, or leases count as your shell's user leases. Details: [Agent rules](/docs/agents/rules#codex). ```bash warden skill install # global, this binary's copy warden skill install --from github # delacournz/warden, updatable via `skills update` warden skill install --project -a cursor -y # into the current project, one agent warden skill show | pbcopy # raw SKILL.md to paste by hand ``` There is no session hook for these agents. Their leases expire by TTL (5 min for a bare claim) and `warden gc` reclaims them, so tell the agent to run `warden ui close --release` when it is done. ## MCP (optional) [#mcp-optional] The skill drives everything through the `warden` CLI. For an agent that prefers MCP tools, `warden mcp` serves the debugging side of a running `warden dev` session (Metro output, the app's console, exceptions, network, eval, logpoints, profiles) over stdio: ```bash claude mcp add warden -- warden mcp ``` See [Debug the running app](/docs/agents/debug) for the tools. ## Verify [#verify] ```bash warden doctor ``` `doctor` checks hooks, the skill and the rule among other things. Then ask the agent to run `warden claim ios --json` and `warden ui snapshot -i`. # Debug the running app (/docs/agents/debug) `warden dev` keeps two records of the app it runs, and agents can read both: | Source | What only it has | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Metro's output** | bundle and transform failures (syntax errors, unresolved imports) with file, line and code frame, HMR rebuilds. The app never got that bundle, so nothing reaches the app's console. | | **The app over CDP** | `console.*` calls, uncaught exceptions, `fetch` calls, evaluating JavaScript, the React tree, the route, logpoints, CPU / heap / React profiles | ## The devtools broker [#the-devtools-broker] A `warden dev` session can hold one Chrome DevTools Protocol connection to the app through Metro's inspector proxy: the **broker**. It is off by default, so a session runs only what the agent loop needs (build, Metro, simulator, app). Turn it on when you start debugging: ```bash warden dev enable devtools # the running session; or start with: warden dev ios --devtools warden dev status # : hud off · devtools on warden dev disable devtools # frees the socket again ``` Once on, it attaches to the dev client, keeps the newest 5 000 console lines and 1 000 requests in memory, and writes them to `console.jsonl` / `network.jsonl` in the session directory (`$WARDEN_HOME/dev//`). The dashboard's `devtools` row shows its state. `warden ui` devtools verbs and `warden mcp` read from it, so nothing that happened after it attached is missed and no tool opens a second debugger connection. Without a broker they connect directly; logpoints and the `profile_*` tools need it. Stacks are symbolicated through Metro: a console line or exception points at `src/app/welcome.tsx:21`, not a bundle offset, with Metro's code frame when it has one. React Native's own frames are kept but flagged `collapsed`. ## From the CLI [#from-the-cli] ```bash warden dev logs --kind bundle_failed,error # does it build? (Metro's output, classified) warden dev logs --since 5m --follow # stream it warden ui exceptions # uncaught errors, symbolicated warden ui logs --level warn --since 2m # console, warnings and up warden ui network --status 4xx # failed requests warden ui eval 'useStore.getState().todos.length' warden ui tree --max 40 # components on screen, with testIDs warden ui route # (tabs)/todos warden ui reload ``` Every command takes `--udid` (default: the only live dev session) and `--json`. `warden dev logs` reads Metro's terminal output, which only the dashboard records. A `--json` or `--no-tui` run answers `metro_log_unavailable`. ## Logpoints [#logpoints] A logpoint logs a value every time a source line runs, without pausing the app and without editing code. It is a conditional breakpoint whose condition logs and returns false. ```bash warden ui logpoint add src/app/welcome.tsx:21 '({ step, title: current.title })' # reproduce the bug, then warden ui logpoint hits # each hit's value, e.g. {step: 0, title: "Welcome"} warden ui logpoint add src/api.ts:88 'res.status' --hits 5 # removed after 5 hits warden ui logpoint list warden ui logpoint clear ``` * The location is a path suffix inside the bundle and a 1-based line, optionally `:col`. The bundle's source map places it; a line with no code moves to the next one that has some, and `list` shows where it landed. * Logpoints are re-set after a reload or a reconnect, and removed when the dev session ends. * They need the Fusebox debugger (React Native 0.76 and later). On a legacy target `add` fails with `logpoints_unsupported`. ## Profiles [#profiles] ```bash warden perf js --duration 10s # through the broker when warden dev runs ``` Over MCP, `profile_start` / `profile_stop` record `cpu` (sampling profile: top self-time functions), `heap` (allocation sampling: top allocation sites) or `react` (slowest commits and components). Artifacts are written under the session's `profiles/` directory. A JS reload ends a recording. ## MCP [#mcp] `warden mcp` is an MCP server on stdio with the same functions as the CLI: ```bash claude mcp add warden -- warden mcp ``` | Tool | Does | | ---------------------------------------------------------------------- | ------------------------------------------------------- | | `dev_sessions` | live dev sessions: device, Metro URL, session directory | | `metro_logs` | Metro's output, classified and filtered | | `app_console` · `app_exceptions` · `app_network` | what the broker captured | | `app_eval` · `app_tree` · `app_route` · `app_reload` | read or reload the running app | | `logpoint_add` · `logpoint_hits` · `logpoint_list` · `logpoint_remove` | logpoints | | `profile_start` · `profile_stop` · `profile_status` | CPU, heap and React profiles | | `devtools_status` · `devtools_resume` | broker state | Each tool takes an optional `udid`; with one dev session running it is not needed. ## Chrome DevTools at the same time [#chrome-devtools-at-the-same-time] An interactive `warden dev` lets several debuggers attach, so the broker and Chrome DevTools (`j` in Expo) work side by side. A headless Expo run allows one: opening DevTools disconnects the broker, which then waits (`warden ui devtools status` shows `yielded`) instead of taking the page back. Devtools calls answer `devtools_unavailable` until `warden ui devtools resume`, which disconnects DevTools in turn. # warden dev (/docs/agents/dev) `warden dev` replaces `expo run:ios` in a worktree: it claims a device, installs the dev build cached for your native fingerprint, starts Metro on a leased port and opens the dev client. Use it whenever you or your agent need the app running. ```bash warden dev # ios by default warden dev android # an emulator warden dev ios -- --clear # extra args go to `expo start` warden dev ios --json # one line { udid, port, metroUrl, leaseIds } once Metro is ready ``` ## What it does [#what-it-does] 1. Finds the app and checks what a dev-client run needs (see [Before it starts](#before-it-starts)). 2. Claims a device and leases a Metro port (default range `8081:100`). 3. Shows the simulator (iOS, macOS): brings its window to the front, launching Device Hub (Xcode 27) or Simulator.app (Xcode ≤ 26) when no viewer is running. `--headless` skips this. Android emulators stay windowless. 4. Installs the build for this fingerprint: installed, cache, EAS, then local build. 5. Starts Metro on the leased port. 6. Opens the dev client once the port is verified to serve this checkout. `warden dev` owns its Metro. A Metro left running by another worktree is refused, never attached to. It holds the device and the port until Metro exits; `q` or Ctrl-C in the dashboard (Ctrl-C in plain output), a kill or the terminal closing (SIGHUP) stops Metro and releases both. In the dashboard's interact mode Ctrl-C goes to Expo, which exits and ends the session. The simulator dev booted is shut down with the session and its Device Hub / Simulator.app window is closed, so nothing is left running or showing; one that was already booted when dev leased it, or one you passed with `--udid` / `--lease`, is left on. `--no-shutdown` keeps dev's own sim booted for the next run (a boot is 10-30 s). A hard kill (`kill -9`, a crashed terminal app) skips all of this; `warden gc` shuts the sim down once its lease has gone stale. The `--json` line is printed first, so an agent can read the `udid` and `metroUrl` and pass them to `warden ui open --metro `. Metro gets `WARDEN_WORKTREE` (the checkout directory name) and `WARDEN_BRANCH` (absent on a detached HEAD) in its environment, so `app.config.ts` can name the dev server after the worktree. Native builds never see them, so the fingerprint is unchanged. ## Before it starts [#before-it-starts] Everything that can be wrong with the request or the app is found on the plain terminal, before a device is leased, anything is built or the dashboard takes the screen. A failure there is one line with the fix, never a dashboard that flashes and closes. * **No config needed.** `warden dev` runs with or without a `warden.config.ts`. Started at a monorepo root (nothing found upwards), it looks below it: the package.json `workspaces`, else `apps/*` and `packages/*`. One app there is used and named (`app apps/mobile (wallet)`). With several, a person picks one, and a pipe gets the list and `--project `. * **Expo or bare React Native.** Read from the app's package.json: `expo` means `expo start --dev-client` and the dev client. Only `react-native` means a bare app: the bundle ids come from `ios/*.xcodeproj` (`PRODUCT_BUNDLE_IDENTIFIER`) and `android/app/build.gradle` (`applicationId`), Metro is `react-native start` on the leased port, the default local build is `react-native build-ios` / `build-android`, and the app itself is relaunched against Metro (iOS: `-RCT_jsLocation 127.0.0.1:`; Android: the device's 8081 reversed to the port). `projects[].framework` in a `warden.config.ts` overrides the guess. `warden e2e` stays Expo-only. * **The project.** The nearest `warden.config.ts`, else the app's `app.json`. An app whose config is code (`app.config.ts` / `.js` / `.mjs` / `.cjs`) with no bundle id in `app.json` is resolved the way Expo does it: `warden dev` runs `expo config --json --type public` in the app directory once and takes the bundle id, slug and scheme from the result. `--bundle-id` still overrides it, and a `warden.config.ts` skips the call. * **`expo-dev-client`.** Metro is started with `--dev-client`, so an Expo app has to list it. A bare app needs none. With a person at the terminal each missing piece is offered as a fix: * a package that is not in `package.json`: *Add and install it now?* runs `expo install ` through the package manager the checkout uses (`yarn`, `pnpm`, `bunx` or `npx`, from the nearest lockfile) in the app directory, then checks again; * an app read through `expo config`: *Write warden.config.ts?* writes a `warden.config.ts` next to the app with the project name and the resolved bundle id, so later starts skip the call (`warden init` writes the same file plus a first e2e spec). In a checkout that formats with prettier the file is run through the checkout's prettier, so its format check stays green. Declining a blocking fix ends the run with the command to run yourself. Without a terminal (`--json`, a pipe, CI) nothing is asked: the blocking issues are the error, each with its command, and the optional ones a note. When a failure happens after the dashboard opened (the claim, the build, Metro dying), its lines are printed to the terminal once the dashboard has closed, followed by the session log directory, so the reason is never lost with the screen. ## Dashboard [#dashboard] On an interactive terminal `warden dev` draws a dashboard instead of printing lines. Tasks are listed on the left: claim device, show simulator, dev build, metro, dev client, session. Every key you press adds a row of its own (a screenshot, a reinstall). The right side shows the live output of the selected row. The metro row shows Metro's real terminal, so Expo's QR code and key menu look the same as outside warden. When the terminal is at least 100 columns wide a HUD box sits at the top right. ```bash warden dev # dashboard on a terminal warden dev --no-tui # plain output, Metro on this terminal ``` ### Two modes [#two-modes] * **List mode** is the default. Warden's keys apply. * **Interact mode** starts with `Enter` or `i` on the metro row. Every key, Ctrl-C included, goes to Expo until you press `Esc`. Expo's own menu keeps its exact bindings (`a`, `i`, `w`, `j`, `r`, `m`, `shift+m`, `o`, `?`). Exiting Expo ends the session the normal way. * `Esc Esc` in quick succession sends a literal Esc to Expo. * `Shift+PgUp` / `Shift+PgDn` scroll Metro's history. ### Keys [#keys] List mode: | Key | What it does | | --------------- | ----------------------------------------------------------------------------- | | `↑` `↓` `j` `k` | select a task | | `Enter` `i` | interact with Metro (see above) | | `Tab` | focus the log pane | | `r` | reload the app: sends `r` to Metro | | `d` | show the simulator window (iOS; a note on Android, which has none) | | `o` | open the dev client again | | `s` | screenshot, saved under the session's `shots/` directory | | `R` | reinstall the dev build, then reopen the client. Metro keeps running | | `x` | restart the simulator and reopen the client (iOS only) | | `c` | restart Metro with `--clear` on the same port | | `l` | start or stop streaming device logs into a row | | `p` | start a perf session on this device, or stop the one recording | | `P` | start a perf session recording every capture | | `m` | add a perf mark: asks for a label in the footer (`Enter` adds, `Esc` cancels) | | `f` | frame stats (Android; the iOS simulator has no frame API) | | `g` | show the report of the last perf session | | `h` | show or hide the HUD | | `?` | key help | | `q` `Ctrl-C` | quit: stops Metro and releases the device and port | | `Ctrl-L` | redraw | Log pane (after `Tab`): `↑` `↓` scroll a line, `PgUp` `PgDn` a page, `g` `G` jump to the top or bottom, `Tab` or `Esc` go back to the list, `q` quits. Help overlay: `Esc`, `?`, `q` or `Enter` close it. Guard rails: * Keys wait for the dev client: before it opens they report `not ready` in their row. * `R`, `x` and `c` run one at a time, and a key already running reports `still running`. * One perf session records per device. `R` and `x` are refused while one records, so the app is not killed under the recording; `p` stops it first. `p` while another process holds a session on the device reports that and names `warden perf stop`. * `g` is refused while the session still records. A session that reaches its cap stops itself and `g` shows its report. * `m` is refused when nothing records. * `x` is iOS only. ### HUD [#hud] The HUD box samples the dev client every second: | | iOS simulator | Android emulator | | ---------------------------- | ------------- | ---------------- | | CPU % with a short history | yes | yes | | Memory | RSS | PSS | | Frames per second and jank % | no frame API | yes | It also shows Metro's last bundle time and module count, and a recording clock with the mark count while a perf session records. It is off by default and samples nothing until turned on: `h` toggles it (the sampler starts and stops with the box), `--hud` starts with it on, `warden dev enable perf` / `disable perf` flips it from another shell, and `--hud-interval ` sets the sampling interval (default `1s`, minimum `200ms`; a smaller value is an error). The box is hidden below 100 columns. ### Session logs [#session-logs] Everything the dashboard shows is also written to `$WARDEN_HOME/dev//`: | File | Holds | | ------------ | ---------------------------------------------------------------------- | | `warden.log` | every task row's lines, one per line with a timestamp and the row name | | `metro.log` | Metro's raw terminal output | | `shots/` | screenshots from `s` | `warden gc` removes a session directory once its lease is gone and its newest file is older than 24 hours. ### When the dashboard is off [#when-the-dashboard-is-off] `warden dev` prints plain lines, with Metro on your terminal, when any of these holds: * `--json`, or the session is not a plain interactive terminal (pipes, CI). * `--no-tui` or `WARDEN_TUI=0`. A terminal under 60 columns or 16 rows shows `terminal too small` until it is resized; the session keeps running. Agents should keep running `warden dev ios --json`: the dashboard is for people and never changes the `--json` output. ## Why not `expo run:*` [#why-not-expo-run] `expo run:ios` rebuilds natively in every worktree: Xcode's DerivedData is keyed by the checkout's path, so a fresh worktree compiles every pod again even when the native code is identical. warden caches the built app by native fingerprint, shared by every worktree of the repo. * Same fingerprint as any earlier build (any worktree, EAS or local): no prebuild, no Xcode, seconds to a running app. * A native change: one local build, cached for every other worktree. * Two worktrees missing the same hash at once share one build (build lock). ## Options [#options] | Flag | Effect | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--profile ` · `--runtime ` | device profile (`iphone-17`, `pixel-10`) and runtime (`latest`, `26.5`) | | `--udid ` · `--lease ` | use this device instead of claiming one (also the default when this session already holds one device lease). A held sim that is shut down is booted and waited for first, but only under this session's own lease; a foreign one is never booted | | `--port ` | Metro port range to lease from (default `8081:100`) | | `--project ` · `--bundle-id ` | project directory (default cwd); override the bundle id / package | | `--variant ` | `projects[].variants` build (default `dev` when the project has one) | | `--scheme ` | dev-client URL scheme (default `exp+`) | | `--ready-timeout ` | wait this long for Metro (default `2m`) | | `--no-eas` · `--no-build` | skip EAS downloads; do not build locally on a cache miss | | `--install-mode clean\|reinstall` | `clean`: fresh app data each run without reinstalling an unchanged build; `reinstall`: uninstall + install each run. `--reset-keychain` also resets the simulator keychain | | `--no-open` | start Metro and take the leases only | | `--no-tui` | plain output: Metro on this terminal, no dashboard (also `WARDEN_TUI=0`; `--json` and pipes are always plain) | | `--hud` · `--hud-interval ` | dashboard only: start with the perf HUD on (default off); HUD sampling interval (default `1s`, minimum `200ms`) | | `--devtools` | start the CDP devtools broker with the session (default off; `warden dev enable devtools` later) | | `--no-shutdown` | leave the simulator booted when dev ends (default: shut down the one dev booted) | | `--wait ` · `--no-wait` · `--priority

` | boot-queue behaviour (default `--wait 30m`) | | `--headless` · `--no-headless` | `--headless`: don't show the simulator and close its Simulator.app windows; the default shows it | | `--count ` · `--max ` · `--ttl ` · `--label

--skill warden -g -a claude-code -y`, `npx` fallback; `--dry-run` prints the command). The CLI places it at `~/.agents/skills/warden` and symlinks `~/.claude/skills/warden`. Without bun or node the step is skipped; use `warden skill show`. The rule `~/.claude/rules/warden.md` is written directly. A leftover `~/.claude/rules/agent-device.md` or `argent.md` from the retired setup is reported by `warden install` but never deleted. ## Codex [#codex] `warden install --codex` merges the SessionEnd hook into `$CODEX_HOME/hooks.json` (default `~/.codex/hooks.json`). Hooks already declared in `config.toml` `[[hooks.*]]` tables count as installed. Codex hooks use Claude's format and stdin JSON, so `warden hook session-end` serves both agents unchanged: ```json title="~/.codex/hooks.json" { "hooks": { "SessionEnd": [{ "hooks": [{ "type": "command", "command": "$HOME/.local/bin/warden hook session-end", "timeout": 3 }] }] } } ``` Codex **skips new or changed hooks until you trust them**: run `/hooks` in Codex once after installing. * Codex caps SessionEnd hooks at 3 s and always sends reason `other`, so the `/clear` exception never applies. Anything SessionEnd can't finish in time is picked up by `warden gc` once the leases go stale. * For the skill in Codex, use `warden skill install` (pick the agent with `-a`). `warden skill install --from github` installs the updatable copy from the release repo instead of this binary's embedded one. * Codex sessions have no session id of their own: set `WARDEN_SESSION_ID`, or leases count as your shell's user leases. # warden ui (/docs/agents/ui) `warden ui` is how your agent looks at a device and drives it: read the screen as text, tap, type, scroll, wait, read the app's console and React tree. Use it to explore, reproduce a bug, verify a change, or get the selectors for a spec. ```bash warden claim ios --json # lease a sim, keep the udid warden ui open com.example.app --udid $UDID # launch warden ui snapshot -i --udid $UDID # the screen as @eN lines warden ui press @e3 --settle --udid $UDID # act, print what changed warden ui close --release --udid $UDID # stop the helper, release the lease ``` It runs on the same driver as [`@delacour/warden-e2e`](/docs/e2e/specs) (see [iOS driver helper](/docs/reference/e2e-driver)), so there is nothing to install on the device and no daemon to manage. For checks that must keep passing, write a [spec](/docs/e2e/specs). Every call is stateless on the command line: the last snapshot and the Metro URL live under `$WARDEN_HOME/ui//`. ## The agent loop [#the-agent-loop] ```bash warden claim ios --json # 1. a device: use the returned udid on every call warden ui open com.example.app --udid $UDID # 2. launch (add --metro for a dev client; --kind simulator installs the repacked sim build first, no Metro) warden ui snapshot -i --udid $UDID # 3. what is on screen, as @eN lines warden ui press @e3 --settle --udid $UDID # 4. act; --settle prints what changed warden ui wait id=todos-screen --udid $UDID # 5. verify with wait / is / get, not a screenshot warden ui close --release --udid $UDID # 6. stop the helper and release the lease ``` 1. **Claim.** `warden claim ios --json` (or `android`); keep the returned `udid` (Android: the serial). Pass it on every call as `--udid ` (`--serial` / `--lease ` also work), or export `WARDEN_UDID` once. With none of those, `warden ui` uses the single device you already lease. 2. **Open.** A bundle id launches the app; a URL opens through the system. For a dev client, run [`warden dev`](/docs/agents/dev) or `warden app ensure` first, then `open --metro ` (iOS; on Android see [Android](#android)). 3. **Snapshot.** `snapshot -i` lists only interactive elements and anything with a `testID`. The `@eN` refs it prints are valid until the next `snapshot`, `find` or `--settle`. 4. **Act on a ref or a selector.** Prefer `id=` selectors for anything you will touch twice: refs go stale when the screen changes. 5. **Verify** with `wait`, `is` and `get`. A bare screenshot is not verification. 6. **Close.** `close --release` stops this device's helper and releases the lease. Leave the device alone only if another step of your task needs it. ## Record a session as a spec [#record-a-session-as-a-spec] Between `warden ui record start ` and `record stop`, every action and query is journaled (`--no-record` skips one call) and `stop` writes a [spec](/docs/e2e/specs) and runs it once on the same device. While recording, `snapshot -i` marks interactive elements with no testID `[no-id]`, and `status` shows the recording. `record use ` runs a helper on the device and records a call to it. Full workflow, route capture, lints and helpers: [Author specs by recording](/docs/e2e/author-specs). ## Reading the screen [#reading-the-screen] ```text $ warden ui snapshot -i --udid $UDID generation 4 · axbridge · 393×852@3x · 5 of 41 elements @e1 [textbox] "Add a todo" id=todo-input value="" enabled hittable (24,118 345×48) @e2 [button] "Add" id=todo-add enabled hittable (24,176 345×48) @e3 [checkbox] "Write specs" id=todo-1 unchecked enabled hittable (24,244 345×56) ``` A line is `@ref [role] "name" id= value="…" (x,y w×h)`. States are `enabled`/`disabled`, `hittable`, `checked`/`unchecked`, `selected`, `focused`, `secure`, `hidden`, `expanded`/`collapsed`; the frame is `(x,y w×h)` in the screen units the header reports. The header says how many elements are shown and whether the backend truncated the tree. | Command | Use | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `snapshot [-i\|--interactive] [--all] [--depth n]` | the screen. `-i` filters to interactive elements and `testID`s; the default caps at 200 lines (`--all` removes the cap); `--depth n` keeps the first `n` levels | | `snapshot --settle [ms]` | wait for a quiet screen (the tree hash is unchanged for 250 ms; default budget 3000 ms), then print what changed against the previous snapshot as `-` / `+` lines and a `= N unchanged` count | | `find ` | only the elements matching a [selector](#targets-and-selectors), each with a fresh `@eN` | | `screen screenshot [path]` | a PNG; default `$WARDEN_HOME/ui//shots/NNN.png` | | `screen video start [path]` · `screen video stop` | record the screen (iOS `simctl recordVideo`, Android `screenrecord`, 3 min device limit); default `$WARDEN_HOME/ui//videos/NNN.mp4` | `snapshot` and `find` bump the **generation** (`generation 4` above). So does anything else that takes a snapshot for you: an action with `--settle`, a `wait` that matches, a `scroll --until` that finds its target. The refs you held before are replaced. ## Targets and selectors [#targets-and-selectors] Actions take a **target**: an `@eN` ref, a selector, or screen coordinates `x y` (points). Coordinates are for canvas content with no accessibility node; everything else should be a selector. **Refs.** `@eN` comes from the latest snapshot of this device. A ref older than 30 s, or any ref with `--verify`, is re-checked against a fresh snapshot first (same id, else same role and name, else same position in the tree). When the element is no longer there the call fails with `stale_ref` (exit 4): take a new snapshot. **Selectors** are `key=value` words joined by spaces: | Key | Matches | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `id=` | the `testID` / accessibility identifier, exactly | | `label=` | the accessibility label, case-insensitive substring | | `text=` | the visible text, case-insensitive substring | | `value=` | the element's value (field content, switch state), case-insensitive substring | | `role=` | the normalized role (`button`, `textbox`, `checkbox`, `switch`, `text`, `heading`, …), the same names on iOS and Android | | `editable=true\|false` | text fields vs everything else | | `nth=` | the i-th match (0-based) when several remain | A value is bare (`id=sign-in`), quoted when it has spaces (`label="Sign in"`), or a regex (`text=/^\d+ left$/i`). Keys combine with AND: `role=button label=add`. When a selector matches several elements, an action picks the most specific visible one: visible first, then the most exact text terms, then the smallest frame, then reading order. Pin a different one with `nth=`. `find` lists every match, and `get count` counts them; `wait`, `is` and `get` use the same most-specific pick as an action. ## Acting [#acting] Every action accepts `--settle [ms]`: wait for a quiet screen afterwards (default budget 3000 ms) and print what changed, so the next ref is already on your screen. | Command | Effect | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `open [--relaunch] [--metro ] [--args …]` | launch the app, or open a URL (on iOS the "Open in …?" sheet is accepted). A bundle id that is already running is attached to, not restarted (`attached to `, `--json` `attached: true`); `--relaunch` starts it fresh. `--metro` starts the dev client on that Metro and remembers it for the devtools commands | | `press ` (alias `tap`) | tap. `--verify` re-checks an `@eN` first | | `fill ` | focus the field, empty it, put the text in, then read it back (retried once). The text is typed key by key; `--via paste` pastes it on iOS (one Cmd+V) instead. Xcode syncs the simulator pasteboard with your Mac clipboard in both directions, so a pasted value shows up in your clipboard history and on every other booted simulator. A paste that cannot be confirmed is typed, and the retry always types. `--secure` skips the read-back and never echoes the text; `--no-verify` skips only the read-back. Use `--` before text that starts with `-` | | `type [--submit]` | type into whatever has focus; `--submit` then presses Enter | | `key ` | `Enter` `Backspace` `Tab` `Space` `Escape` `Delete` `Up` `Down` `Left` `Right` | | `scroll [amount]` | scroll the content that way (`down` reveals what is below). `amount` is the share of the region one swipe travels (0-1, default 0.5). `--in ` scrolls inside an element; `--until ` swipes until the selector is on screen (at most `--max` swipes, default 10) and prints its fresh ref | | `gesture swipe [--from ]` | a raw finger swipe: `gesture swipe up` moves the finger up, which reveals what is below. `--from` swipes inside an element | | `gesture longpress [ms]` | press and hold (default 800 ms). `--verify` re-checks an `@eN` first | | `gesture pinch [target] ` | two fingers around an element, a point or the screen centre; `scale` above 1 zooms in, below 1 zooms out; 0.1 to 10, not 1. `--duration` (default 500 ms), `--radius` (finger distance from the centre, a positive number) | | `gesture rotate [target] ` | two fingers turn around the target or the screen centre; positive is clockwise, negative works: `gesture rotate @e3 -45`; not 0, at most 720 either way. `--duration` (default 500 ms), `--radius` | | `gesture path --path 'x,y x,y …' [--path …] [--duration]` | trace 1-2 finger paths (each at least 2 points, on the screen; points split on spaces or `;`); the fingers go down and lift together. `--duration` defaults to 600 ms. Coordinates are points on iOS and px on Android, the same as `x y` targets | | `alert accept\|dismiss\|get` | answer the system alert or sheet on top, or read its title, message and buttons. With no alert showing, `get` succeeds with an empty `buttons` list (`--json`: `present: false`) and `accept` / `dismiss` fail with `no_alert` | | `home` · `back` | the home button; back is Android only | | `screen rotate [orientation]` | `portrait`, `portrait-upside-down`, `landscape-left`, `landscape-right`; without an argument it reads the current one | | `screen appearance [light\|dark]` | set or read light / dark mode | ### `screen` vs `gesture` [#screen-vs-gesture] `screen …` acts on the device's screen: orientation, appearance, screenshots, video. `gesture …` is touch. The same verb can name either job: `screen rotate landscape-left` turns the device, `gesture rotate @e3 45` turns two fingers around an element. This replaced the old top-level `rotate`, `appearance`, `screenshot`, `video`, `swipe` and `longpress`, which are removed (no aliases; they fail as unknown commands). `pinch` and `rotate` take the amount last. The target before it is optional: an `@eN` ref, a selector, `x y`, or nothing for the screen centre. `pinch`, `rotate` and `path` accept `--settle`; `pinch` and `rotate` also `--verify`. A scale or degrees out of range, a lone coordinate, or a bad `--path` is a `usage` error (exit 1) with the syntax as the hint; a device that cannot do the gesture fails `failed` with `details.driver: E_UNSUPPORTED` (exit 1), and nothing is sent. Both platforms cap a gesture at 2 fingers, which go down and lift together (the iOS simulator's HID event carries one phase for both contacts). ## Waiting and asserting [#waiting-and-asserting] These poll the live screen every 100 ms for the first second, then every 250 ms (`wait stable` keeps 100 ms), for up to `--timeout` (default 5000 ms) and never need a screenshot. | Command | Result | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `wait ` | until a match is on screen; prints it with a fresh `@eN` (the snapshot it matched in becomes the new generation) | | `wait absent ` | until no match is visible | | `wait text "…"` | until that text is visible | | `wait stable [ms]` | until the screen has stopped changing for that long (default 250 ms) | | `is visible\|hittable\|enabled\|checked\|selected\|focused` | prints `true` / `false`; exit 0 when true, 1 when false or nothing matches: usable in a shell `if` (`--json`: `{ matched, state, element?, waitedMs, reason? }`) | | `get text\|value\|label\|id\|role\|frame\|count` | print one bare value (`frame` as `x y w h`; `count`: how many match, `0` is a success); no match is `target_not_found` | A `wait` that runs out fails (exit 1) with `wait_target_absent` (never in the tree), `wait_target_hidden` (in the tree, not visible), `wait_target_present` (`absent`: still visible), `wait_text_absent` or `wait_not_stable`. `--json` failures carry `details: { matched: false, waitedMs, reason }`. Into an active spec, `wait ` (as a visible check), `is` and `get` (a text property) are recorded; `wait absent|text|stable` are not. ## The app's console, network and React tree [#the-apps-console-network-and-react-tree] For a dev client launched with `open --metro `. A release build, or a dev client with no Metro, fails with `devtools_unavailable` (exit 3). `--metro ` overrides the Metro for one call; otherwise warden uses `WARDEN_METRO_URL`, the URL `open --metro` remembered, then the Metro behind the ports your session leases. | Command | Effect | | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `eval [--await]` | run JavaScript in the app (Hermes) and print the value; an exception is reported as `failed` with its details | | `tree [--all] [--max n] [--screen ]` | the React components with `testID`, label, text, screen and rect. Use it for a `testID` the accessibility tree does not show; `--all` includes inactive screens | | `logs start [--bodies]` | start a background collector that records the app's console and `fetch` calls (`--bodies` also records bodies) | | `logs [n]` · `logs mark