Screenmap runs it in CI, screenshots the screens your change touched, and posts them to the pull request.
Screenmap works out which screens your change can reach, captures them on a simulator or an emulator, and posts what it found. The comment shows the part of the screen that changed. The link opens the full map.
Click any screen for its route and the file it comes from.
Screenmap shows you the old and the new version, with the parts that moved marked.
Every screen is there, with the ones your pull request touched highlighted, so you can see where the change sits.
/water new
/compare changed
/grind removed /water | new route on this branch |
/compare | its own source changed (compare.tsx) |
/grind | route gone from this branch |
Open screenmap viewer for this PR →
Before and after
1 new route · 1 route gone
How these were captured
/settings/beta-features new
/settings bottom state
/settings/account unaffected
/profile/:name unaffected /settings/beta-features | New screen: 'Enable beta features' toggle, an instability info banner, a Share feedback button, and an empty state ('No beta features at the moment'). |
/settings | New 'Beta features' row appears between Languages and Help. |
/settings/account | Only a lingui i18n API refactor (msg/useLingui → t-macro) — no visual change. |
/profile/:name | Implicated via Navigation.tsx, which only registers the new BetaFeaturesSettings screen — nothing rendered on Profile changes. |
Open screenmap viewer for this PR →
Before and after
1 new route
How these were captured
Everything happens in your GitHub Actions. Bundles live on a branch in your repo, and the viewer opens them in your browser.
MIT licensed. You pay for CI minutes and your agent, nothing else.
iOS captures on a macOS runner, Android on ubuntu-latest, which bills at
about a tenth of the macOS rate. In CI it is one platform per job; the two bundles
fold into one map, and the viewer gets an iOS / Android switcher.
Claude Code by default, with presets for Codex, Gemini and OpenCode. For anything else, give Screenmap a command and the name of its API key variable.
Hosted simulators from argent cloud and Expo, so you can run the iOS side without a macOS runner of your own.
Set up Screenmap in this repository.
Screenmap is a GitHub Action that maps every screen of an Expo / React Native app
and comments on each pull request with screenshots of the screens that change
touched. Source: https://github.com/aleqsio/screenmap
Work through this in order.
1. Confirm this repo is an Expo / React Native app using expo-router or a
react-navigation route map. Find the app directory: the repo root, or
something like apps/mobile in a monorepo. Call that PROJECT.
2. Set up the EAS side. Check the app config for extra.eas.projectId — the EAS
lane shells out to eas fingerprint:generate and eas build, and both refuse to
run against an unlinked project. If it is missing, run "npx eas init".
Then look at eas.json for a build profile that produces a dev client for the
platform you want. iOS needs developmentClient: true and ios.simulator: true,
and Screenmap defaults to a profile named "development-simulator". Android
needs developmentClient: true and android.buildType: "apk" — an .aab cannot be
installed on an emulator — and defaults to "development-emulator". If yours
are named something else, pass eas_profile with the real name. If no such
profile exists, add one. If this repo already builds its own dev client, you
can skip EAS entirely: pass app_path pointing at it — an .app for iOS, an .apk
for Android — instead of expo_token.
3. Create .github/workflows/screenmap-pr.yml with exactly this:
name: screenmap · PR changes
on:
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: screenmap-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: write # push bundles to the screenmaps branch
pull-requests: write # the sticky comment
actions: write # start the baseline if no map exists yet
jobs:
changes:
runs-on: macos-26
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: aleqsio/screenmap@v1
with:
mode: pr
expo_token: ${{ secrets.EXPO_TOKEN }} # EAS builds or reuses the dev client
# app_path: build/MyApp.app # ...or your own simulator build, with no expo_token
# platform: ios # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
agent_provider: claude
agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
4. Create .github/workflows/screenmap-baseline.yml with exactly this:
name: screenmap · baseline
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: screenmap-baseline
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
actions: write # re-run PRs waiting on this map
jobs:
baseline:
runs-on: macos-26
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: aleqsio/screenmap@v1
with:
mode: baseline
expo_token: ${{ secrets.EXPO_TOKEN }} # EAS builds or reuses the dev client
# app_path: build/MyApp.app # ...or your own simulator build, with no expo_token
# platform: ios # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
agent_provider: claude
agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
flows_pr: "true"
5. Adapt both workflows to this repo:
- If PROJECT is not the repo root, add "project: PROJECT" under "with:".
- If this repo is private, add 'publish: "false"' so bundles stay as run
artifacts instead of being pushed to a branch.
- If I asked for Android too, remember it is one platform per job: keep the
iOS job on macOS, add a second with "platform: android" on ubuntu-latest,
and fold the bundles together with "screenmap-ci merge --inputs
ios=<a>.scrmap,android=<b>.scrmap". The workflow templates in the repo show
the shape.
- Set the agent to yourself. Work out which CLI you are running as and put
that in agent_provider in both workflows, then set agent_api_key to the
secret that matches it:
- claude -> ANTHROPIC_API_KEY
- codex -> OPENAI_API_KEY
- gemini -> GEMINI_API_KEY
- opencode -> AGENT_API_KEY
If you are some other agent, use its CLI name and the env var its key
normally lives in, and tell me which one you picked.
6. If any route takes a parameter, write .screenmap/config.json with real values
for them. A route like /brew/[id] is deep-linked as /brew/1, and if 1 is not
a real id the run captures the not-found screen and reports it as that
screen. Use ids that exist in the app's fixtures, seed data or test account:
{ "params": { "id": "v60" } }
Plain "id" covers every route with that parameter, and prefixing the route id
("brew/[id].id") scopes it to one route. Anything else the agent needs, like
how to sign in, which controls to leave alone, or how slow a screen is, goes
in .screenmap/SKILL.md.
7. Do NOT create, print, or paste any secret yourself. Instead, tell me to add
these in Settings -> Secrets and variables -> Actions:
- EXPO_TOKEN (required) — expo.dev -> Account settings -> Access tokens.
- the agent key you named in step 5 (optional). Without it Screenmap still
runs, on the routes it can reach on its own. Tell me where to get that
provider's key.
8. Tell me that an iOS run needs a macOS runner, which bills at ten times the
Linux rate, so one run costs about what a short iOS build costs. An Android
run goes on ubuntu-latest, at about a tenth of that.
When you are done, summarise what you changed and what is left for me to do by
hand. Do not commit or push unless I ask. You need all four of these.
npx eas init, with a build
profile that produces a simulator build, or an apk for Android. Or a
build of your own, if you would rather not use EAS at all.
ubuntu-latest, at about a tenth of that.
In your repo: Settings → Secrets and variables → Actions.
EXPO_TOKEN Required Lets Screenmap fetch or start the build.
expo.dev → Account settings → Access tokens.
ANTHROPIC_API_KEY Optional Lets the agent explore screens with no recorded path. Without it Screenmap runs deterministically, on the routes it can reach on its own.
console.anthropic.com → API keys.
.github/workflows/screenmap-pr.yml
This is the one that comments on pull requests. It captures iOS, which is why the job
runs on macOS. For Android, set platform: android and
runs-on: ubuntu-latest.
name: screenmap · PR changes
on:
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: screenmap-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: write # push bundles to the screenmaps branch
pull-requests: write # the sticky comment
actions: write # start the baseline if no map exists yet
jobs:
changes:
runs-on: macos-26
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: aleqsio/screenmap@v1
with:
mode: pr
expo_token: ${{ secrets.EXPO_TOKEN }} # EAS builds or reuses the dev client
# app_path: build/MyApp.app # ...or your own simulator build, with no expo_token
# platform: ios # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
agent_provider: claude
agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }} .github/workflows/screenmap-baseline.yml
This one keeps the map of main up to date, so pull requests have
something to compare against. When the agent works out how to reach a screen, this is
also the job that opens a pull request adding that path to your repo.
name: screenmap · baseline
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: screenmap-baseline
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
actions: write # re-run PRs waiting on this map
jobs:
baseline:
runs-on: macos-26
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: aleqsio/screenmap@v1
with:
mode: baseline
expo_token: ${{ secrets.EXPO_TOKEN }} # EAS builds or reuses the dev client
# app_path: build/MyApp.app # ...or your own simulator build, with no expo_token
# platform: ios # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
agent_provider: claude
agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
flows_pr: "true" Screenmap comments with the screens your change touched and a link to the full map. The first pull request also builds the map of your default branch, which it does on its own.
Screenmap fingerprints your project and looks for a finished EAS build that matches.
If there is one, it downloads it, so a JavaScript-only pull request never waits on a
rebuild. If there is not, it runs eas build on your usual profile. If you
already produce a build of your own — an .app for iOS, an
.apk for Android — pass app_path instead and Screenmap skips
EAS.
Point it at the app with project: apps/mobile. On a private repo, set
publish: "false" to keep bundles as run artifacts instead of pushing them
to a branch.
Screenmap runs with nothing configured. Two files in .screenmap/ fill in
what only you know about the app.
Deep-linking a route like /brew/[id] needs an id. Screenmap falls back to
1, which usually lands on the not-found screen. Set real values in
params: a key like "brew/[id].id" applies to that one route,
and a bare "id" is the fallback for every route that takes an
id.
SKILL.md is optional. Put in it whatever the agent should know before it
starts tapping: how the test account signs in, which controls to leave alone (delete,
sign out, purchase), how long a slow screen takes to settle.
platforms decides what a local run captures, and each platform has its
own block for the device to use. Both in one list puts every screen in one map with a
switcher. In CI the Action takes one platform per job instead, and the bundles are
merged afterwards. A config written before this still works: a top-level
device means iOS.
{
"effort": "balanced",
"scheme": "myapp",
"platforms": ["ios", "android"],
"ios": { "device": "iPhone 17 Pro" },
"android": { "device": null },
"params": { "brew/[id].id": "v60" },
"suspects": { "depth": 2, "broadCap": 8 },
"waits": {
"transition": 2500,
"network": 6000,
"boot": 15000
},
"agent": {
"enabled": true,
"provider": "claude",
"scan": "params",
"maxScreens": 8
}
} Every key is optional. Start from the config template and the skill template.
Every screen your change touched gets a screenshot at every level, and new screens and new
links between them land in the comment whichever one you pick. effort decides
how many of those screens the agent opens for itself, to confirm the deep link arrived
where it should, find real parameters when it did not, and record a flow that makes the
next run cheaper. Set it as an input on the Action or as "effort" in
config.json. With no key set you get deterministic, which replays
the paths you recorded without spending a token. You record those from
your own terminal, once, and again whenever you add a screen.
| effort | The agent double-checks |
|---|---|
deterministicno key | nothing, and spends no tokens |
fast | the screens no recorded flow can reach |
balanceddefault | also param routes that already have a flow |
thorough | every screen the run captured |
The GitHub Action drives the same Claude Code skill you can run yourself. In your own
terminal it writes a .scrmap file you can open in the viewer.
Captures and flows land in .screenmap/out/. Open the bundle in the viewer, or
keep it as the baseline your next run diffs against.
iOS needs a Mac; Android needs an emulator on adb. One local run can do
both. --static renders the routes on their own if you would rather not
boot a device.
claude plugin marketplace add aleqsio/screenmap
claude plugin install screenmap@screenmap /screenmap # full run, packs a .scrmap
/screenmap --static # routes only, no device
/screenmap --platform both # iOS and Android in one map
/screenmap replay <flow> # replay a recorded flow
/screenmap pr <number> # a PR's changes as .diff.scrmap No. Screenmap finds your screens from the router and records the tap paths itself. You get them as a pull request.
No. It posts a comment and nothing else.
About 12 minutes for a JavaScript-only change. Add an EAS build if your native code changed.
Yes, if you use expo-router or a react-navigation route map. The iOS lane needs a macOS runner; Android runs on ubuntu.
It captures on an Android emulator, and both platforms can land in one map with a switcher in the viewer. In CI it is one platform per job: iOS on macOS, Android on ubuntu, which bills at about a tenth of the rate.
Screen text is read with tesseract instead of Apple Vision, so fewer words come back. The run summary and the PR comment name the backend when it is not Vision.
Storybook shoots the components you wrote stories for. Screenmap shoots your real app, including the screens nobody wrote a story for.
Yes. The demo map runs in your browser.