Appearance
Claude Code operations — driving the apps, proving the device, and the Windows traps
The operating manual's practical knowledge for a session working in this repo, moved here verbatim on 2026-09-23 (QRS-1288). The four procedures with steps are also skills: /build-both-natives, /verify-device-bundle, /pull-design, /promote-to-prod (repo .claude/skills/), plus /run-mobile and /run-web inside the apps.
What this page is for
Everything here is a measured lesson about the boundary between tools — the harness, the shell, the emulator, the preview server. None of it is product knowledge; all of it has cost a session at least once.
Driving either app from a session
Two Claude Code skills exist and are the only way to DRIVE either app from here — both start a real server, drive it with Playwright over stdin, and screenshot:
apps/mobile/.claude/skills/run-mobile/— serves theexpo export -p weboutput on 4180; commands includelogin/logout(seed a signed-in onboarded merchant),theme/os(in-app preference vsprefers-color-scheme, deliberately separate),locale,viewport,check(layout invariants, measured after hydration),probe(theme/contrast coherence),buttons/click-text(by accessible name — agetByTextclick reports OK and does nothing, because every pressable wraps its own label).apps/web/.claude/skills/run-web/— boots the real built SSR server against a local mock of the two public RPCs. The only thing that exercises the/:slug/setu-cardloader,headersexport and template resolver. ⚠/:slugitself is only the 301 hop — see the route note above.- ⚠ Three ports, never crossed: 8080 = the human preview, 4173 = Playwright, 4180 = the run-mobile driver.
- ⚠ Inactive tab screens stay MOUNTED (hidden only by an ancestor
aria-hidden="true"), so a naivebody *scan ordocument.body.innerTextreports the previous screen's controls as findings on the current one — which reads exactly like a click that failed. ⚠ Only the run-mobile driver filters on that attribute (this said "both" until 2026-08-28);e2e/theme-consistency.spec.tsshares the blind spot. - ⚠
probecannot measure text on a gradient — the contrast walk readsbackgroundColoronly, so the brand QR card (alinear-gradientover a transparent colour) reports its heading at 1.05:1 "unreadable" when it is roughly 6:1. Those are skipped and counted; judge them from a screenshot.
Prove the device is running your code
PROVE THE DEVICE IS RUNNING YOUR CODE BEFORE YOU BELIEVE ANY DEVICE RESULT [ENFORCED — 2026-08-01]. A whole session was spent "verifying" an onboarding fix against an app that was silently running the previous day's JS bundle. Every symptom pointed at a real bug — the wizard ran, a server-side flag flipped, but the new saveProfileStep never fired — and the true cause was that Metro never served a bundle at all. adb reverse, force-stop, relaunch, and even pm clear did not dislodge it. Before trusting any on-device observation:
- Check
adb shell pm dump <pkg> | grep lastUpdateTime— if it predates your change, the binary is stale. - Check Metro's own terminal actually logged a bundle build. No
BUNDLE ./...line ⇒ nothing was served, whatever the app is showing you. Metro can sit at "Waiting on http://localhost:8081" forever and look healthy. - A second Metro on port 8081 makes this silent.
expo run:androidprintsPort 8081 is being used by another processand carries on, so the app deep-links to a stale server. Kill straynodeprocesses first (a wedged Metro shows as minutes of accumulated CPU and an unresponsive/status), thenexpo start --clear. - Diagnostic
console.warnthat never appears inlogcatis evidence of a stale bundle, not of dead code — read it that way round before rewriting logic that was already correct.
adb shell input tap does not reliably actuate this app's controls. RN Pressable/PressableScale ignores the synthetic events for buttons while still accepting them for plain text inputs — so taps appear to work (fields fill, language switches) while every CTA silently no-ops, which reads exactly like a broken button. Drive the UI with Maestro (semantic tapOn: "<label>", runs from WSL against emulator-5554) instead; see [[mobile-dev-loop-and-machine-setup]].
Windows shell traps
THREE Windows shell traps that cost real time, all about the boundary between tools [2026-08-18, third added 2026-08-20].(1) Git Bash's /tmp is not Windows Python's /tmp. A heredoc that writes /tmp/x.sql and a Python script that then reads it fail with FileNotFoundError, which reads as "my script is broken" rather than "these two programs disagree about where /tmp is". Use the session scratchpad path for anything that crosses from the shell into another interpreter. (2) Heredocs break on nested quotes, repeatedly. SQL with ''-escaped apostrophes and JS/Python with ''' will terminate a <<'EOF' early or produce unexpected EOF while looking for matching. Three commands died this way in one session. The reliable pattern is to WRITE the script to a file and then run it, which also makes the anchors reviewable and the operation re-runnable. (3) ⚠ MSYS REWRITES AN ENV VAR THAT LOOKS LIKE AN ABSOLUTE PATH, AND THIS IS THE MOST DANGEROUS OF THE THREE [2026-08-20]. QRSETU_WEB_BASE_URL=/app npx expo export -p web does not pass /app; Git Bash converts it and the program receives D:/Program Files/Git/app. Prefix with MSYS_NO_PATHCONV=1. It is the worst of the three because the other two fail, while this one succeeds with a wrong value: had the config not validated its input, the export would have exited 0 and emitted a bundle whose every asset pointed at a Git-install path — green build, dead site. Same family as MSYS_NO_PATHCONV=1 breaking curl -o /dev/null into size_download=0 (which read as a production asset outage and was not one). The generalisable rule: any /-leading value crossing from Git Bash into a non-MSYS program is suspect, and a guard that rejects a malformed value beats a comment asking for a well-formed one.
Git hygiene in this tree
⚠ git add -A IS RISKIER HERE THAN IN MOST REPOS. This tree routinely carries long-lived uncommitted portal work, so add -A sweeps other changes into whatever you are committing. On 2026-08-18 it pulled ~1,600 lines of unrelated strategy documentation into a commit whose message described four RPCs — nothing lost, nothing pushed, but the message then misdescribed half its own diff and both changes became harder to find later. Stage deliberately, or run git status --short and read it before committing.
Where the rest lives
- Gates, their blind spots and the hook inventory: Quality gates.
- The build loops: Android + iOS build & test, Parity verification.
- The Supabase CLI token trap and the three projects:
supabase/CLAUDE.md, Supabase. - Where new knowledge goes: Claude context architecture § 7.