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
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/expo/eas-simulator/downloadUnzip 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:stopwhen 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
eascommand vianpx --yes eas-cli@latest …— guarantees a CLI new enough to havesimulator:*(a globaleasis often too old), and--yesskips npx's prompt. (Bareeasis fine ifeas --versionis current.)Authenticated. Interactive machine →
npx --yes eas-cli@latest login. Cloud sandbox / CI / headless agent has no browser login — setEXPO_TOKEN(expo.dev → Account → Access Tokens) in the env instead. Verify either way withnpx --yes eas-cli@latest whoami.Run from an Expo project directory. A fresh app needs one-time setup:
npx --yes eas-cli@latest initto create/link the project (when there's noprojectId), and setios.bundleIdentifierin app config if it's missing — a freshcreate-expo-appoften has none, andprebuild/eas buildneed it (they prompt or fail without it; e.g.dev.<owner>.<slug>). Read current config withnpx expo config --json(it may live inapp.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-onlyhas no automation interface. See references/controllers.md..env.eas-simulatoris written/managed by eas-cli (not this skill): it holds the session id (EAS_SIMULATOR_SESSION_ID) + the daemon URL/token, soget/stop/execdefault 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.gitignorewon't cover it — add.env.eas-simulatorif 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 (theeas-cli/agent-deviceinvocations themselves are cross-platform).
Session lifetime
--max-duration-minutes Nis 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 Nstops 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 producerappium). 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 sessionIf 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-simulatorTo 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
openthewebPreviewUrlon the sim. It's a browser preview, not a deep link and not anagent-device openargument; 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 existsRules:
Derive it from the user's request, not from the mode or the tooling.
Mode C session,agent-device ios, andtestsay 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 --helpThe 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.
| Command | Purpose |
|---|---|
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):
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.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.
| Mode | What it is | Choose when | Live edits? |
|---|---|---|---|
| A — Local release build | Build 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 + tunnel | Dev (Debug) build + EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel + connect the dev client to Metro | The 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>):
| Verb | Does |
|---|---|
apps --platform ios | List user-installed apps (the blank sim shows none); add --all to include system apps |
install <appId> <path> --platform ios | Install a local .app (uploads it) |
install-from-source <url> --platform ios | Install from a URL — the VM downloads it (use for EAS artifacts) |
open <appId|deep-link> --platform ios | Launch 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 -i | Interactive 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 stop | Record 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 reload | Point 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.
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/execsessions the wrong app + drops a stray.env.eas-simulator;pwd/ checkapp.json).session live —
IN_PROGRESSviasimulator: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.)execis a wrapper, not a driver.simulator:execloads.env.eas-simulatorand spawns the command you pass; the device verbs come from the controller (npx agent-device@latest). There is nosimulator:tap.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).Stop sessions you created on completion or failure and reset the dotenv.
--non-interactivedoes 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.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:41in 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
references/run-your-app.md — full command sequences for modes A, B, and C (read before running a mode).
references/controllers.md — agent-device verb reference and the
argentalternative.references/logs-and-crashes.md — device logs and crash reports from an iOS session (read when the app crashes or misbehaves).
references/troubleshooting.md — concrete errors and fixes.
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
- ATTRIBUTION.mdAdded by AutoGineer813 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md26.1 KiB
- agents/openai.yaml426 B
- references/controllers.md13.7 KiB
- references/logs-and-crashes.md7.6 KiB
- references/run-your-app.md21.6 KiB
- references/troubleshooting.md14.5 KiB
references/troubleshooting.md
Troubleshooting
Concrete errors seen while validating this flow, and the fix.
| Symptom | Cause | Fix |
|---|---|---|
| Approval review rejects the Mode C Metro tunnel or dev-client Connect action | Review may lack or misinterpret the transport details or existing authorization; a signed URL alone does not establish private access | Follow Tunnel scope and approvals. Include the original authorization source and verified transport in the request; use reconsideration only where the host permits it. Preserve the requested live workflow while resolving approval. |
| Controller recording download fails or times out | The local transfer can fail even though EAS retains the recording | Fetch it from EAS session artifacts using the original EAS session id and the recording’s downloadUrl. |
Command simulator:start not found | eas-cli too old (commands are hidden but present from ≥ 20.3.0) | Run via npx --yes eas-cli@latest …, or upgrade eas-cli. |
simulator:start rejects --name (e.g. Nonexistent flag: --name) | eas-cli too old — --name was added after simulator:start itself | Run via npx --yes eas-cli@latest …, or upgrade eas-cli. If you can't upgrade, retry once without --name; the session starts unnamed. |
An Expo user account is required / whoami shows logged-out | No browser login on a cloud/CI/headless box, or EXPO_TOKEN unset/invalid | Set EXPO_TOKEN (expo.dev → Account → Access Tokens) in the env; verify npx --yes eas-cli@latest whoami. (Interactive machines can eas login.) |
simulator:start/build: no linked project / missing projectId | A fresh create-expo-app isn't linked to EAS | npx --yes eas-cli@latest init to create/link it (writes extra.eas.projectId). |
prebuild/eas build prompts for or fails on a missing iOS bundle identifier | A fresh app often has no ios.bundleIdentifier | Set it in app config (e.g. dev.<owner>.<slug>); confirm via npx expo config --json (may live in app.config.js). |
--max-duration-minutes rejected | The value is above the plan limit (40 minutes on Free, 115 on paid plans); inspect the CLI error | Pass a value within the plan limit, or omit the flag to use the default. |
| Browser-preview session stops despite ongoing interaction | Browser-preview activity does not reset --max-idle-time-minutes; only controller activity (agent-device, argent, Appium) does | Use the maximum duration as the lifetime bound for user-driven previews. Set it with --max-duration-minutes, and omit --max-idle-time-minutes unless controller inactivity is the intended stop condition. |
simulator:start fails with not enabled for this account / not-allowlisted | EAS Simulator is limited-access and isn't enabled for this account | Don't retry. Confirm with simulator:availability, then hand off gracefully — tell the user and fall back to a local sim / EAS Build (see SKILL.md Check availability first). |
start keeps "Waiting for … session to be ready" but it never returns | start's readiness poll can miss a session that's actually live | Don't rely on it — poll npx --yes eas-cli@latest simulator:get --id <id> --json for status: IN_PROGRESS + a populated remoteConfig. |
ERR_NGROK_3200 / endpoint offline; Remote daemon is unavailable | The session's tunnel/daemon dropped — left idle and timed out, or the VM was torn down | A drop invalidates the whole session (installed app, @e refs, Metro). Don't retry the failed verb — start a fresh session, reset the dotenv, and re-run install→open→drive from the top, acting immediately. |
| Two sessions running / orphaned session | A second start (e.g. to "retry" a slow boot) creates another session and overwrites the dotenv id, orphaning the first | Poll the existing session instead. Find orphans with simulator:list --status in-progress and stop those you created with simulator:stop --id <id>. |
| A device verb hangs (no return for a minute+) | Slow daemon; press/screenshot can block ~90s | Bound it with agent-device's own --timeout <ms> (e.g. --timeout 120000) — not a shell timeout wrapper (macOS has no timeout binary, so timeout 120 … fails with command not found and skips the verb). On timeout snapshot -i to see if the action landed before retrying (taps can double-fire). Don't blind-retry. |
install requires an active session or an explicit device selector | install can't infer the device | Pass --platform ios (or open something first to establish a session). |
DEVICE_NOT_FOUND: No device named <udid> when targeting a non-default device (iPad, second sim) | agent-device's --device takes a name only | Pass the device name from agent-device devices (e.g. --device "iPad Pro 13-inch (M5)"), or pass a UDID with --udid. |
INVALID_ARGS "already bound" when passing --device | The default agent-device session is already bound to the first simulator | Use a separate agent-device session for the second simulator: --session <name> --device "<name>" (see run-your-app.md). |
Unknown command: tap | An older agent-device version; tap is now a hidden alias of press | Use press <ref|selector> (e.g. press @e2 or press 'label="Open"') or click <target>. |
record stop times out | The remote device is still exporting the clip | Run record stop again; add --platform ios if it cannot find the recording. The clip also uploads as a session artifact (see controllers.md). |
--application-archive-url install fails | Unsupported archive type | iOS needs a .tar.gz or .tgz that contains a .app (.zip, .app.zip, and .ipa fail). Android needs an .apk (.aab fails). |
agent-device@latest --help through simulator:exec shows the EAS CLI help | oclif catches --help | Use ... agent-device@latest help [topic]. |
SESSION_NOT_FOUND: No active session. Run open first. | A verb (e.g. screenshot) ran before any app/session was opened — or you used Method 1 (simulator:start --open-url), which launches the app but creates NO agent-device session | open <app|url> first (or pass --platform ios). After a Method-1 launch, attach without relaunching: agent-device open <bundleId> --foreground --platform ios (pass the bundle id — --foreground alone fails AMBIGUOUS_MATCH), then screenshot. |
| Screenshot looks plausible but the session/UI is wrong (e.g. Safari, an iPhone shot when you booted an iPad, or "incompatible Expo Go SDK") | agent-device silently falls back to a LOCAL simulator when .env.eas-simulator has no remote config — no error, believable-but-wrong output. Common cause: a concurrent simulator:start on the same account/machine overwrote the shared dotenv with its own id (the dotenv is a single file, NOT concurrency-safe). | Confirm you're on the REMOTE VM: simulator:get --json returns the id start printed, AND the verb's "Session state:" path is under /Users/expo/ (iOS) or /home/expo/ (Android) on the remote device, not /Users/<you>/ (local). session state-dir prints the local path, so it is not proof; devices --json host is a turtle-worker-*. The sessions/ vs remote-diagnostics/ directory name is NOT a reliable tell. If concurrency is possible, drive by explicit id — load the daemon vars from simulator:get --id <id> --json — instead of trusting the dotenv. |
simulator:exec / build / simulator:stop: "Run this command inside a project directory." | Run from the wrong cwd | Run from the Expo project directory (where app.json/eas.json live). |
| New session's id shows as the previous one; "Overwriting previous simulator session (id: …)" | .env.eas-simulator names an earlier session | Inspect it 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 and drive sessions by explicit id/config. Replacing the file does not stop the remote session. |
No .env.eas-simulator written after start | --out-config-type env was selected, or the CLI reported a file-write failure | Use the default --out-config-type dotenv for the exec flow. --json changes output and implies non-interactive mode, but does not by itself suppress the completed dotenv write. --out-config-type env prints only the controller export lines and no EAS_SIMULATOR_SESSION_ID, so pass --id to get, stop, and events. |
pod install fails: Unicode Normalization not appropriate for ASCII-8BIT | Ruby 4 + CocoaPods with a non-UTF-8 locale | Re-run with LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pod install. |
(Mode C) Deep-link open lands on the dev-client launcher, not the app | The "Open in '<app>'?" system dialog wasn't accepted, so the deep link didn't take | Accept the dialog with agent-device alert accept 2500 --platform ios (not a UI tap). If it still lands on the launcher, fall back to "Enter URL manually" → fill the https://<host>.on.expo.app manifest URL → "Connect" (see run-your-app.md Mode C). |
| (Mode C) App shows expo-router "Unmatched Route" | The connect URL was parsed as a route path | press 'label="Go back"' (or navigate to /). |
(Mode C) Dev client shows a ? placeholder / blank after connect | Bundle not fetched yet | press 'label="Reload"' and wait ~40-60s for the first build+transfer over the tunnel. |
(Mode C) expo start fails: "port 8081 already in use" | Another Metro owns 8081 | Don't kill it. Start on your own --port — both EXPO_UNSTABLE_TUNNEL_V2=1 (account-signed) and plain ngrok accept any port. Only the LEGACY ws-tunnel path is 8081-locked (see the WS_TUNNEL_PORT row). Reuse a Metro only if you started it this session. |
(Mode C) expo start / node killed with exit 137 | 137 = SIGKILL — almost always the OOM killer (memory pressure, common in constrained cloud sandboxes, esp. a native build + Metro at once). Not a port clash. | Reduce memory pressure: don't run a native build and Metro concurrently; give the sandbox more memory; retry. |
| (Mode C) Edits won't live-reload no matter how often you reconnect | A release build is installed — its JS is baked in, so it ignores Metro | Stop reconnecting: install the dev (Debug) build, connect it to Metro, reload. Reconnecting a release build to Metro is a no-op. |
expo start --tunnel errors for a robot/EXPO_TOKEN user | The ngrok robot-user guard blocks plain (ngrok) tunnels | Use ws-tunnel v2 (account-signed, any port): EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel --port <any> — needs login / an EAS-linked project. Do NOT use EXPO_FORCE_WEBCONTAINER_ENV; that forces the legacy path, which is 8081-locked. |
CommandError: WS-tunnel only supports tunneling over port 8081 | You're on the legacy ws-tunnel path — no v2 account URL (older CLI where EXPO_UNSTABLE_TUNNEL_V2 is a no-op, not logged in, or EXPO_FORCE_WEBCONTAINER_ENV set) | Get onto the account-signed v2 path: set EXPO_UNSTABLE_TUNNEL_V2=1 and log in / link the project — then any --port works. Otherwise use --port 8081, or the ngrok path (drop the flag; non-robot only). |
| Unexpected charges / a session you forgot | start --non-interactive does NOT auto-stop | Always npx --yes eas-cli@latest simulator:stop --id <id>. List leftovers with npx --yes eas-cli@latest simulator:list. |
| Screenshot shows old content / my recent edits don't appear | Running a release build (Mode A/B) whose JS was baked in before your edits — typically a reused/stale build | A/B reflect code at build time, not now. Rebuild (ensure the build's fingerprint matches current source), or use Mode C (dev + Metro) so live edits show via Fast Refresh. The screenshot itself is fresh — it's the build that's stale. (9:41 in the status bar is the sim default, not staleness.) |
(Android) The emulator stopped, and agent-device boot times out (Daemon request timed out) while the device stays booted=false | Without --headless, agent-device starts the emulator with a window. The EAS Linux image cannot run the windowed emulator, so it exits at once | Boot headless. Set AGENT_DEVICE_HEADLESS=1 for the whole run, or pass --headless on each boot: AGENT_DEVICE_HEADLESS=1 npx --yes eas-cli@latest simulator:exec npx agent-device@latest boot --platform android --device <avd-name>. Get the AVD name from agent-device devices --platform android. The variable is read by the local client, so set it where you run the command. |
(argent) Every argent run/tools call returns 401 Unauthorized right after linking | argent link without --yes no-ops on an already-linked URL ("Already linked. No changes."), keeping a stale token from a previous session | Re-link with --yes so the new token is written — see the link command in controllers.md. |
/crashes returns 404 Not found on the preview API | The session's preview server predates the crash and log routes | Use agent-device logs for the device log, and tell the user crash reports aren't available on that session. Don't call /logs there. See logs-and-crashes.md. |
sim_api stops with API_BASE: no preview API | The API= lookup failed: the session is not IN_PROGRESS, is not iOS, or has no preview API | Read the lookup's own error line above it. Check the --id, or start a new session. See logs-and-crashes.md. |
sim_api fails with 401 | The session ended or its token changed, so the preview server rejects the old one | Run the API= lookup again. It prints the session's status when the session is no longer IN_PROGRESS. See logs-and-crashes.md. |
A crash shows up but its logTailSource is none, buffer-rolled-past, or no-app-lines with no lines | Nothing held the device log when the crash happened, or a busy device rolled the buffer past it (common in the first minutes after boot) | Start the /crashes?tail=1 hold (or call sim_api /logs "snapshot=1&follow=1&limit=1" more often than every 8 seconds) before reproducing. After a boot, reproduce again once logging settles, a minute or two later. |
Performance expectations
Set the user's expectations honestly — this is experimental:
Boot is variable: ~90s warm to ~15 min cold. Poll patiently.
snapshotcan be slow on iOS (tens of seconds).First bundle load over the tunnel (Mode C) is the slow part; subsequent Fast Refreshes are fast.
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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download eas-simulator.zip