Skip to content

Expo development with Claude Code, Codex, or Cursor works when the agent knows two facts first: the pinned Expo SDK (SDK 57 is current on npm as of 2026-09-26) and whether the app runs in Expo Go or a development build. Expo’s official skills, a fingerprint-gated EAS pipeline, and Maestro flows then make each change verifiable without reading every line.

This page is for developers and tech leads who ship an Expo app to the stores. A teammate adds react-native-vision-camera, Expo Go stops loading the app, and the agent keeps writing code for whichever setup it guessed. A week later an over-the-air (OTA) update reports success, yet no phone picks it up, because the update targets a runtime that no installed build has.

Before you start: this page assumes the React Native basics (navigation, state, native modules) from React Native development patterns, and an agent that can install skills, as described in how to install and manage skills.

  • An agent setup with Expo’s official skills and MCP server, installed per tool
  • A scaffolding prompt that pins the SDK and the build type, so the agent stops guessing
  • A copy-paste prompt for an Expo Router auth gate, and the Maestro flow that proves it works
  • A fingerprint-gated EAS Workflows pipeline that decides between a store build and an OTA update for you
  • A staged OTA rollout with the commands to watch it and roll it back
  • A “what breaks” map for SDK upgrades, Expo Go mismatches, credentials, and runtime mismatches

Which Expo versions does this page assume?

Section titled “Which Expo versions does this page assume?”

Tell the agent the versions. Its training data spans several SDKs, and each SDK changes APIs, config keys, and the React Native version under it. The table lists what npm served on 2026-09-26.

PackageVersion on 2026-09-26Note
expo57.0.25 (SDK 57, released 2026-06-30)the sdk-56 dist-tag still receives patches; SDK 58 is on the next dist-tag, so check npm view expo dist-tags before pinning
react-native / react0.86.3 / 19.2.3the versions SDK 57’s bundledNativeModules.json pins
expo-router57.0.23Stack.Protected takes a guard prop
eas-cli24.8.0fingerprint, workflow, and update-rollout commands used below
nativewind4.2.75.0 is a release candidate (5.0.0-rc.0); pin one major

Never let the agent write a version into package.json by hand. npx expo install <package> picks the version that matches your SDK, and npx expo install --check lists every package that drifted.

Set up Expo skills and the Expo MCP server

Section titled “Set up Expo skills and the Expo MCP server”

Expo publishes its own agent skills in expo/skills (about 2.6k stars on GitHub on 2026-09-26). The Expo plugin for Claude Code and Codex (1.13.7 on 2026-09-26) bundles 24 skills, such as expo-router, expo-upgrade, eas-update, and eas-workflows. It also registers Expo’s remote MCP server at https://mcp.expo.dev/mcp. The skills load on demand, so they cost little context until a task needs one. MCP servers cost context per tool; Claude Code and Codex both defer tool definitions behind tool search, so the cost stays lower there, but every server you connect still adds to it.

Terminal window
# Skills + Expo MCP server in one plugin
claude plugin install expo@claude-plugins-official

To add only the MCP server, run claude mcp add --transport http expo https://mcp.expo.dev/mcp, then open /mcp in a session to complete any sign-in it asks for. Without --transport http, Claude Code 2.1.283 saves the URL as a stdio command, and the connection fails later.

For how skills and MCP servers compare, see how to install and manage skills and the frontend skills roundup.

Scaffold an Expo app the agent cannot misread

Section titled “Scaffold an Expo app the agent cannot misread”

The prompt matters less than the two facts it pins. Put them in AGENTS.md (or CLAUDE.md) as well, so every later session inherits them.

The fingerprint runtime-version policy matters later. It hashes everything native in the project, so any native change produces a new runtime version by itself. That removes the most common OTA accident: a JavaScript update that calls a native module the installed build does not have.

Run claude --permission-mode manual in an empty directory and paste the prompt. The expo-overview skill routes the task to expo-project-structure and expo-dev-client. Manual mode makes every eas command wait for your approval. From v2.1.283 (both release channels on 2026-10-12), Claude Code starts interactive sessions in auto mode, where a classifier approves commands for you.

Build an auth-protected Expo Router layout

Section titled “Build an auth-protected Expo Router layout”

Most Expo Router bugs sit in the auth gate. A redirect before the root layout mounts triggers a “navigate before mounting the Root Layout” warning. A redirect during render flashes protected content for a frame. Stack.Protected removes both, because the router never registers the protected screens while the guard is false.

The agent should produce this tree:

app/
_layout.tsx # SessionProvider + Stack.Protected guards
(auth)/
_layout.tsx
login.tsx
(tabs)/
_layout.tsx # Tabs
index.tsx
profile.tsx
.maestro/
auth.yaml

You do not need to read the layout to accept it. Search it for router.replace and <Redirect: a hit inside the render path means the redirect flash is back, so send it back with “move the guard into Stack.Protected”. Then run the Maestro flow on a simulator with the development build installed (maestro test .maestro/auth.yaml). The flow is the acceptance criterion: sign-in, a protected tab, and sign-out either pass on a real runtime or they do not.

Install the Maestro CLI once per machine with curl -fsSL "https://get.maestro.mobile.dev" | bash, the command in the Maestro README (the repository’s main branch sets CLI version 2.11.0 on 2026-09-26). It needs Java 17 or newer, and maestro test needs a booted simulator or emulator with the development build already installed.

The usual NativeWind failure is a wall of className="bg-[#1d4ed8]" values. A rebrand then touches every screen. Ask for semantic tokens so colours live in one place.

The last sentence turns the style rule into a gate. You stop reviewing colours by eye, because the check fails on the first hard-coded hex value. More UI recipes for React Native and Flutter are in mobile UI recipes.

Ship with a fingerprint-gated EAS pipeline

Section titled “Ship with a fingerprint-gated EAS pipeline”

The riskiest decision in an Expo release is “store build or OTA update?”. An agent should not make it by judgement. eas-cli 24.8.0 ships a deploy workflow template that makes the decision mechanically: eas workflow:create saves it as .eas/workflows/deploy.yml by default, modelled on Expo’s deploy-to-production example. A fingerprint job hashes the native project, and a get-build job looks for an existing production build with that hash. If one exists, the workflow publishes an update. If not, it builds and submits to the stores.

You verify the pipeline without reading every YAML key:

  1. workflow:validate passes for both files.
  2. The production file contains jobs of type fingerprint, get-build, build, submit, and update, and each update job’s if: checks that its get-build job returned a build_id. Search for type:, not for job names: the template names its jobs get_ios_build, build_android, and so on.
  3. Before you merge a native-looking change, compare your working tree with the latest production build. Get the build ID with eas build:list --platform ios --build-profile production --limit 1 --json, then run eas fingerprint:compare --build-id <BUILD_ID> (add --non-interactive in CI). Repeat for Android: run the same eas build:list with --platform android and compare against that build ID. Bare eas fingerprint:compare opens an interactive picker, so an agent or a CI step cannot run it unattended. A different hash means the next push triggers a store build, not an OTA update.

If your team runs CI on GitHub Actions instead, keep the same decision logic and have the agent read the existing workflows first through the GitHub MCP server. The generic pipeline patterns are in CI/CD pipelines with agents.

An OTA update reaches every user on a channel within minutes, so ship it the way you would ship a server change: to a slice first.

  1. Publish to 10% of users on the production branch:

    Terminal window
    eas update --branch production --message "Fix checkout total rounding" --rollout-percentage 10
  2. Watch launches and crashes for the update group:

    Terminal window
    eas update:insights <GROUP_ID> --days 1
  3. If the numbers match the previous update, widen the rollout with eas update:edit <GROUP_ID> --rollout-percentage 100.

  4. If crashes rise, revert with eas update:revert-update-rollout, or roll back to an earlier update or the embedded bundle with eas update:rollback.

GROUP_ID is the update group ID that eas update prints. Hand the agent this sequence as a runbook, not as permission. It can prepare the commands and read update:insights, and a person approves each widening step. The general pattern is in progressive delivery.

Agents usually stop after the permission prompt. A working setup also sends the token to your backend, refreshes it, and routes a tapped notification to the right screen.

The Jest test proves the mapping from payload to route. The Maestro flow proves that the route opens on a device. Delivery itself still needs one manual send to a physical device per platform, because simulators do not reproduce every push path.

How you verify agent output without reading every line

Section titled “How you verify agent output without reading every line”

Expo gives you enough machine-checkable signals that review becomes a check of evidence, not of code.

GateCommandWhat it catches
Types, lint, unit testsnpm run check (tsc --noEmit, expo lint, jest --ci)wrong API shapes, dead imports, logic regressions
Dependency driftnpx expo install --check and npx expo-doctorversions outside the SDK’s range, duplicate native modules
Native changeeas fingerprint:compare --build-id <BUILD_ID> --non-interactivea JavaScript-only PR that changed the runtime
BehaviourMaestro flows in .maestro/auth gates, deep links, and navigation on a real runtime
Release healtheas update:insights and Sentrycrashes and launch failures per update group

Make the agent attach the output of each gate to the pull request. The reviewer then reads what changed in behaviour, the Maestro result, and whether the fingerprint moved. That is the same evidence bundle the rest of the site uses. For crash triage, the @sentry/react-native config plugin uploads source maps during EAS builds, and the Sentry MCP server (https://mcp.sentry.dev/mcp) lets the agent read the symbolicated trace itself. Device testing beyond Maestro, such as device farms and visual checks, is covered in mobile app testing.

What breaks in Expo projects, and how do you recover?

Section titled “What breaks in Expo projects, and how do you recover?”
  • An SDK upgrade leaves native dependencies behind. Bumping expo is the easy part. Run npx expo-doctor, paste the full output, and use this prompt: “We upgraded to Expo SDK 57. Use the expo-upgrade skill. Fix each mismatch with npx expo install --fix, and list every third-party package with no SDK 57 support instead of forcing a version.” Then rebuild the development client, because the old one no longer matches.
  • It works in Expo Go but not in the development build, or the reverse. The cause is almost always a module that is not in Expo Go, or a config plugin that applies only during prebuild. Tell the agent which environment fails and ask it to check whether the module needs a config plugin entry in app.json and a new development build.
  • eas build fails on signing. Paste the build log and ask the agent to name the failing credential, for example an expired distribution certificate or a missing push key. Do not let it regenerate credentials: eas credentials changes shared signing state for the whole team, so a person runs it.
  • An update “succeeds” but no device receives it. The update’s runtime version matches no installed build. Take the latest store build’s ID from eas build:list --platform ios --build-profile production --limit 1 --json and run eas fingerprint:compare --build-id <BUILD_ID>. If the hashes differ, the change needs a store build, and the fingerprint-gated workflow would have produced one.
  • A bad update is already live. Run eas update:revert-update-rollout if it is still in a staged rollout, or eas update:rollback otherwise. Then add a Maestro flow that reproduces the bug, so the next pull request fails before it ships.

Did this guide help you?

One tap, no sign-in.

not at all a lot