expo/expo-tuft-development
Get an Expo app running on the user's phone from a Tuft machine. Read this at the start of any session where the user asks for an app — including brand-new apps, before scaffolding or picking how to deliver it; assume the deliverable is an installable build on their device, not a web preview, unless they say otherwise, and start that build before feature work so it lands mid-session. Also use for EAS account/Apple/device setup via the Tuft dashboard flow, making development builds with EAS, Expo dev-client iteration, remotely exposing Metro with `tuft host`, generating a dev-client deep link, diagnosing device-only failures with Tuft telemetry, deciding whether a native rebuild is required, and handing a working preview back to the user.
npx skills add https://github.com/expo/tuft-skills --skill expo-tuft-development
You run on a persistent machine with full control over it, and the machine outlives the conversation. Act like it:
tuft host, and point the app at that URL. All the user ever sees is their app working.eas device:list, account from eas whoami, everything about the project from the repository itself; (2) failing that, pick a reasonable default, record it in your next status update, and keep building — a stated default takes the user one message to overturn, whenever they happen to read it. Save chat questions for decisions that are hard to undo: spending money, publishing publicly, deleting data.AGENTS.md files before acting.package.json, app.json/app.config.*, eas.json, the lockfile, and the current Git status.Native development builds are the primary means of distribution. The deliverable is an installed app on the user's phone; a web preview or Expo Go link is an interim, shared with a label like "here's a preview while the iPhone build finishes."
Builds run on EAS servers and take roughly 10–20 minutes, so sequence a session to hide that latency behind your own work instead of adding it after:
eas whoami and eas device:list. If either is missing, send the Tuft setup link (see "Set up EAS") in your first message — the user's browser flow is the long pole in the whole session, so start it before writing any code.eas build --profile development --platform ios (platform from device:list) in the background the moment the native surface is settled.npx expo install <package> for Expo-managed dependencies so versions match the SDK.Every eas command that acts on the user’s account — eas build, eas credentials, eas device:*, eas submit, eas update — needs EAS CLI on this machine to be logged in.
eas whoami
Success prints the account; failure means the machine is not logged in.
https://dash.tuft.dev/expo/setup
The flow runs entirely in their browser and walks through three steps against this machine: sign EAS CLI in with their Expo account, connect Apple Developer, and register their iPhone. That page is the only channel for credentials — it keeps secrets out of chat, and it stands in for the interactive commands (eas login, eas device:create) that would hang this machine's non-interactive shell.
timeout 300 bash -c 'until eas whoami >/dev/null 2>&1; do sleep 10; done'
Exit 0 means the login landed; exit 124 means the user has not finished — follow up in chat rather than looping forever.
eas device:list
If it is empty, point the user back at the same setup link — the last step registers their iPhone by UDID.
Setup state is normal EAS CLI state on this machine; a user who prefers the terminal can run eas login locally and the checks above observe it identically.
eas.json has a development profile; create a minimal one if missing: {
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
}
}
}
eas build --profile development --platform ios
(Android: --platform android.) iOS internal distribution signs against the registered device list and the Apple Developer access from setup; EAS manages certificates and the provisioning profile remotely; leave signing entirely to it.
eas build:list). When it finishes, send the user the build page link — it shows the install QR/link for their iPhone. scripts/run-metro.sh <project-dir> <stable-project-name>
The ordering it encodes: publish the port through tuft host first, because Metro embeds EXPO_PACKAGER_PROXY_URL in every manifest and bundle URL it serves — the public URL has to exist before Metro starts. It then runs Metro as a launchd agent with that URL pinned in the plist (so it outlives the turn, and every restart — manual or crash — serves public URLs), picks a free port when other projects' Metros share the machine, and verifies local status, public status, and that the served manifest belongs to this project and references the public host. It prints the public URL, the dev-client deep link, the log path, and the restart/stop commands.
EXPO_PUBLIC_*) lives in the project’s env files, which Metro reads on every restart.launchctl kickstart -k and re-verify before resending the same stable link. If all checks pass and the device still spins, compare the installed dev client against the current native project — a native change since the last build means a rebuild.Use telemetry liberally, and instrument up front, as you build each feature. Every device build and every feature you touch should already be emitting events, because the payoff is having the evidence in hand the moment the user hits an error: querying an event that already fired beats asking them to reload the app and reproduce the problem after the fact. Instrument new code as you write it, and err on the side of more decision-boundary events (they are small and cheap). Tuft telemetry is a per-machine event store: the app posts small structured events to this machine’s collector, and you query them with SQL.
setup_expo_telemetry MCP tool, passing the project’s absolute path. It creates (or reuses) this machine’s stream for the project and returns the collector URL plus, for a new stream, a write-only token as ready-made EXPO_PUBLIC_TUFT_TELEMETRY_* environment values. Pass rotate: true only to deliberately revoke and reissue the token..env.local). That file is the only place the token — like any credential — ever appears.tuft-telemetry npm package and initialize it once near the app root with those env values.kind (event or marker), level, name, optional route, trace_id/span_id for grouping a flow, and a payload of attributes:search_telemetry MCP tool — one bounded read-only SQLite statement over streams(id, name, project_identity, created_at, last_seen_at) and events(id, stream_id, event_id, received_at, occurred_at, launch_id, sequence, kind, level, name, trace_id, span_id, route, update_id, schema_version, payload). Patterns that cover most debugging: -- Most recent events for a stream
SELECT occurred_at, level, name, payload FROM events
WHERE stream_id = '…' ORDER BY id DESC LIMIT 100;
-- Recent errors across streams
SELECT occurred_at, name, payload FROM events
WHERE level = 'error' ORDER BY id DESC LIMIT 50;
-- One flow, in order
SELECT occurred_at, name, payload FROM events
WHERE trace_id = '…' ORDER BY sequence;
-- A user-reported marker code
SELECT occurred_at, payload FROM events
WHERE kind = 'marker'
AND json_extract(payload, '$.attributes.reportCode') = 'ABC123'
ORDER BY id DESC LIMIT 1;
npx tuft-telemetry wait <event-name> --timeout 30s
It exits the moment the new event arrives (timeout exits 124), so you confirm the exact event fired rather than assuming.
Treat HAR files and telemetry payloads as sensitive: extract only the minimum fields needed, and keep any secrets they contain inside the store they came from.
Follow this loop:
For third-party authentication:
For map/data-provider work:
At minimum, run:
npm run typecheck
npm test
curl -fsS --max-time 5 http://127.0.0.1:8081/status
Use the repository’s equivalent scripts when these names differ. Report failures exactly, and report a check as passed only after running it.
Keep chat updates short:
Every URL you send must be reachable from the user’s device: for anything served from this machine, that is its persistent tuft host URL.
Take expo/expo-tuft-development from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.