Skip to content
Back to skills

Ios Simulator Build

ASecurity

Builds Release iOS simulator apps (clean prebuild, pinned Xcode, UDID-named simulator) and proves the .app. Use when smoke-testing on the iOS simulator, or xcodebuild, simctl or CocoaPods fails. Not for TestFlight (ios-release-testflight).

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 18, 2026
developmentgoshellreactnodeawstestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill ios-simulator-build --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ios Simulator Build?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ios Simulator Build
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-ios-simulator-build/badge)](https://www.skillsdirectory.com/skills/zakariaf-ios-simulator-build)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ios-simulator-build
description: Builds Release iOS simulator apps (clean prebuild, pinned Xcode, UDID-named simulator) and proves the .app. Use when smoke-testing on the iOS simulator, or xcodebuild, simctl or CocoaPods fails. Not for TestFlight (ios-release-testflight).
---

# iOS simulator build

Turns the Expo app into a Release simulator app with one command, `npm run build:ios:sim -- --variant preview`, and proves what was built: the pinned Xcode made it, the variant is the one asked for (test-only code present in development and preview, absent in production), and the app starts and draws on a simulator this repo owns. No Xcode GUI, no Metro, no signing. Verified on 2026-10-04 with Expo SDK 57 (expo 57.0.26), React Native 0.86.3, Xcode 26.6 and the iOS 26.5 simulator runtime; the Android twin is `android-emulator-build`.

## Rules that must hold

1. **Every simulator build runs through one script and starts from a clean prebuild.** `scripts/build/build-ios-sim.ts` preflights every npm script it will run (exit 2, nothing built), then `npx expo prebuild --platform ios --clean`, the contract's post-prebuild scripts, xcodebuild, install, launch and screenshot. Why: `ios/` is generated (Continuous Native Generation, owned by `expo-prebuild-and-config-plugins`) and a stale `ios/` from another variant builds the wrong identity; a missing audit script must stop the run before the slow, destructive prebuild.
2. **Xcode is selected by version through `DEVELOPER_DIR`, per process; never `xcode-select` or `sudo`.** `scripts/ios/toolchain.ts` pins `XCODE_VERSION` and `IOS_RUNTIME`; `selectXcode()` picks the matching `/Applications/Xcode*.app` and asserts `xcodebuild -version`. Why: a Mac often holds several Xcodes (the newest is the one named `Xcode.app`), and one global switch needs a password and changes every build on the machine.
3. **The variant is exported once, for the whole run.** `variantEnv(variant)` from `src/config/app-variant.ts` sets `APP_VARIANT` = `EXPO_PUBLIC_APP_VARIANT` on the one environment every step gets. Why: the variant enters the binary three times, each in its own process: prebuild (Info.plist), the Constants build phase (`EXConstants.bundle/app.config`) and Metro (the JS bundle); a lost variable silently mixes variants.
4. **Metro's cache is keyed on the public values before any Release build.** `metro.config.js` sets `cacheVersion` from the `EXPO_PUBLIC_*` values (the key and its proof belong to `app-variants-and-environment`). Why: this build runs with `CI=1`, and `@expo/cli` then skips `--reset-cache` in the Release bundling phase, so without the key a production build made after a preview build can ship the preview bundle.
5. **The simulator build is Release, for the simulator SDK, on one UDID, unsigned.** `-configuration Release -sdk iphonesimulator -destination id=<udid> ONLY_ACTIVE_ARCH=YES ARCHS=arm64 CODE_SIGNING_ALLOWED=NO`, DerivedData in `build/ios-sim/dd`. Why: Release embeds the Hermes bundle users run; `expo start`, `expo run:ios` and Debug builds load JS from Metro with a dev menu, and no signing material is ever touched.
6. **Simulators are named `<prefix>-<purpose>`, created once on the pinned runtime and targeted only by UDID.** Only prefixed simulators are shut down, erased or deleted, one at a time, never `all` or `booted`; Maestro runs pass `--device <udid>` and their own driver port; Android tooling passes `adb -s <serial>` and starts emulators with `-avd <name>`. Why: several sessions, agents and CI jobs share a Mac, and an implicit device reaches someone else's.
7. **Every tooling process runs with `EXPO_NO_TELEMETRY=1` and `CI=1`.** Why: no telemetry, and no interactive prompt that would hang an unattended run.
8. **Wait on a signal, never a fixed sleep; then check the screenshot and look at it.** The ready signal is the contract's `readyMarker` file in the app's data container, else two identical screenshots after at least 3 s. Why: a build that "succeeded" can still launch to a black, white or empty screen; only the picture shows it.
9. **The built `.app` proves its variant.** `check-sim-app` reads the artifact: the pinned Xcode (`DTXcode`), the simulator platform, the test-only sentinel (present in development and preview, absent in production), Info.plist and `EXConstants` agreeing on the variant's bundle id, usage strings in every language, the privacy manifest, no ATS exception, and for production no placeholder, no test artefact and the encryption flag. Why: `** BUILD SUCCEEDED **` says nothing about what shipped inside.
10. **A change to variants, Metro, test-only code or the build tooling is proven by a preview build and a production build made back to back, without clearing any cache.** Why: only that order shows a missing cache key or a broken gate.
11. **Human steps are asked for, never worked around.** Installing Xcode, accepting its licence and its first-launch setup need `sudo` and an admin password; the agent stops and asks. Why: a workaround (another Xcode, `xcode-select`) builds with the wrong SDK and hides the problem.
12. **Generated and output folders stay out of git:** `ios/`, `android/`, `build/` and `reports/`. Why: prebuild regenerates the native folders, `build/` holds DerivedData and logs, `reports/` the screenshots.

## Workflow

1. **First time in a repo, or after a setup check fails:** read [references/environment-and-xcode.md](references/environment-and-xcode.md) (toolchain, human steps, Xcode selection, the Xcode 27 move) and [references/build-variants.md](references/build-variants.md) (the three moments, the Metro trap, what proves a variant).
2. **Install the templates that are missing** (line 1 of each names its repo path; fill `__SIMULATOR_PREFIX__` and `__APP_ID__` in the contract):

   | Template | Goes to |
   |---|---|
   | `templates/scripts/ios/toolchain.ts` + test (canonical) | `scripts/ios/` |
   | `templates/scripts/ios/simulators.ts` + test | `scripts/ios/` |
   | `templates/scripts/build/*.ts` (+ tests) | `scripts/build/` |
   | `templates/scripts/e2e/maestro-args.ts` + test | `scripts/e2e/` (e2e-maestro owns it; skip when present) |
   | `templates/contracts/ios-sim-build.json` + `.schema.json` | `contracts/` |
   | `templates/src/config/app-variant.ts` + test (canonical) | `src/config/` (skip when present) |

   Add `"build:ios:sim": "node scripts/build/build-ios-sim.ts"` to `package.json`, check the `.gitignore` lines (rule 12), and run `npm run typecheck`, `npx eslint scripts --max-warnings 0` and `npx jest scripts --ci --maxWorkers=2`. Metro's cache key comes from `app-variants-and-environment`; post-prebuild scripts (a privacy-manifest audit) go into the contract's `postPrebuild`.
3. **Check the setup:** `node ${CLAUDE_SKILL_DIR}/scripts/check-sim-setup.mjs .`. Fix every `FAIL` (each names the file, the rule and the fix) and rerun until `RESULT: PASS`. [references/contract-and-checks.md](references/contract-and-checks.md) explains every rule and contract key.
4. **Build and run:** `npm run build:ios:sim -- --variant preview` (`--help` prints the options; `--sim <purpose>` picks another simulator; `--link <path> --wait-for <testID>` also screenshots a deep-linked screen through a Maestro sub-flow, because `simctl openurl` leaves iOS's "Open in ...?" prompt up). Read [references/simulator-commands.md](references/simulator-commands.md) when a step fails, when you run a step by hand, or before changing the script. The run prints the `.app` path and the screenshot path.
5. **Prove the build:** `node ${CLAUDE_SKILL_DIR}/scripts/check-sim-app.mjs . --app <printed .app path> --variant preview`, then `node ${CLAUDE_SKILL_DIR}/scripts/check-screenshot.mjs reports/ios-sim/`, then open the PNG and look at it (the intended screen, text drawn, no alert, no notification banner, no blank area).
6. **When the change touches variants, `metro.config.js`, test-only code or the build tooling:** run `--variant production` right after the preview build without clearing any cache, and `check-sim-app.mjs` on both. [examples/variant-proof.md](examples/variant-proof.md) shows the commands and the output.
7. **EAS or Android:** an EAS simulator build (`ios.simulator: true`) is checked the same way after extracting its `.app`; the Android emulator build is `android-emulator-build`. Both lanes are in [references/eas-and-android.md](references/eas-and-android.md).
8. **On any failure**, match the text in [references/failures.md](references/failures.md), fix the cause (never the check), rerun the failing step, then the checks. Stop and ask the developer for anything that needs a password.
9. **Report** in plain words: variant, simulator name and model, the `.app` and screenshot paths, what the screenshot shows, every SKIP line, and the three `RESULT` lines. Shut down this session's simulators by UDID at the end.

## Definition of done

- [ ] `npm run build:ios:sim -- --variant <v>` ran from a clean prebuild and printed `ready (...)` with a screenshot path.
- [ ] The screenshot was opened and looked at, and shows the intended screen (not black, white, empty or covered by an alert).
- [ ] For variant, Metro, test-only or tooling changes: a preview and a production build made back to back both pass `check-sim-app.mjs`.
- [ ] Only `<prefix>-<purpose>` simulators were created or touched, each by its UDID; no `xcode-select`, `sudo`, `booted` or `all` anywhere; this session's simulators are shut down.
- [ ] Expected SKIP line only: `SKIP src/test-only.ts [test-code] not yet created (debug-menu-and-test-hooks creates it)` while the app has no test-only gate.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-sim-app.mjs . --app <app> --variant <v>` and `node ${CLAUDE_SKILL_DIR}/scripts/check-screenshot.mjs reports/ios-sim/` print `RESULT: PASS`.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-sim-setup.mjs .` prints `RESULT: PASS`

## Anti-patterns

- **Opening `ios/` in Xcode to fix a build setting.** The next prebuild erases it; put the setting in `app.config.ts` or a config plugin.
- **`sudo xcode-select -s /Applications/Xcode.app` because "xcodebuild picked the wrong one".** Pass `DEVELOPER_DIR` from `selectXcode()`; installing and licensing Xcode is the developer's step.
- **`APP_VARIANT=production npx expo prebuild`, then `xcodebuild` in a fresh shell.** The Constants phase and Metro see no variant; export once and keep one environment.
- **`npx expo start`, `npx expo run:ios` or a Debug build to "check quickly".** Metro, the dev menu and LogBox are not what users run, and `run:ios` builds on whatever Xcode `xcode-select` points at.
- **`sleep 10 && xcrun simctl io <udid> screenshot ...`.** Too short on a cold Mac, wasted time on a warm one; poll the ready signal.
- **`xcrun simctl shutdown all`, `erase all` or any command with `booted`.** Shut down or delete only your own `<prefix>-<purpose>` simulator, by UDID.
- **`eas build:run -p ios --latest` without `--simulator <udid>`.** It installs on the first booted simulator, which may be another session's.
- **Declaring success from `** BUILD SUCCEEDED **`.** Launch, screenshot, check and look.
- **Deleting a post-prebuild script because the preflight stopped the first build.** Install the tooling it runs; the audit is what keeps an undeclared required-reason API out of the build.
- **`npx expo export --clear` or deleting `$TMPDIR/metro-cache` to fix a stale variant.** It hides the missing cache key until the next build; key `cacheVersion` on the public values.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/environment-and-xcode.md](references/environment-and-xcode.md) | Toolchain versions, human steps, Xcode selection, tool environment, the Xcode 27 move with scene support, `.gitignore`, disk space | Workflow step 1, and when Xcode or the Mac changes |
| [references/build-variants.md](references/build-variants.md) | The three variants on a simulator, the three moments, the Metro cache trap under `CI=1`, the test-only sentinel, the evidence `check-sim-app` reads, the back-to-back proof | Workflow steps 1 and 6 |
| [references/simulator-commands.md](references/simulator-commands.md) | Every command of the run with its flags, simulator models and sizes, the ready signal, verified pitfalls, other simctl commands, timings | Workflow step 4, and when running a step by hand |
| [references/contract-and-checks.md](references/contract-and-checks.md) | The contract keys, the `rn-skills.config.json` keys, every checker rule with its reason, when multi-package | Workflow step 3, and when a check fails |
| [references/eas-and-android.md](references/eas-and-android.md) | The EAS simulator lane (profiles, `--local`, `build:run --simulator`) and the Android counterpart of every step | Workflow step 7 |
| [references/failures.md](references/failures.md) | Symptom, cause and fix for environment, prebuild, xcodebuild, variant, simulator and screenshot failures | Workflow step 8, whenever something fails |
| [examples/variant-proof.md](examples/variant-proof.md) | A real preview build of the reference Tasks app: timings, the checker output, a real finding, the production step and the report | Workflow steps 5 and 6 |
| `templates/scripts/ios/toolchain.ts` | The Xcode pin, `selectXcode()`, `toolEnv()`, `xcodeBuildNumber()` (synced from the library; do not edit here) | Workflow step 2 |
| `templates/scripts/ios/toolchain.test.ts` | Its tests (synced; do not edit here) | With the toolchain |
| `templates/scripts/ios/simulators.ts` | `<prefix>-<purpose>` simulators: create, warm up, boot, status bar, install, launch, own-only shutdown and delete | Workflow step 2 |
| `templates/scripts/ios/simulators.test.ts` | Its tests | With the simulators module |
| `templates/scripts/build/build-ios-sim.ts` | The CLI behind `npm run build:ios:sim` | Workflow step 2 |
| `templates/scripts/build/build-steps.ts` | Its side effects: logged steps, workspace, plist values, stale DerivedData, package.json kept clean, the ready poll | Workflow step 2 |
| `templates/scripts/build/sim-build-args.ts` | The command line, parsed with the app's variant resolver | Workflow step 2 |
| `templates/scripts/build/sim-build-config.ts` | The contract reader (valibot), defaults filled | Workflow step 2 |
| `templates/scripts/build/sim-build-plan.ts` | Prebuild and xcodebuild arguments, paths, the preflight, the `--link` Maestro line | Workflow step 2 |
| `templates/scripts/build/derived-data.ts` | Stale DerivedData detection (a copied or moved repo) | Workflow step 2 |
| `templates/scripts/build/ready-signal.ts` | When the launched app counts as ready | Workflow step 2 |
| `templates/scripts/build/` | The build modules above and the tests of the pure ones (`*.test.ts` beside each) | Workflow step 2 |
| `templates/scripts/e2e/maestro-args.ts` | `maestroGlobalArgs`, `freeDriverPort`, `driverPortFor`, `maestroRunLine` (synced from the library; do not edit here) | Workflow step 2, when the repo has none |
| `templates/scripts/e2e/maestro-args.test.ts` | Its tests (synced from the library; do not edit here) | With maestro-args |
| `templates/contracts/ios-sim-build.json` | The contract: simulator prefix, device type, app id, post-prebuild scripts, usage strings, Info.plist keys, ready marker | Workflow step 2 |
| `templates/contracts/ios-sim-build.schema.json` | Its schema, with what each key does and its default | When editing the contract |
| `templates/src/config/app-variant.ts` | The canonical variant resolver (`variantEnv`, `parseAppVariant`; synced from the library; do not edit here) | Workflow step 2, when the repo has none |
| `templates/src/config/app-variant.test.ts` | Its tests (synced; do not edit here) | With the resolver |
| `scripts/check-sim-setup.mjs` | Checks the repo setup: contract, Xcode pin, scene support, build arguments, variant environment, Metro key, npm scripts, device safety, `.gitignore` | Workflow step 3 and the definition of done |
| `scripts/check-sim-app.mjs` | Checks a built `.app` against its variant | Workflow steps 5-7 and the definition of done |
| `scripts/check-screenshot.mjs` | Checks screenshots for the device size, a blank screen and a light system banner | Workflow step 5 |
| `scripts/lib/sim-contract.mjs` | Reads the contract, the Xcode pin and package.json for both repo checkers | Never by hand |
| `scripts/lib/device-safety.mjs` | The `no-xcode-select`, `sim-safety` and `adb-device` rules | Never by hand |
| `scripts/lib/sim-app-sentinels.mjs` | The `test-code` rule: sentinels and the test-only gate | Never by hand |
| `scripts/lib/sim-app-rules.mjs` | Identity, placeholder, ads, usage-string, Info.plist and store rules of `check-sim-app` | Never by hand |
| `scripts/lib/screen-measure.mjs` | Dominant colour and light-banner rows of a decoded screenshot | Never by hand |
| `scripts/lib/plist.mjs` | XML and binary plist reader (synced from the library; do not edit here) | Never by hand |
| `scripts/lib/usage-strings.mjs` | Usage descriptions per language (synced from the library; do not edit here) | Never by hand |
| `scripts/lib/ship-placeholders.mjs` | The placeholder values a ship gate refuses (synced from the library; do not edit here) | Never by hand |
| `scripts/lib/maestro-spawns.mjs` | The `maestro-device` rule (synced from the library; do not edit here) | Never by hand |
| `scripts/check-lib.mjs` | Shared checker library (synced from the library; do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Proves all three checkers pass their good fixtures and catch every planted bug | After changing a checker or a template |
| `assets/shared.json` | The shared files this skill syncs in | When adding a shared file |
| `assets/template-placeholders.json` | Values for the template placeholders in template checks | When a template gains a placeholder |
| `tests/bases/` | The repo, built `.app` and screenshot bases the fixtures assemble from | When adding a fixture |
| `tests/fixtures/` | Overlay cases per checker: good, a bad case per rule, pass and error cases | When adding a rule |

## Related skills

- `app-variants-and-environment` - the variant resolver, the Metro cache key and the exported-bundle proof.
- `expo-prebuild-and-config-plugins` - `app.config.ts` as the single source and never editing `ios/`.
- `debug-menu-and-test-hooks` - the test-only gate, its entry and the sentinel this build looks for.
- `ios-release-testflight` - signed archive, export, validation and upload.
- `eas-build-and-submit` - EAS profiles, credentials and cloud builds.
- `android-emulator-build` - the same proof on an Android emulator.
- `e2e-maestro` - flows, the open-link sub-flow and the screenshot matrix on a preview build.
- `privacy-and-store-declarations` - the privacy-manifest audit a post-prebuild script runs.
- `expo-sdk-upgrade` - moving the Expo SDK or Xcode.
- `troubleshooting-playbook` - failures outside the simulator build.

Files in this skill

  • SKILL.md18.5 KB
  • assets/shared.json1 KB
  • assets/template-placeholders.json79 B
  • examples/variant-proof.md7.4 KB
  • references/build-variants.md10 KB
  • references/contract-and-checks.md12.8 KB
  • references/eas-and-android.md6.8 KB
  • references/environment-and-xcode.md8.3 KB
  • references/failures.md11.6 KB
  • references/simulator-commands.md14.3 KB
  • scripts/check-screenshot.mjs8.3 KB
  • scripts/check-sim-app.mjs14.8 KB
  • scripts/check-sim-setup.mjs16.5 KB
  • scripts/lib/device-safety.mjs13.9 KB
  • scripts/lib/maestro-spawns.mjs9.7 KB
  • scripts/lib/plist.mjs10.5 KB
  • scripts/lib/screen-measure.mjs2.8 KB
  • scripts/lib/ship-placeholders.mjs8 KB
  • scripts/lib/sim-app-rules.mjs8.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.