Generated banner for Eas Simulator
Generated logo for Eas Simulator

Eas Simulator

ExpoOfficialPublished by Expo

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk

From the author

Run and control a user's app on a remote iOS/Android simulator hosted on EAS cloud. Read before running any `eas simulator:*` commands - it has the current syntax for this experimental API. Use whenever the user needs a simulator they can't run locally - 'run my app on a cloud simulator', 'use eas simulator to run/install/screenshot my app', 'I'm on Linux/Cursor and need an iOS device', 'no sim on this box / headless CI', 'let an agent click through my app and screenshot it', 'test my dev build on a remote sim with live reload', 'stream a sim to my browser' - even when they don't say 'EAS Simulator' or 'cloud'. On a host WITHOUT a local simulator (Linux, CI, cloud sandbox) it's the default; on macOS, do NOT auto-trigger for a plain 'run on the simulator' - use it only for a cloud/remote/shareable sim, an iOS version they lack, or an agent-driven session. NOT for local sims (expo run:ios, Xcode, Android Studio), EAS Build/Update, web preview, or physical devices.
— the description by Expo

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

EAS Simulator provides a way to run and interact with iOS and Android simulators in the cloud, making it ideal for environments that cannot support local simulators, such as Linux, CI/CD pipelines, or headless cloud agents. It allows developers to install apps, perform automated interactions, and capture screenshots or recordings. The skill integrates with controllers like agent-device to drive the simulator, enabling workflows like automated testing, visual regression, and live debugging through tunnels.

What it does

  • Boots remote iOS and Android simulators on EAS cloud infrastructure.
  • Supports automated app interaction via controllers like agent-device.
  • Enables live debugging and hot-reloading via tunnels for development builds.
  • Captures screenshots and screen recordings for visual verification.
  • Provides access to device logs and crash reports for debugging.
  • Offers a browser-based preview for manual inspection.

When to use it

  • When you need to test an app on a simulator but lack local hardware (e.g., Linux, CI).
  • When you need an agent to automatically click through an app and verify behavior.
  • When you need to perform visual regression testing with screenshots or recordings.
  • When you need to debug a development build on a remote device with live reload.

Advantages

  • Enables simulator testing on non-macOS environments.
  • Integrates seamlessly into automated agentic workflows.
  • Supports live development loops with hot-reloading.
  • Provides centralized access to device logs and crash reports.

Usage examples

“Run my app on a cloud simulator.”
The agent initiates an EAS Simulator session, installs the app, and provides a web preview URL for the user to interact with the device.
“Use eas simulator to screenshot my app's login flow.”
The agent starts a session, installs the app, uses agent-device to navigate to the login screen, and saves a screenshot to the local project directory.
“I'm on Linux and need to test my iOS app.”
The agent verifies EAS Simulator availability, starts an iOS session, and sets up the environment for testing the app remotely.
“Let an agent click through my app and verify the checkout flow.”
The agent starts a session, installs the app, and executes a series of automated press and navigation commands to validate the checkout process.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/expo/eas-simulator/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip eas-simulator.zip -d .agents/skills

Documentation by Expo

EAS Simulator

EAS service - costs apply. Sessions count toward your account's pricing and limits and stop at their maximum duration. Stop a session with eas simulator:stop when the task is done. See https://expo.dev/pricing for current terms.

EAS Simulator runs a remote iOS simulator or Android emulator on EAS infrastructure that you drive from your machine — from the CLI, from an AI agent (via agent-device), and from a browser preview. It's the unlock for environments that can't run a simulator locally (Linux boxes, cloud/background agents like Cursor Cloud), and for letting an agent verify a change on a real device instead of only reasoning about code.

The simulator:* commands are experimental and hidden, and need a recent eas-cli (≥ 20.3.0 as of writing) — which is why this skill runs everything via npx --yes eas-cli@latest. Flags and verbs may change; the relevant subcommand's --help output is authoritative.

When to use

The frontmatter description carries the trigger phrases. In short: use this to get a user's app onto a cloud simulator and interact with it — especially from a Mac-less or cloud/sandbox agent. Not for local sims (expo run:ios, Xcode, Android Studio), store builds/signing (that's EAS Build), or physical devices. For the macOS case, see Cloud vs local next.

Cloud vs local: decide this first

  • Explicit cloud/remote/shareable request: use EAS Simulator after checking access, on any host.

  • Generic simulator request: use a suitable local simulator when available. If the host cannot run the requested simulator (for example, iOS on Linux or a cloud sandbox), use EAS Simulator after checking access. A non-macOS host may still support a local Android emulator.

  • Honor an explicit local choice; hand off to expo run:ios / Xcode / Android Studio as appropriate. Clarify only when the requested environment remains ambiguous and affects the task.

When the user asks for EAS Simulator or a cloud simulator, start the session and do the requested work without asking again. Stop the session when the work is done. Only check in if the user set a limit you would go past, or if the work grows beyond what they asked for.

Prerequisites

  • Run every eas command via npx --yes eas-cli@latest … — guarantees a CLI new enough to have simulator:* (a global eas is often too old), and --yes skips npx's prompt. (Bare eas is fine if eas --version is current.)

  • Authenticated. Interactive machine → npx --yes eas-cli@latest login. Cloud sandbox / CI / headless agent has no browser login — set EXPO_TOKEN (expo.dev → Account → Access Tokens) in the env instead. Verify either way with npx --yes eas-cli@latest whoami.

  • Run from an Expo project directory. A fresh app needs one-time setup: npx --yes eas-cli@latest init to create/link the project (when there's no projectId), and set ios.bundleIdentifier in app config if it's missing — a fresh create-expo-app often has none, and prebuild/eas build need it (they prompt or fail without it; e.g. dev.<owner>.<slug>). Read current config with npx expo config --json (it may live in app.config.js). The first Mode-C run is slow (native build); later runs reuse it.

  • A controller to drive the device. This skill uses agent-device (open source, MIT), run on demand via npx agent-device@latest — nothing globally installed. Appium and argent are alternative automation interfaces; web-preview-only has no automation interface. See references/controllers.md.

  • .env.eas-simulator is written/managed by eas-cli (not this skill): it holds the session id (EAS_SIMULATOR_SESSION_ID) + the daemon URL/token, so get/stop/exec default to that session (usually omit --id; pass --id <id> to target another). It carries a token → keep it gitignored (eas-cli marks it "do not commit" but may not add the ignore rule, and a fresh app's .gitignore won't cover it — add .env.eas-simulator if missing).

  • The command blocks assume a POSIX shell (bash/zsh) — printf, lsof, $(seq …) loops won't run in cmd/PowerShell. On Windows, run them in WSL or Git Bash, or translate as you go (the eas-cli/agent-device invocations themselves are cross-platform).

Session lifetime

  • --max-duration-minutes N is the hard automatic-stop deadline. Any plan can set it, up to the plan limit (40 minutes on Free, 115 on paid plans). The timer starts when the session is ready.

  • --max-idle-time-minutes N stops a session after that many inactive minutes. Omitted means no idle timeout: the session runs until its maximum duration or an explicit stop.

  • Controller activity resets the idle timer (agent-device, argent, and Appium commands, which are recorded as session events with producer appium). Browser-preview activity does not reset it. For a user-driven browser preview, rely on the maximum duration, not idle time, to bound the session.

Check availability first

EAS Simulator is a limited-access EAS feature that is still rolling out, so it isn't enabled on every account. Check access before starting a session; this read-only command does not create a session.

npx --yes eas-cli@latest simulator:availability --json
# → {"available": true, ...}  enabled → continue to the core loop
# → {"available": false, ...} not enabled → do NOT start a session

If it's not available, don't call simulator:start (it will fail). Instead, hand off gracefully so you keep making progress without this skill:

  • Tell the user EAS Simulator isn't available on their account yet — it's coming soon.

  • Fall back to their normal local path for the actual goal — expo run:ios / Xcode / Android Studio for a local sim/emulator, an EAS Build, or whatever else fits. Don't dead-end on the cloud sim; the request was almost never "use EAS Simulator specifically."

(If simulator:availability isn't recognized, the CLI is too old — upgrade, or treat a not enabled for this account error from simulator:start the same way: stop and fall back.)

The core loop (always the same)

A session is: start → (install your app) → drive → stop. eas-cli owns the session; the device verbs (open/tap/screenshot) come from the controller, which npx --yes eas-cli@latest simulator:exec runs for you with the session's connection env loaded.

# 1. Start a session (boots the remote sim + agent-device daemon; writes .env.eas-simulator).
# If the dotenv names a session, inspect it with simulator:get --json first. Reuse it when it
# belongs to this run; stop it only when it is in scope and no longer needed. An IN_PROGRESS
# session may be intentionally concurrent, so preserve its id/config before resetting the dotenv.
# Continue below only after choosing how to handle that existing session.
printf '# managed by eas-cli\n' > .env.eas-simulator   # clear only after resolving any live session
npx --yes eas-cli@latest simulator:start --platform ios --type agent-device --non-interactive \
  --name "Checkout flow screenshots"   # always name it — see 'Always name the session'
#    Then confirm it's live: simulator:get --json → status IN_PROGRESS (bounded poll in run-your-app.md).

# 2. Drive it through `exec` (loads the session env, then runs the command you give it).
#    agent-device runs on demand via npx — nothing installed globally.
npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <app-or-url> --platform ios
npx --yes eas-cli@latest simulator:exec npx agent-device@latest snapshot -i          # interactive UI tree → @e1, @e2 refs
npx --yes eas-cli@latest simulator:exec npx agent-device@latest press @e2            # tap a ref (use 'press' or 'click')
npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./shot.png

# 3. Stop the session and reset the dotenv. Omit --id to target the dotenv session.
npx --yes eas-cli@latest simulator:stop
printf '# managed by eas-cli\n' > .env.eas-simulator

To watch it live, hand the user the webPreviewUrl that start prints (in --json, remoteConfig.webPreviewUrl; for web-preview-only sessions, remoteConfig.previewUrl). All current session types include a browser preview, on iOS and Android; agent-device, appium, and argent also provide automation, while web-preview-only provides no automation interface. On Android, the expo.dev session page does not show the preview inline yet: Open preview opens it in a new tab. Android support is in development, so some iOS features may not work there. This URL is for the user's browser — you cannot open it for them, and it must never touch the sim:

  • "Open it here" (Cursor/VS Code) → print the URL on its own line and tell the user to open Simple Browser (Cmd/Ctrl+Shift+P → "Simple Browser: Show") and paste it. Then stop: do not shell out to a system browser or a Cursor/VS Code URL handler, and do not ask "did a tab appear?" — you can't confirm it, the handoff is done.

  • Never open the webPreviewUrl on the sim. It's a browser preview, not a deep link and not an agent-device open argument; routing it to the device renders a browser-in-a-browser (a real past failure).

  • Headless agent (no display) → just return the URL as the deliverable.

  • Keeping it alive for the user to drive → use --max-duration-minutes N (up to the plan limit). Browser-preview activity does not reset --max-idle-time-minutes, so idle timeout is not a reliable lifetime bound for this case. Tell the user when the session expires, using the CLI's reported duration or expiry. Keep it running for the requested preview; stop sessions created for one-shot tasks when the task finishes.

start also prints a job-run URL.

Always name the session

Pass --name "<description>" on every simulator:start. The name appears in simulator:list, simulator:get, and on the Simulator sessions page on expo.dev, where it replaces the generic title on each row. Unnamed, every row reads "Simulator session" over a random id — a wall of identical entries nobody can navigate. Write the name for a human scanning that list days later, not for yourself during this run.

Write what the session is for, in a few plain words:

--name "Checkout flow screenshots"     # what you did
--name "Dev build — dark mode fix"     # what you were testing
--name "Login repro for issue 412"     # why it exists

Rules:

  • Derive it from the user's request, not from the mode or the tooling. Mode C session, agent-device ios, and test say nothing.

  • Length: aim for 3–6 words, ~40 characters, and treat 50 as the practical limit. It renders as a single-line title in a narrow table column, so a long name clips. The API accepts up to 255 characters and rejects an empty/whitespace-only name, but 255 is a ceiling you never approach, not a target. One noun phrase, no sentences.

  • Be specific within that budget. Include a ticket or PR number when there is one.

  • Sentence case: capitalize the first word only, and leave identifiers in their real casing (Dev build for expo-router v4, Repro for EXPO-1234). It's a row title, so no Title Case, no all-lowercase, and no trailing period.

  • Don't repeat what the table already shows. Every row already displays the session id, platform, start time, duration, and who created it — so no ids, no iOS, no dates, no your-own-name. Spend the whole budget on what those columns can't say: the purpose.

  • If the user names it, use their name as-is.

  • Sessions are per-run, so name each new one for that run. Don't reuse an old name for different work.

--name is newer than simulator:start itself, so an older installed eas-cli can reject it. If that happens, run via npx --yes eas-cli@latest or upgrade; as a last resort, retry once without --name (the session starts unnamed). See references/troubleshooting.md.

Commands at a glance

Query the installed CLI for the complete current flag set before using non-default start flags, machine-readable/config output, list filters, or session events:

# Replace `start` with the simulator subcommand you are about to run.
npx --yes eas-cli@latest simulator:start --help

The examples below cover the common workflow; they are intentionally not an exhaustive copy of the CLI surface. Keep non-obvious behavioral guidance from this skill—especially Session lifetime—even when constructing the command from --help.

CommandPurpose
npx --yes eas-cli@latest simulator:availability [--json] [--non-interactive]Check access without creating a session.
npx --yes eas-cli@latest simulator:start --platform ios|android --name "<description>" [flags]Create a session; boot the sim + selected interface; write .env.eas-simulator by default; print the preview + job-run URLs. Always pass --name. --json does not suppress the dotenv; use --out-config-type env when no file should be written (it prints only the controller export lines, without EAS_SIMULATOR_SESSION_ID, so pass --id to get/stop/events).
npx --yes eas-cli@latest simulator:exec <cmd> [args…]Load .env.eas-simulator, then run <cmd> with that env. The bridge to the controller.
npx --yes eas-cli@latest simulator:get [--id <id>] [--json] [--non-interactive]Session status + connection details, including the session name. Use this to confirm readiness (see Operating principles).
npx --yes eas-cli@latest simulator:list [filters] [--limit N] [--after <cursor>] [--json]List and paginate project sessions; filter by status, type, platform, name prefix, and tags.
npx --yes eas-cli@latest simulator:events [--id <id>] [--follow|--json]Show recorded activity events; --follow watches until the session ends.
npx --yes eas-cli@latest simulator:stop [--id <id>] [--json] [--non-interactive]Stop a session (idempotent).

Running the user's app — pick a mode

The remote sim boots blank — no Expo Go, no apps. Install a build, then drive it — but match the build type to the goal first (the box below); that's where live-session runs derail. Full sequences: references/run-your-app.md — read before running a mode.

Match the build to the goal before installing anything — this is where live-session runs derail. Two traps, same root (grabbing a build that doesn't fit the request):

  1. Wrong type. Live edits (Mode C) require a dev build. A static build — a local Release (A), the default EAS sim build (B), or any build left on the sim from an earlier screenshot run — freezes its JS at build time and can never hot-reload. For a live request, ignore existing builds entirely and install a dev build (local Debug, or an EAS build with developmentClient: true). Never reconnect Metro to a static build hoping it'll reload — it won't.

  2. Stale. A static look must match current source — reuse only a fingerprint-matched build, else build fresh; reuse is explicit-only.

So a leftover EAS/release build is not a shortcut for "iterate live" — it's the wrong binary. The fact that a build exists never makes it the right one.

ModeWhat it isChoose whenLive edits?
A — Local release buildBuild a Release .app locally, agent-device install it (uploads)User has a Mac toolchain and wants a quick "run my current code on a cloud device"No (rebuild to see changes)
B — EAS build (rare, explicit-only)eas build a simulator build, agent-device install-from-source <url> (the VM downloads it)Only when explicitly asked — the user names an existing/EAS build, or wants a static EAS artifact for CI/sharing. Not for "show me"/"iterate" (use C). Sim builds need no credentials.No
C — Local dev build + tunnelDev (Debug) build + EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel + connect the dev client to MetroThe agentic edit-and-see loop — change code and see it live (Fast Refresh)Yes

Quick decision — default to C; A and B are explicit-only:

  • C (almost everything): iterate, interact, poke the app, live edits — and most "show me my app" (current code needs a build anyway, so live+current wins). Mac → dev client builds locally; no Mac → build it on EAS (developmentClient: true). Unsure → C.

  • A: only an explicit one-shot static screenshot on a Mac.

  • B: only when the user names an existing/EAS build or wants a static EAS artifact (CI/sharing) — see the box above for why a static build is the wrong tool for "iterate."

Before starting a Mode C tunnel or connecting the dev client, read Tunnel scope and approvals. Carry existing authorization for this project's remote development transport through tunnel creation, connection, and live edits; include its source and the concrete data flow in any approval request.

Driving the device (agent-device)

If a controller fails to download a recording, retrieve it from EAS session artifacts.

agent-device is the controller. Common verbs (run each as npx --yes eas-cli@latest simulator:exec npx agent-device@latest <verb>):

VerbDoes
apps --platform iosList user-installed apps (the blank sim shows none); add --all to include system apps
install <appId> <path> --platform iosInstall a local .app (uploads it)
install-from-source <url> --platform iosInstall from a URL — the VM downloads it (use for EAS artifacts)
open <appId|deep-link> --platform iosLaunch an app (bundle id) or follow an app deep link (exp+slug://…). A first-time deep link raises a system "Open in '<app>'?" dialog — expect it (don't burn a snapshot discovering it) and press 'label="Open"' to hand off; it can be slow, so bound it with agent-device's own --timeout (e.g. press 'label="Open"' --timeout 120000) — not a shell timeout wrapper (macOS has no timeout binary). (Mode C sidesteps this dialog for the Metro-connect link via "Enter URL manually" — see run-your-app.md.) Not for the webPreviewUrl — that's a browser preview for the user, never the device.
snapshot -iInteractive accessibility tree → @e1-style refs
press <ref|selector>Tap (e.g. press @e2 or press 'label="Open"'). click <target> also works. Use press or click: tap is a hidden alias that older versions reject
fill <ref> "text"Type into a field
screenshot <path>Capture the screen to a local PNG (downloaded from the daemon) — requires an app to be open (open first). On iOS the default is 1x logical points (402x874 on iPhone 17); add --pixel-density 3 for full resolution
record start [path] / record stopRecord the screen to a video at the record start path (record stop takes no path) — use this for motion (animations, gestures, transitions, timing), which a single screenshot can't capture
metro prepare / metro reloadPoint a dev client at Metro / reload (Mode C)

Screenshots vs. video. Default to screenshot for static state, but for anything that moves — an animation, a transition, a gesture, a timing/jank question — record a video and inspect the frames instead; a still can't prove motion. Both controllers record (agent-device record start/stop, argent screen-recording-start/stop). Recordings sample at ~30fps — enough to see visible jank, not to prove sub-frame 60/120Hz hitches. For timing specifically, argent drops static frames by default (turn trimStatic off) — that plus other per-controller gotchas are in references/controllers.md.

For the full verb set and the argent controller alternative, see references/controllers.md.

When the app crashes: device logs and crash reports (iOS)

When the app crashes or closes on launch, read the iOS session's crash reports and device log before guessing from screenshots. Read them from the preview API URL in simulator:get --json. That URL carries the session token, so never print it.

Hold the device log before you reproduce the crash. Without the hold, the crash gets a report but an empty log tail. Commands, fields, and fallbacks are in references/logs-and-crashes.md.

Operating principles

The non-obvious mental model worth internalizing. Specific error→fix lookups (hung verbs, tap rejected, --platform, --json, pod install locale, orphaned sessions, boot variability) live in references/troubleshooting.md.

  1. Establish ground truth, then reset — don't patch-loop. Never assume an existing session or Metro is yours or healthy. Before driving, confirm:

    • cwd — you're in the intended Expo project dir (a misdirected start/exec sessions the wrong app + drops a stray .env.eas-simulator; pwd / check app.json).

    • session live — IN_PROGRESS via simulator:get --json (a stopped session keeps its id + remoteConfig, so the dotenv alone isn't proof).

    • Metro on its own port — reuse only if you started it this session; else start one on a free port (--port <N>, e.g. 8082), don't kill another server to reclaim :8081 (run-your-app.md).

    • build fits intent — a release build can't live-reload; if live edits are wanted and a release build is installed, install the dev build, don't reconnect.

    If current code isn't rendering after your first connect, stop poking live state: reset to baseline (stop session → clear dotenv → kill your Metro) and redo the mode once; a second failure → stop and report. Never restart Metro in place, reconnect more than once, rebuild the native client to fix a JS/connection problem, or surface a preview URL while state is unknown. (A daemon drop — ERR_NGROK_3200 / Remote daemon is unavailable — is the same: reset, don't retry.)

  2. exec is a wrapper, not a driver. simulator:exec loads .env.eas-simulator and spawns the command you pass; the device verbs come from the controller (npx agent-device@latest). There is no simulator:tap.

  3. Act immediately; don't park an idle session. Sessions are short-lived — install and drive right after start. Leaving one idle drops the tunnel/daemon (→ reset, per #1).

  4. Stop sessions you created on completion or failure and reset the dotenv. --non-interactive does not stop a session when your task ends. For a requested live preview, follow the duration guidance above. Poll the existing session during a slow boot; starting another creates an extra session and overwrites the dotenv's session id.

  5. Screenshot only the correct, fresh build. Mode C only after the dev client connects to Metro; A/B only from a build matching current source — reusing a pre-existing build is the #1 "my edits don't show" cause (see the build caveat above). (9:41 in the status bar is the sim default, not staleness.)

Stop and clean up

After the task, stop the session you created and reset the dotenv so a later run doesn't try to reuse the dead session. For a requested live preview, keep it available for the agreed duration instead:

npx --yes eas-cli@latest simulator:stop          # omit --id → stops the dotenv session (or pass --id <id>)
printf '# managed by eas-cli\n' > .env.eas-simulator   # clear the stale session id so it isn't reused
# if you started Metro for Mode C, stop it too (Ctrl+C in its terminal, or kill the expo process)

References

Source of truth: Expo docs and the eas / agent-device CLIs (npx --yes eas-cli@latest simulator:* --help, npx --yes eas-cli@latest simulator:exec npx agent-device@latest help [topic]; --help after simulator:exec shows the EAS CLI help instead). This skill teaches how to apply them; it doesn't replace them.

Submitting Feedback

If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:

npx --yes @expo/agent-cli@latest feedback --category skills --subject "eas-simulator" "<actionable feedback>"

Only submit when you have something specific and actionable to report. Include as much relevant context as possible. If an agent repeatedly fails at an Expo task or the user has to step in, submit a brief report of the task, expected result, and what went wrong using --category evals and the relevant Expo package or command as --subject.

Files

references/run-your-app.md

Running your app on the remote sim — tested sequences

The remote sim boots blank. You install a simulator-targeted build onto the session, then open it. Pick a mode from SKILL.md. (Sequences validated against eas-cli 20.3.x + agent-device 0.17.x in mid-2026. These commands are experimental — check the relevant subcommand's --help before using non-default flags.)

In all modes, the session is started the same way and driven through npx --yes eas-cli@latest simulator:exec. Replace dev.example.app with the app's iOS bundleIdentifier (from app.json → ios.bundleIdentifier), and run from the project directory.

These sequences are iOS. For Android: build via npx --yes eas-cli@latest build --platform android (or local Gradle), install the .apk instead of an .app (.aab fails), and skip pod install. Android sessions include a browser preview (webPreviewUrl), which the expo.dev session page opens in a new tab with Open preview. Android support is still in development, so some iOS features may not work.

Starting a session (shared by all modes)

# If the dotenv names a session, inspect it first with simulator:get --json. Reuse it when it belongs
# to this run; stop it only when it is in scope and no longer needed. An IN_PROGRESS session may be
# intentionally concurrent, so preserve its id/config before resetting the dotenv. Replacing the file
# does not stop the remote session. Reset only after choosing how to handle the existing session.
printf '# managed by eas-cli\n' > .env.eas-simulator

# Start (the default --out-config-type dotenv writes .env.eas-simulator). It boots the sim + agent-device daemon.
# --json changes stdout but does not suppress the completed dotenv write; use --out-config-type env for no file.
# --name is required practice: it labels the session in simulator:list/get and on expo.dev.
# Describe what the run is for, in the user's terms — see "Always name the session" in SKILL.md.
npx --yes eas-cli@latest simulator:start --platform ios --type agent-device --non-interactive \
  --name "Checkout flow screenshots"

start's own poll is unreliable, so confirm liveness with a bounded loop (boot is ~90s–15min). get/exec/stop default to the session in .env.eas-simulator, so you can omit --id:

# Poll up to ~16 min; IN_PROGRESS + remoteConfig = live; a terminal status = failed boot (stop + restart).
for i in $(seq 1 64); do
  S=$(npx --yes eas-cli@latest simulator:get --json --non-interactive 2>/dev/null)
  echo "$S" | grep -q '"status": *"IN_PROGRESS"' && echo "$S" | grep -q remoteConfig && { echo "live"; break; }
  echo "$S" | grep -qE '"status": *"(STOPPED|ERRORED)"' && { echo "boot failed — stop + restart"; break; }
  sleep 15
done

If you need the id explicitly, it's EAS_SIMULATOR_SESSION_ID in .env.eas-simulator. start also prints a webPreviewUrl (browser preview on iOS and Android; remoteConfig.previewUrl for web-preview-only sessions — surface it per the SKILL.md "watch it live" rules) and a job-run URL. Once live, the session env is in .env.eas-simulator, so simulator:exec works.

Targeting a device — iPad, or several at once

Boot a specific device at session start with eas simulator:start --device "<name|UDID>" (eas-cli ≥ 22.4.0) — this is how you run on an iPad instead of the default iPhone:

npx --yes eas-cli@latest simulator:start --platform ios --device "iPad Pro 13-inch (M5)" \
  --non-interactive --name "iPad run"
# then install / launch / screenshot as usual — the iPad renders larger (e.g. 1032x1376).

The value must be a device the remote runner offers (NOT your local Xcode set), by name or UDID. List them from a live session:

npx --yes eas-cli@latest simulator:exec npx agent-device@latest devices --json

Available iOS devices today: iPhone 17 (the default) / 17 Pro / 17 Pro Max / 17e / Air; iPad (A16), iPad Air 11-inch (M4), iPad Air 13-inch (M4), iPad mini (A17 Pro), iPad Pro 11-inch (M5), iPad Pro 13-inch (M5); plus Vision Pro and Apple TV. On Android, --device takes an AVD hardware profile id (e.g. pixel_7); all profiles render at 720x1600, density 300.

Add a second simulator mid-session: a session boots only one simulator at start. Use a separate agent-device session for the second one: pass --session <name> --device "<name>" (boot takes about 46 seconds), and pass --session <name> on each later verb for that device. Passing --device in the default session fails with INVALID_ARGS "already bound".

npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId> "<devClientURL>" \
  --platform ios --session ipad --device "iPad Pro 13-inch (M5)" --relaunch
  • ⚠️ The controller --device takes a name only — a UDID returns DEVICE_NOT_FOUND; use --udid for a UDID. (The start-time CLI --device above takes either.)

  • devices reports each device's name, kind, and booted state, but not its iOS version.


Mode A — Local release build (embedded JS, no Metro)

A Release build bundles the JS into the binary, so it renders without Metro. Good for a quick "run my current code on a cloud device" when a Mac toolchain is available.

# 1. Generate native project + build a Release simulator .app
npx expo prebuild --platform ios          # set ios.bundleIdentifier in app.json first to avoid prompts
# pod install can fail on Ruby 4 + CocoaPods with a Unicode/ASCII-8BIT error — fix with a UTF-8 locale:
( cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install )
LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 xcodebuild \
  -workspace ios/<App>.xcworkspace -scheme <App> \
  -configuration Release -sdk iphonesimulator -derivedDataPath ios/build build
# → ios/build/Build/Products/Release-iphonesimulator/<App>.app

# 2. Start a session (see "Starting a session" above), then install + open + drive
APP=ios/build/Build/Products/Release-iphonesimulator/<App>.app
npx --yes eas-cli@latest simulator:exec npx agent-device@latest install dev.example.app "$APP" --platform ios
npx --yes eas-cli@latest simulator:exec npx agent-device@latest open dev.example.app --platform ios
npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./shot.png

# 3. Stop
npx --yes eas-cli@latest simulator:stop          # omit --id → stops the dotenv session

The install here uploads the (~90MB) .app to the remote daemon over the tunnel, which installs it on the sim with simctl.


Mode B — EAS build (the VM downloads it; no credentials)

Explicit-only (see the SKILL.md mode picker): a static EAS artifact for CI/sharing, or when the user names an existing EAS build. For no-Mac live iteration use Mode C with an EAS dev-client build (see Mode C below), not this. Simulator builds are unsigned, so EAS asks for no credentials.

⚠️ Check for an existing build first. Before triggering a new build, check if a fingerprint-matched one already exists — it saves ~15-20 min:

npx --yes eas-cli@latest build:list --platform ios --profile <your-sim-profile> --status finished --json | \
  head -20   # <your-sim-profile> = the profile you find/create in step 1; look for one whose fingerprint matches current source

If one matches, skip straight to step 3 with its artifact URL.

⚠️ Order matters: build FIRST, start the session LAST. The build takes ~15-20 min and a session left idle that long times out (ERR_NGROK_3200) — don't start until you have the artifact URL.

# 1. Find or create a simulator build profile in eas.json.
#    Read eas.json if it exists and look for a build profile with ios.simulator: true.
#    If one exists, note its name and skip to step 2.
#    If not, add one named "sim" — use node, python3, jq, or a direct JSON edit, whichever
#    is available. Preserve all other profiles. Minimum: { "ios": { "simulator": true } }

# 2. Build (no credentials prompt for a simulator build). Prints an artifact URL when done (~15-20 min).
npx --yes eas-cli@latest build --platform ios --profile sim --non-interactive
# → https://expo.dev/artifacts/eas/<hash>.tar.gz

# 3. Start a session, then install-from-source so the VM downloads the artifact (no local upload)
ART="https://expo.dev/artifacts/eas/<hash>.tar.gz"
npx --yes eas-cli@latest simulator:exec npx agent-device@latest install-from-source "$ART" --platform ios
npx --yes eas-cli@latest simulator:exec npx agent-device@latest open dev.example.app --platform ios
npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./shot.png

# 4. Stop
npx --yes eas-cli@latest simulator:stop          # omit --id → stops the dotenv session

Build freshness: reuse only a build whose fingerprint matches current source (npx --yes eas-cli@latest build:list --platform ios --json, or get-build by fingerprint per Callstack's public eas-agent-device workflow); otherwise rebuild or use Mode C. Tell the user which build you used. (Why this matters → SKILL.md "Reusing an existing build" caveat.)


Mode C — Dev build + tunnel (live edits via Fast Refresh)

The agentic edit-and-see loop: a dev (Debug) build loads JS from your Metro over a tunnel, so edits appear on the remote sim via Fast Refresh. Two ways to connect the dev client to Metro:

  • Method 1 (recommended, eas-cli ≥ 22.4.0): launch at session start — simulator:start installs the build, applies launch-args, and opens the Metro URL in one command, and the "Open in?" dialog is auto-handled. Needs a remote build source (--build-id, --build-fingerprint, --application-archive-url, or --expo-go); a local .app can't be passed here. --build-fingerprint selects the most recent finished, non-expired build with that hash (iOS: it must be a simulator build; Android: any distribution type can match, so use an APK profile). An iOS --application-archive-url must be a .tar.gz or .tgz that contains a .app (.zip, .app.zip, and .ipa fail); Android needs an .apk.

  • Method 2 (fallback): drive the connect with the controller — for a local .app build, or eas-cli < 22.4.0.

⚠️ Don't install a release build as a "quick interim" and screenshot it — it shows stale, build-time code. Use a dev build + Metro; screenshot only after the dev client is connected.

Get a dev-client build (either method needs one)
  • Local (Mac): npx expo install expo-dev-client; npx expo prebuild --platform ios --clean (set ios.bundleIdentifier first); ( cd ios && LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install ); then xcodebuild -workspace ios/<App>.xcworkspace -scheme <App> -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build-debug build → ios/build-debug/Build/Products/Debug-iphonesimulator/<App>.app. A local .app → Method 2 only.

  • EAS (no Mac, or to use Method 1): ensure a profile with developmentClient: true + ios.simulator: true, then npx --yes eas-cli@latest build --platform ios --profile <dev-sim> --non-interactive. Note the build id (Method 1's --build-id) or the artifact URL. Reuse a fingerprint-matched build to skip the ~15-20 min.

⚠️ Metro FIRST, then simulator:start — the runner opens --open-url during startup, with no retry.

# 1. Start Metro with a tunnel on your own free port (tunnel-backend details at the end of this mode).
EXPO_UNSTABLE_TUNNEL_V2=1 npx expo start --tunnel --port <your-free-port>   # background it durably
#    Capture the manifest host. Headless runs won't print it — read ngrok's API (curl -s 127.0.0.1:4040/api/tunnels)
#    or the manifest (curl -s -H "expo-platform: ios" localhost:<port>/ → launchAsset.url).

# 2. Start the session AND install+launch+open the app in one command (--launch-arg = one token per flag):
#    Dev client: --build-id <id>, --open-url <scheme>://expo-development-client/?url=https://<manifest-host>
#                (scheme = app.json `scheme`, NOT the slug; URL-encode the inner url if it has a path/query)
#    Expo Go:    --expo-go instead of --build-id, and --open-url exp://<manifest-host>  (no port; https opens Safari)
npx --yes eas-cli@latest simulator:start --platform ios --build-id <BUILD_ID> \
  --launch-arg "-EXDevMenuIsOnboardingFinished" --launch-arg "1" \
  --launch-arg "-EXDevMenuShowsAtLaunch" --launch-arg "0" \
  --launch-arg "-EXDevMenuShowFloatingActionButton" --launch-arg "0" \
  --open-url "<scheme>://expo-development-client/?url=https://<manifest-host>" \
  --non-interactive --name "Coin flip live edits"
#    The app installs, launches with the launch-args (onboarding/dev-menu/gear suppressed), and opens the URL.
#    The "Open in '<app>'?" dialog is auto-bypassed (the CLI writes the scheme approval) and the approval
#    persists session-wide — so NO `alert accept` is needed, here or for later controller opens.
#    `start` prints NOTHING about the install/launch — confirm from Metro's `iOS Bundled …` line.

# 3. To screenshot/drive, ATTACH the controller once — the CLI launch makes NO agent-device session, so a bare
#    `screenshot` fails `SESSION_NOT_FOUND`. `open --foreground` attaches without relaunching:
npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId> --foreground --platform ios
npx --yes eas-cli@latest simulator:exec npx agent-device@latest screenshot ./live.png
#    VERIFY it's the REMOTE sim, not a silent local-sim fallback (agent-device falls back to a LOCAL sim with no
#    error when the dotenv lacks remote config → believable but WRONG screenshots). Decisive tells: `simulator:get
#    --json` returns the id `start` printed, AND the attach's "Session state:" path is under /Users/expo/ (remote
#    VM), not /Users/<you>/ (your Mac). The `sessions/` vs `remote-diagnostics/` directory name is NOT reliable.

# 4. Fast Refresh: edit a source file → it hits the remote sim with no reload. Screenshot again to confirm.
# 5. Stop: npx --yes eas-cli@latest simulator:stop   # then kill the Metro process
Method 2 — drive the connect with the controller (fallback)

For a local .app (can't be passed to --build-id) or eas-cli < 22.4.0. Start a plain session (see "Starting a session"), install the build, then deep-link the dev client:

# install: a local .app uploads over the tunnel; an EAS artifact uses install-from-source (VM downloads it):
npx --yes eas-cli@latest simulator:exec npx agent-device@latest install <bundleId> "$DEVAPP" --platform ios
#   (EAS artifact instead:  install-from-source "https://expo.dev/artifacts/eas/<hash>.tar.gz" --platform ios)

# connect: `open <bundleId> <devClientURL>` deep-links into the bundle, skipping the launcher UI. Assemble
# <devClientURL> from the app's SCHEME (app.json `scheme`, NOT the slug): <scheme>://expo-development-client/?url=https://<manifest-host>
npx --yes eas-cli@latest simulator:exec npx agent-device@latest open <bundleId> "<devClientURL>" --platform ios --relaunch \
  --launch-args "-EXDevMenuIsOnboardingFinished" --launch-args "1" \
  --launch-args "-EXDevMenuShowsAtLaunch" --launch-args "0" \
  --launch-args "-EXDevMenuShowFloatingActionButton" --launch-args "0"
# a controller open on a BARE session (no Method-1 launch to pre-approve the scheme) can raise the
# "Open in '<app>'?" dialog — accept it (no-op if absent; not needed after a Method-1 launch):
npx --yes eas-cli@latest simulator:exec npx agent-device@latest alert accept 2500 --platform ios
# then screenshot; if it shows the launcher not the app, the deep link didn't take → manual fallback:
#   press 'label="Enter URL manually"' → snapshot -i → fill @<field> "<manifest URL>" → press 'label="Connect"'
#   → press 'label="Reload"'; press 'label="Go back"' if expo-router shows "Unmatched Route".
Dev-menu launch flags (both methods)

The launch-args are iOS UserDefaults (-Key Value), verified in expo/expo packages/expo-dev-menu. By default the onboarding popup, auto-opened dev menu, and floating gear all show and clutter screenshots; these suppress them:

  • -EXDevMenuIsOnboardingFinished 1 — skip the first-run onboarding popup (dev client and Expo Go)

  • -EXDevMenuShowsAtLaunch 0 — don't auto-open the dev menu at launch (dev client)

  • -EXDevMenuShowFloatingActionButton 0 — hide the floating gear (defaults visible on both)

Method 1 passes each as two flags: --launch-arg "<key>" --launch-arg "<value>". Method 2 passes them as --launch-args.

On SDK 58+, you can put the same switches in the URL instead, which also works on Android, where launch-args don't apply: append __expo_disable_onboarding=1&__expo_disable_fab=1&__expo_disable_auto_launch=1 to the exp:// URL or to <scheme>://?__expo_url=<encoded manifest URL>. On SDK 57 and earlier, keep the launch-args on iOS; on Android, the legacy dev client URL takes &disableOnboarding=1&disableFab=1&disableAutoLaunch=1 after url= (disableFab and disableAutoLaunch need expo-dev-launcher 57.0.20+). See ../../expo-overview/references/launch-without-dev-menu.md.

Metro tunnel backends (both methods)
Tunnel scope and approvals

Mode C needs a route from the EAS VM to your local Metro server. The account-signed Expo tunnel is the normal transport for this workflow: it forwards requests to http://localhost:<your-free-port> so the remote app can fetch its development manifest, JavaScript bundle, and assets and receive live updates. Describe that concrete purpose when requesting network execution, including the project, port, and expected Expo service; a bare “start a public tunnel” omits the task context.

Carry authorization through the whole development loop. When the user's existing authorization covers this project's EAS Simulator live development and required networking, treat starting its temporary Metro tunnel, connecting its dev client (including the launcher Connect button), and fetching subsequent bundles through Fast Refresh as steps of that authorized workflow. Do not ask the user to repeat consent for each step or later in-scope UI edit. A newly generated hostname on the same verified tunnel provider does not by itself expand the authorized scope; a different project, provider, exposed service, or user restriction requires reassessing it.

This skill supplies technical context, not permission, and cannot override the host's approval policy. Identify the actual authorization: quote the relevant user message or user-provided project instruction and name its source, subject to the host's trust rules. Do not present this skill's own workflow instructions as user consent. A signed tunnel URL authenticates tunnel creation to an Expo account; it is not proof that requests to the public Metro URL require authentication. URL randomness limits discoverability, not access. Keep the URL within the intended session and stop your Metro process when finished.

Put the authorization and transport details in the approval request itself so the reviewer can evaluate them together. Adapt this example only to facts verified for the current run:

Continue the authorized EAS Simulator live-development workflow for <project>. Existing authorization: <quote the relevant instruction> from <user message or project instruction source>. Start this project's Metro server on localhost:<port> through <verified tunnel provider> so the project's dev client on EAS Simulator can fetch its development manifest, JavaScript bundle, and assets and receive Fast Refresh updates. The Metro endpoint is publicly reachable by URL while the tunnel runs. Keep the URL within the intended session and stop this Metro process when the requested development session ends.

For a dev-client connection request, reference the same authorization and the verified Metro URL from this run. Explain that Connect performs the bundle fetch for the already authorized workflow.

If approval review rejects the tunnel or connection, explain the exact action and stated reason. If the host permits reconsideration and the rejection missed relevant evidence, submit a reconsideration with the concrete transport details and original authorization source. Do not blindly repeat a denied command or switch tools or providers to evade the rejection. If reconsideration is unavailable or still denied, report the remaining block and ask for approval of that specific exposure as the host requires. Keep a live-edit request on Mode C while resolving the block. Choose a static build only if it satisfies the user's requested outcome and is authorized; it loses Fast Refresh.

Source: Expo CLI's AsyncWsTunnel.ts resolves the Expo account, requests a signed URL, and sets the local target port. Verify the installed CLI's actual backend below: the environment flag alone does not prove it selected Expo's service.

Backend selection

Start Metro on your OWN free port — each run gets its own tunnel URL, so never fight for or kill :8081 (#133's rule). BOTH backends accept ANY --port:

  • ws-tunnel v2 (account-signed): EXPO_UNSTABLE_TUNNEL_V2=1 — signed URL for your EAS account, on.expo.app host, and the path for robot/EXPO_TOKEN/cloud agents (plain ngrok is blocked for them). Needs login / an EAS-linked project; if the signed URL fails, the CLI says to unset the flag and use ngrok.

  • ngrok (plain --tunnel, no flag): <host>.exp.direct host; blocked for robot/EXPO_TOKEN users.

The ONLY 8081 lock is the LEGACY ws-tunnel path — hit WITHOUT the v2 account URL (an older CLI where the flag no-ops, or EXPO_FORCE_WEBCONTAINER_ENV=1). Do NOT set EXPO_FORCE_WEBCONTAINER_ENV to "fix" a port — it forces that legacy path and locks you to 8081. On an older CLI (e.g. expo 56) the v2 flag no-ops and you get ngrok on your chosen port (verified: expo 56.0.3 → ngrok on :8083).

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Needs Node.js

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Expo
Publisher
Expo (Official)
Source
expo/skills · plugins/expo/skills/eas-simulator · commit a9e5bd3
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/expo/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Expo or Expo.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download eas-simulator.zip