All skills
expo avatar

/eas-simulator

@c0dadf3 official
by expoexpo/skills2.6k stars
156

EAS service (paid). 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.

Use this Skill: https://skilld.dev/gh/expo/skills/eas-simulator

This session only. Nothing lands on disk.

referencestroubleshooting.md

≈3.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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 requested duration may not be supported by the account; inspect the CLI error Use the default session limit when the custom duration is unavailable.
Appium or browser-preview session stops despite ongoing interaction Only activity reported through agent-device and argent resets --max-idle-time-minutes; Appium commands and browser-preview activity do not Use the maximum duration as the lifetime bound for Appium and user-driven previews. Customize it with --max-duration-minutes when supported by the account, and omit --max-idle-time-minutes unless inactivity from a supported controller 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) In a remote session agent-device's --device resolves by name, not udid (despite the CLI docs) Pass the device name from agent-device devices (e.g. --device "iPad Pro 13-inch (M5)"), not the udid.
Unknown command: tap The tap verb is press Use press <ref|selector> (e.g. press @e2 or press 'label="Open"').
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/ (remote), not /Users/<you>/ (local); 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.
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.
  • snapshot can be slow on iOS (tens of seconds).
  • First bundle load over the tunnel (Mode C) is the slow part; subsequent Fast Refreshes are fast.

Source: SKILL.md on GitHub

No alerts2d3 checks · Risk SAFE
  • Gen Agent Trust Hub2d

    This skill allows for remote control of iOS and Android simulators via EAS Cloud. It is functionally robust but contains an inherent risk of indirect prompt injection because it processes untrusted UI data and logs from the remote device. It follows security best practices for credential management.

  • Socket2d

    No alerts

  • Snyk2d

    Risk: LOW · No issues

Signed by skilld at c0dadf3. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 3 days ago
What it can do
Runs commands
version
1.0.0
All 9 allowed tools
Bash(npx *eas-cli@*)Bash(npx *agent-device@*)Bash(npx expo *)Bash(eas *)Bash(expo *)Bash(xcodebuild*)Bash(pod*)Bash(argent *)Bash(ffmpeg*)

README badge

README badge for expo/skills/eas-simulator