Skip to content

Commit c11bb10

Browse files
mikepitreclaude
andcommitted
docs(expo): document the remote session for agents and contributors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3bb522d commit c11bb10

7 files changed

Lines changed: 163 additions & 17 deletions

File tree

‎.claude/skills/README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ relevant.
4545

4646
## Skills in this repo
4747

48-
| Skill | Use it for |
49-
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
50-
| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. |
51-
| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). |
52-
| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, with video and screenshots as evidence. |
48+
| Skill | Use it for |
49+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
50+
| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. |
51+
| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). |
52+
| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, local or borrowed on a CI runner, with video and screenshots as evidence. |

‎.claude/skills/verify-clerk-expo/SKILL.md‎

Lines changed: 40 additions & 6 deletions
Large diffs are not rendered by default.

‎.claude/skills/verify-clerk-expo/references/devices.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ Read this when a local lane fails to lease or boot, or when you need a lane's UD
88
- **Android.** A lane boots the `Clerk_Verify_Pixel` AVD with `-read-only -no-window` on port 5558 + 2n, so its serial is `emulator-5560` or `emulator-5562`. `-read-only` lets two lanes share the AVD and throws away their writes. Any other emulator on a lane port is foreign: `doctor` lists it as `lane-ports` and the CLI never kills it.
99
- The machine holds four iOS and two Android lanes, across all agents. While `--wait <seconds>` waits, the CLI prints one `wait` line naming the lanes in use.
1010
- Before leasing, `up` and `run` release lanes whose claiming process is gone and whose worktree no longer exists, and print a `reap` line for each.
11+
12+
A remote device is not a lane: see [remote.md](remote.md).

‎.claude/skills/verify-clerk-expo/references/freshness.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ Fast Refresh stays on. When you save a JS change while an app from an earlier ru
3333

3434
The check dates a Metro revision it has not seen before by the second it was built. If two edits land within the same second, or an edit is reverted while Metro's watcher is still behind, the check can, rarely, launch a bundle one edit older than `dist`. Touching files cannot force a newer revision, because Metro skips modules whose transform key did not change.
3535

36-
## A standalone build has none of this
36+
## A remote lease has none of this
3737

38-
The `Verify end-to-end tests` workflow sets `VERIFY_LOCAL_BUILD=standalone`, so its devices run a standalone Release app with the JS embedded, with no watch build, no Metro, and none of the three checks.
38+
A remote lease runs a standalone Release build with the JS embedded, so there is no watch build, no Metro, and none of the three checks. Its build key also covers the JS inputs, and a JS edit reaches the app by commit, push, `run`. See [remote.md](remote.md). The `Verify end-to-end tests` workflow sets `VERIFY_LOCAL_BUILD=standalone`, so its local devices run the same kind of app.
3939

4040
## Troubleshooting
4141

@@ -60,4 +60,4 @@ A JS edit does not change it, and a new version of a JS-only package does not ch
6060

6161
A later standalone build with the same fingerprint runs no `expo prebuild`, `xcodebuild`, or Gradle. It builds the workspace packages, exports the bundle from the working tree with `expo export:embed`, compiles it with the test app's `hermesc`, and puts it in a copy of the kept app. The build fails unless the app then holds exactly the bundle it compiled. It builds natively instead, and prints `not reused:` with the reason, when the kept app holds no Hermes bundle, when it runs another Hermes bytecode version, when an Android bundle has image assets, or when the Android SDK has no build-tools 35 or newer to align the APK again. On Android the app is signed with the generated project's debug keystore when the working tree has one and with a new key otherwise, so a device that already holds the app under another key needs it uninstalled first.
6262

63-
A job of the workflow that had to build natively stores the app as a run artifact named `verify-expo-native-<platform>-<fingerprint>` for seven days, and a later job takes it only from a run of the same branch of this repository. When there is none, the job builds natively. Unset, nothing is kept and every standalone build is a native build.
63+
A job of the workflow that had to build natively stores the app as a run artifact named `verify-expo-native-<platform>-<fingerprint>` for seven days, and a later job takes it only from a run of the same branch of this repository. When there is none, the job builds natively. Unset, nothing is kept and every standalone build is a native build. A remote session does not set it.

‎.claude/skills/verify-clerk-expo/references/instances.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ A declaration that breaks a rule the CLI can check from the file stops `run` bef
5656

5757
One team key creates, changes, and deletes the application, and reads its secret key. The key needs four scopes: `applications:read`, `applications:manage`, `applications:delete`, and `application_secret_keys:read`. The CLI reads `CLERK_PLATFORM_API_KEY` first, then the file that `CLERK_PLATFORM_API_KEY_FILE` names. A variable that is set and does not work is an error. With neither variable set, the CLI reads the key's 1Password secret reference from `VERIFY_PLATFORM_KEY_REFERENCE` and asks the 1Password CLI for the key. The 1Password app then asks a person to approve, and a refused or unanswered request is an error.
5858

59-
Before it asks 1Password, the CLI sends one request with no key. In a cloud environment that holds the key as an API credential for `api.clerk.com`, the environment adds the key after the request leaves the machine, so that request succeeds and no key is ever in the session. The output then names the source as `a key attached outside this machine (no key is in this process)`.
59+
Before it asks 1Password, the CLI sends one request with no key. In a cloud environment that holds the key as an API credential for `api.clerk.com`, the environment adds the key after the request leaves the machine, so that request succeeds and no key is ever in the session. The output then names the source as `a key attached outside this machine (no key is in this process)`. [Remote devices](remote.md) has the setup.
6060

6161
`doctor` checks the credential every time and names its source in the `instances` line. `up`, `run`, and a `down` that deletes an application use it. `screen` and `attach` never use it.
6262

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Remote devices
2+
3+
A machine that is not a Mac cannot run an iOS simulator, and a machine with no hardware virtualization cannot run the Android emulator. There the CLI starts a job on a GitHub Actions runner through `.github/workflows/verify-remote.yml`. The job boots a simulator or an emulator, builds the test app, and opens a tunnel, and the CLI drives the device through that tunnel. The job is called a session.
4+
5+
## What the machine needs
6+
7+
- Node 24, at 24.8.0 or newer. On any other Node, the CLI reruns itself under `node@24` through `npx` and prints a `note` line that says so. `npm ci --prefix integration/expo-native` runs once per worktree. The machine needs no Xcode, no Android Studio, and no `pnpm install`, because the runner builds the app.
8+
- A GitHub token in `GH_TOKEN` or `GITHUB_TOKEN`, or a logged-in `gh`. The token must be able to read the repository over REST.
9+
- Permission to dispatch `verify-remote.yml`.
10+
- Permission to `git push` your own branch, if you want to verify a commit that you make on this machine. Without it you can still verify commits that GitHub already has.
11+
- Network access to `api.github.com`, `*.trycloudflare.com`, `api.clerk.com`, and `*.clerk.accounts.dev`.
12+
- A Clerk Platform API credential. In a cloud environment, add an API credential for `api.clerk.com` with path prefix `/v1/platform/`. The environment then adds the key after a request leaves the machine, and no key is ever in the session.
13+
14+
`doctor` checks each of these and prints the fix. `doctor --live` then proves the path end to end, as `SKILL.md` describes under Doctor.
15+
16+
## What a session builds
17+
18+
A session builds a commit that GitHub has, never your working tree. It runs `node src/fixture.ts <platform>` on the runner, which is the same module the local build calls. The result is a standalone app: a Release build with `expo-dev-client` left out and the JS embedded, so the device needs no Metro.
19+
20+
The build key of a local dev client covers the native inputs only. The build key of a standalone app also covers everything the JS bundle is made from: `.npmrc`, `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `tsconfig.json`, `turbo.json`, `packages/clerk-js`, `packages/expo`, `packages/expo-biometrics`, `packages/expo-google-signin`, `packages/react`, `packages/shared`, and `integration/templates/expo-native`. `up` and `run` fail with `BUILD_FAILED` when one of those paths has uncommitted changes or when GitHub does not have HEAD, and the fix is to commit and push.
21+
22+
After you push an edit to one of those paths, `run` asks the same session to build the new commit. The session keeps its generated native project while the native inputs are unchanged, so a JS edit skips `pnpm add`, `expo install`, and `expo prebuild --clean`. Specs run from your working tree, so an edit to a spec needs no commit. A session that already holds the build for your commit is reused as it is.
23+
24+
A session keeps the code under `src/core/` from the commit it started on. When `src/core/MANIFEST` in your checkout differs from the one the session started on, `up` and `run` fail with `NOT_READY`. The fix is to commit and push the change, then `down`, then `up`. The session reads `src/fixture.ts` and `src/platform/session-device.ts` again from each commit it builds. The build key leaves out everything under `.claude/`, so a commit that changes only those two files does not make a held session rebuild. After such a commit, run `down`, then `up`.
25+
26+
## Runners
27+
28+
| Platform | Default label | Device |
29+
| -------- | -------------- | --------------------------------------------------------------------------- |
30+
| iOS | `macos-26` | An `iPhone 17 Pro` on iOS 26.5, which the session creates |
31+
| Android | `ubuntu-24.04` | The `Clerk_Verify_Pixel` AVD, booted by the package's own Android lane code |
32+
33+
Both are GitHub-hosted labels, which cost nothing for a public repository, and `REMOTE_DEVICE` in `src/host.ts` holds them. `--runner <label>` on `up` or `run` uses another label. The Blacksmith labels this repository uses are `blacksmith-6vcpu-macos-26` for iOS and `blacksmith-8vcpu-ubuntu-2204` for Android, and they are faster and billed by the minute. A held session keeps its label, and `--runner` with another label fails until `down`. A remote device is not a lane, so the lane pool and `--wait` do not apply to it.
34+
35+
`IOS_RUNTIME` in `.github/workflows/verify-remote.yml` names the iOS runtime of that simulator, so a new runner image cannot change the iOS version under the specs. On a runner that lacks the runtime, the session fails and lists the runtimes the runner has.
36+
37+
Before the session's runner starts, a job of a few seconds reads the request. That job runs on `ubuntu-latest`, a free GitHub-hosted label, whatever the session's label, and so does the session of `doctor --live`. If a run publishes no tunnel within 20 minutes, `up` fails with `NOT_READY`, says whether the run is still queued, and gives its URL. While a run has not published its tunnel, the CLI prints a `wait` line that names the label it is waiting for and for how long:
38+
39+
```console
40+
wait run <run> has waited 0s, now for a ubuntu-latest runner to read its request
41+
wait run <run> has waited 14s, now for a macos-26 runner
42+
wait run <run> has waited 24s, now for the tunnel on macos-26
43+
```
44+
45+
The app build decides how long `up` takes, because the device boots while the app builds. On a free GitHub-hosted label `up` takes tens of minutes, nearly all of it the build, and a Blacksmith label is several times faster. Run `up` right after you push.
46+
47+
The CLI ends a session that is not ready 40 minutes after it asked for the build, and `up` then fails with `NOT_READY`.
48+
49+
## Session lifetime
50+
51+
A session stops itself after 15 minutes without a call from the CLI, and always 60 minutes after it started. `VERIFY_REMOTE_IDLE_MINUTES` (1 to 120) and `VERIFY_REMOTE_CAP_MINUTES` (2 to 360) change the limits for sessions you start. `down` stops the session at once and waits for the runner job to finish.
52+
53+
After an idle stop, the next `up` or `run` prints a `lost` line that ends in `renewing` and starts a new session. A session with under two minutes left before its cap is replaced the same way, with an `ending` line.
54+
55+
Each checkout has a random id in `.verify/remote/owner`, and its sessions carry that id. When the checkout holds no lease, `up` and `run` end any session with that id that is still running, and `down --stale` does so at any time. The idle stop ends whatever they miss, such as the session of a checkout that was deleted.
56+
57+
## Evidence from a machine that cannot attach
58+
59+
`attach` always tries `gh pr edit --attach` itself first, on every kind of machine, and hands off only when that cannot work. The hand-off sends the evidence of a remote run to the session's runner. It sends the video and the screenshots from the sealed run directory, with a manifest that names the run, the pull request, the device, the commit, and each file's size and SHA-256. The files travel over the session's tunnel, with the session's token, in pieces of 1 MiB. The session job keeps them in one directory and uploads that directory as the artifact `verify-evidence` when the job ends. The artifact holds nothing else of the session.
60+
61+
`.github/workflows/verify-attach.yml` then runs, from the default branch. It downloads the artifact, checks every file against the manifest and against its own limits, and puts the block of the run's platform in the description with `gh pr edit --attach`. It never runs anything from the branch or from the artifact. The workflow did not run the tests, so the line it writes says that the session reported the result, and links the session's run in the Actions tab.
62+
63+
The repository needs this set up once:
64+
65+
1. `verify-remote.yml`, `verify-attach.yml`, and `scripts/verify-attach.mjs` are on the default branch. GitHub starts `verify-attach.yml` only from there, so until then a hand-off succeeds and nothing is published.
66+
2. A machine account has write access to the repository.
67+
3. That account has a fine-grained personal access token that is limited to this repository and has the permission "Pull requests: read and write".
68+
4. The repository has an environment named `verify-evidence`, and its deployment branches are limited to the default branch.
69+
5. The token is the secret `VERIFY_EVIDENCE_TOKEN` of that environment. The repository has no repository secret of that name.
70+
71+
Do not store the token as a repository secret. Anyone who can push a branch can add a workflow to it, and a workflow on any branch can read a repository secret. A secret of an environment that allows only the default branch reaches only jobs that run on the default branch, and the job of `verify-attach.yml` is one.
72+
73+
Without the environment or its secret, the workflow prints a notice and publishes nothing.
74+
75+
The workflow publishes only when all of these hold. Otherwise it prints the reason in its run and publishes nothing.
76+
77+
- The pull request that `--pr` names is open, and its branch is in this repository, not in a fork.
78+
- Its branch is the branch the session was started on.
79+
- The commit the run was made at is one of the pull request's commits.
80+
- The commit that branch had when `up` started the session is one of them too. GitHub lists the first 250 commits of a pull request, and the workflow reads no further.
81+
- The hand-off has at least one file.
82+
- The description has the two evidence comments of the platform once each, or neither.
83+
84+
`attach` checks the same rules before it sends anything, and fails with the reason.
85+
86+
A push after the run does not lose the evidence. The line in the block names the commit the run was made at, and when the pull request has moved on it ends with "The pull request has newer commits." To show the newer commit, `run` again and `attach` again in the same session, which replaces the block. The session builds the new commit, and no new session is needed. After a rewrite of the branch's history that removes either commit, `down` and `up` before the next `run`.
87+
88+
A hand-off holds one video of up to 100 MiB and screenshots of up to 10 MiB each, with at most 50 files and 150 MiB in all. A session keeps one hand-off, and a later `attach` in the same session replaces it. `down` gives a session that holds evidence up to five minutes to upload it before it cancels the run.
89+
90+
The runs of `verify-attach` for one branch take turns, and each one publishes.
91+
92+
The workflow reads the description again just before it writes. An edit that someone saves after that read, and before `gh` has uploaded the files and written the description, is overwritten. That is a few seconds for screenshots and up to a few minutes for a large video. `attach` on a machine whose `gh` can attach has the same window.
93+
94+
If the description has no evidence a few minutes after `down`, read the newest run of `verify-attach` in the Actions tab of the repository. It says what it refused and why.
95+
96+
## Network and proxy
97+
98+
Node ignores `HTTPS_PROXY` unless it is told to honor it, so the CLI decides. It goes through the proxy when the direct path to GitHub does not work, which means that it cannot connect or that GitHub rejects the machine's token on it. In that case the CLI reruns itself with the proxy in effect for itself, e2e, and agent-device. A machine that reaches GitHub directly stays direct. `doctor` prints the choice and the reason in `remote-env`.
99+
100+
A host that the environment blocks shows in `doctor` as `blocked:` and what answered. The fix is to add the host to the allowed domains of the environment that the CLI runs in.
101+
102+
## GitHub access
103+
104+
A session uses GitHub in two ways. REST starts, reads, and ends sessions. `git push` gets a commit that you make to GitHub, so that the session can build it.
105+
106+
Claude Code cloud sessions are one example of a sandbox where the two differ. REST there uses the session user's own access. `git push` needs the Claude GitHub App installed on the repository, and a 403 on push means that it is not. The sandbox's `GH_TOKEN` is a placeholder that only the sandbox's proxy turns into a real credential, which is the case above where GitHub rejects the token on a direct connection.
107+
108+
## The Backend API in a cloud environment
109+
110+
The Backend API calls that seed users use the instance's own secret key on `api.clerk.com`. A cloud API credential with path prefix `/v1/platform/` is attached to Platform API calls only, so those calls keep the instance's own key. A cloud API credential with no path prefix replaces that key on every request to the host, and the call gets 401. `up` and `run` make one such call as soon as they create an application. On a 401 the command fails with `NOT_READY`, and the message names this cause and the fix, which is to set the credential's path prefix. The `clerk-api` check of `doctor` fails the same way while the worktree holds an application.

0 commit comments

Comments
 (0)