Skip to content
Back to skills

Expo Prebuild And Config Plugins

ASecurity

Keeps ios/ and android/ generated from app.config.ts and tested config plugins. Use when adding a plugin, an Info.plist or AndroidManifest entry, or a prebuild step. Not for native code (native-modules-and-new-architecture).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmenttypescriptgoswiftnodetestinggitapi

Works with

  • 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 expo-prebuild-and-config-plugins --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Expo Prebuild And Config Plugins?

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

Security grade badge for Expo Prebuild And Config Plugins
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-expo-prebuild-and-config-plugins/badge)](https://www.skillsdirectory.com/skills/zakariaf-expo-prebuild-and-config-plugins)

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: expo-prebuild-and-config-plugins
description: Keeps ios/ and android/ generated from app.config.ts and tested config plugins. Use when adding a plugin, an Info.plist or AndroidManifest entry, or a prebuild step. Not for native code (native-modules-and-new-architecture).
---

# Expo prebuild and config plugins

Continuous Native Generation: `ios/` and `android/` are build output of `app.config.ts`, its config
plugins and the installed native packages. Every native change is a config key or one line in the
plugin list; local plugins are typed, idempotent and tested; the generated folders stay out of git
and out of every script's reach; a committed snapshot of the introspected native config makes each
native change visible in review. Two checkers prove it. Leaving this model is a recorded decision.

## Rules that must hold

1. **`app.config.ts` is the one config, and a pure function.** No `app.json`, `app.config.json` or
   second `app.config.*`; it returns the object synchronously, the same environment gives the same
   JSON, it holds no `null`, and it applies no plugin in code. Why: Expo reads `.ts` first and ignores
   the rest; a Promise is refused; the fingerprint hashes the config; `null` reaches the app as `{}`
   (all verified on SDK 57). (`single-config`, `config-evaluates`, `config-deterministic`,
   `config-null`, `config-mods`)
2. **`ios/` and `android/` are generated output.** `.gitignore` (and `.easignore`, which replaces it
   for EAS) ignores `/ios` and `/android`, anchored; git tracks nothing there; no npm script, `scripts/`
   file or CI step writes into them. Why: SDK 57's prebuild deletes them by default, EAS prebuilds
   only when they are absent, and an unanchored `ios/` also hides a local module's Swift code.
   (`native-ignored`, `native-tracked`, `native-ignore-scope`, `easignore-native`, `native-edit`)
3. **A config key before a plugin.** `ios.infoPlist`, `ios.entitlements`, `locales`,
   `android.permissions`/`blockedPermissions`, `ios.deploymentTarget` and expo-build-properties
   (SDK versions, `manifestQueries`, cleartext, frameworks) cover most needs; a plugin only for what
   none covers. Why: keys are validated, introspectable and maintained by Expo across upgrades.
   (`builtin-first`)
4. **Nothing reaches the native projects implicitly.** Every installed package whose plugin prebuild
   applies on its own (`assets/auto-plugins.json`) is listed, with the options the app means, in every
   variant; no plugin is listed twice; a permission a plugin adds whatever its options (expo-file-system's
   storage pair, which every app gets through `expo`) is removed only by `android.blockedPermissions`
   (exact case, `WRITE_EXTERNAL_STORAGE`): block it unless the app needs it (shared-storage access).
   Why: the defaults add push and Sign in with Apple entitlements, permissions and English purpose
   strings (seen in the reference app), and a run-once plugin silently drops a second entry's options
   (verified). (`auto-plugin-listed`, `plugin-duplicate`)
5. **Local plugins live in `plugins/with-<capability>.ts`, listed by path.** Default export (a typed
   `ConfigPlugin<Options | undefined>`, tagged `@public`), Expo's helpers from `'expo/config-plugins.js'`
   on SDK 57 (bare from SDK 58), never `@expo/config-plugins`. Why: `npx expo config` then shows the
   file; the bare specifier fails with `ERR_MODULE_NOT_FOUND` once Node imports the file as an ES
   module (verified). (`plugin-entry`, `plugin-import`)
6. **Typed mods first; text mods merge; dangerous mods are justified.** A text mod edits through
   `CodeGenerator.mergeContents` or behind an `includes` check (inline or in a `plugins/lib/` helper);
   `withDangerousMod` carries `// dangerous-mod: <reason>`, writes files only inside it (or helpers
   it calls), and writes them whole or guards an append. Why: only typed mods are
   introspected; dangerous mods run first and break silently when the SDK template changes.
   (`dangerous-mod`, `text-mod-idempotent`)
7. **Every plugin is idempotent and validates its options.** Applying it twice equals once; a bare
   entry or a bad option throws a message showing the right entry. Why: `prebuild --no-clean` and
   `expo run:*` rerun mods onto existing files (verified: an appending plugin left 1, 2, 3 copies).
8. **Every local plugin has a sibling test that imports it and that Jest runs, through Expo's own mod chain.** `runMod`
   (`test/config-plugins/run-mod.ts`), a fresh config per test, a second run, bad options, the other
   platform untouched; the Jest config's test roots include `plugins/`. Why: Expo ships no plugin
   test harness, the `with*` helpers mutate the config they get, and an unreached test passes silently.
   (`plugin-test`)
9. **One owner per native key, and a deliberate order.** Mods run newest first: the plugin listed
   first in `plugins` writes last and wins a shared key (verified). Two plugins on one file are tested
   together. Why: otherwise a reorder silently changes the app.
10. **Native changes are proven by introspection and reviewed as a snapshot.** `contracts/native-config.json`
    is regenerated with `--write` only after a deliberate change and committed with it. Why: the
    generated folders never appear in a pull request; the snapshot shows a new permission or entitlement
    from any plugin or package. (`native-snapshot`, `native-drift`)
11. **Prebuild is clean in scripts and CI, and before every local build.** Never `--no-clean` in a
    script; `npx expo prebuild --clean` before `expo run:*` or a local release build after any config,
    plugin, package or variant change. Why: `expo run:*` builds an existing folder as it is (verified);
    a layered prebuild depends on earlier runs. (`prebuild-clean`)
12. **Leaving CNG is a recorded decision.** `contracts/native-generation.json` `"mode": "committed"`
    with a `reason` and `decidedOn`, both folders committed, no prebuild in any script; native keys
    then change by hand. Why: committed folders and prebuild-only keys drift apart (expo-doctor reports
    it). (`native-mode`)

## Workflow

1. **Place the change.** Read [references/app-config.md](references/app-config.md) ("Built-in keys
   before plugins", "Auto-applied plugins"). A key covers it: edit `app.config.ts` and go to step 6.
   A new native package: dependency-management installs it, then list its plugin here (rule 4).
2. **First time in a repo:** read [references/cng-and-native-folders.md](references/cng-and-native-folders.md);
   copy `templates/contracts/native-generation.json` to `contracts/`; anchor `/ios` and `/android` in
   `.gitignore`; make sure the Jest config's test roots include `'<rootDir>/plugins/**/'`
   (unit-and-component-tests owns `jest.config.js`); copy `templates/test/config-plugins/run-mod.ts`
   and its test. Then `node ${CLAUDE_SKILL_DIR}/scripts/check-native-snapshot.mjs . --write` and read
   every key of `contracts/native-config.json`: it is the audit of what prebuild writes today.
3. **Write the plugin** when no key covers the need: read
   [references/config-plugins.md](references/config-plugins.md); copy
   `templates/plugins/with-android-optional-features.ts` and its test as the shape (or keep them when
   the app needs optional hardware); [examples/with-gradle-jvm-args.ts](examples/with-gradle-jvm-args.ts)
   shows a gradle.properties mod. Pure transform, option validation, one typed mod per file.
4. **Test it** per [references/testing-and-proof.md](references/testing-and-proof.md) (levels 1 and 2):
   `npx jest --ci --selectProjects unit --runTestsByPath plugins/<file>.test.ts`, and
   `npx jest --listTests | grep plugins/` lists it.
5. **List it** in `app.config.ts` by path, with its options and a comment naming the owning skill, the
   way [examples/app-config-plugin-list.ts](examples/app-config-plugin-list.ts) does. Variant-dependent
   options are computed in the config (app-variants-and-environment owns the variants).
6. **Prove it:** `APP_VARIANT=<v> EXPO_PUBLIC_APP_VARIANT=<v> npx expo config --type introspect --json`
   for each variant; `node ${CLAUDE_SKILL_DIR}/scripts/check-native-snapshot.mjs .` and read every
   `native-drift` line; when each one is intended, rerun with `--write`. Text or dangerous mods also
   get `npx expo prebuild --clean --no-install --platform <p>`, then the same with `--no-clean`, and a
   look at the generated file (the change appears once).
7. **Run the app's gates:** `npm run typecheck`, `npx eslint . --max-warnings 0`, `npx jest --ci`.
8. **Check:** `node ${CLAUDE_SKILL_DIR}/scripts/check-native-generation.mjs .` and the snapshot check.
   Each `FAIL` names the file, the rule and the fix; [references/pitfalls-and-rules.md](references/pitfalls-and-rules.md)
   explains every rule and error text. Fix and rerun until both print `RESULT: PASS`.
9. **Leaving or returning to CNG:** stop and ask the developer first; it changes how every later
   native change is made. Then follow "When to leave CNG" or "Coming back to CNG" in
   [references/cng-and-native-folders.md](references/cng-and-native-folders.md).
10. **Report** like [examples/native-change-report.md](examples/native-change-report.md): what changed,
    the snapshot diff, the proof commands and every `RESULT` line, and what a build still has to show.

## Definition of done

- [ ] `app.config.ts` is the only app config; it evaluates for every variant, twice the same, without `null`.
- [ ] Every native change is a config key or one entry in `plugins`; every installed auto-applied package is listed; no entry twice.
- [ ] Local plugins sit in `plugins/`, listed by path, default-export a typed plugin, import `expo/config-plugins.js` (SDK 57), use typed mods, guard text mods, justify dangerous ones.
- [ ] Each plugin's sibling test passes, applies the plugin twice, covers bad options, and `npx jest --listTests` lists it.
- [ ] Introspection reviewed for each variant; `contracts/native-config.json` regenerated on purpose and committed with the change.
- [ ] Text or dangerous mods: a clean and then a `--no-clean` prebuild show the change once.
- [ ] `.gitignore` ignores `/ios` and `/android` (anchored); git tracks nothing there; no script writes there or runs `--no-clean`; or the committed mode is recorded with its reason.
- [ ] Expected SKIP line only while the app has no Jest config: `SKIP jest.config.js [plugin-test] not yet created (unit-and-component-tests creates it)`. `note:` lines are information, not failures.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-native-snapshot.mjs .` prints `RESULT: PASS`
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-native-generation.mjs .` prints `RESULT: PASS`

## Anti-patterns

- **Opening `ios/` in Xcode to fix a setting, or `sed -i` on `AndroidManifest.xml` in a script.** The
  next prebuild erases it and the build differs by machine. Put it in a key or a plugin.
- **A plugin for something a key does.** `LSApplicationQueriesSchemes` belongs in `ios.infoPlist`,
  Android `<queries>` in expo-build-properties' `manifestQueries`, permissions in `android.permissions`.
- **Leaving an installed package off the plugin list "because it works".** Prebuild applies its plugin
  with defaults: an `aps-environment` entitlement the signing profile lacks, `RECORD_AUDIO` for an app
  that never records, generic English purpose strings App Review may find too vague.
- **`import withX from './plugins/with-x.ts'` and `withX(config)` in `app.config.ts`.** It works and it
  hides the plugin from `npx expo config`; list `'./plugins/with-x.ts'` instead.
- **`'expo/config-plugins'` on SDK 57** (or `'expo/config-plugins.js'` on SDK 58), or
  `'@expo/config-plugins'` at any time.
- **Appending to the manifest, a Gradle file or the Podfile without checking.** Every `--no-clean`
  run adds another copy; replace by identity or use `CodeGenerator.mergeContents`.
- **A shared `const BASE: ExpoConfig` in plugin tests.** The helpers write `config.mods` onto it and
  later tests run earlier tests' mods.
- **`npx expo run:ios` after changing the config, without a clean prebuild.** It builds the old project.
- **`npx expo prebuild --no-clean` in CI or a build script.** The output then depends on the runner's history.
- **`ios/` in `.gitignore` instead of `/ios`.** A local Expo module's native folder is never committed.
- **An `.easignore` that forgets `/ios` and `/android`.** EAS uploads the stale folders and skips prebuild.
- **`--write` on the native snapshot to make the check pass.** Read each drift line first; an unexpected
  permission is the finding.
- **Committing the native folders "for now".** Record the decision with its reason, or keep them generated.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/cng-and-native-folders.md](references/cng-and-native-folders.md) | Why generated folders; what SDK 57's prebuild does (clean default, package.json edits); `.gitignore`/`.easignore`; EAS and `expo run`; local modules; leaving and returning to CNG; timings | Workflow steps 2 and 9 |
| [references/app-config.md](references/app-config.md) | How Expo finds, loads and serializes the config; context; determinism; what ships; `expo config` types; built-in keys before plugins; auto-applied plugins; variants | Workflow step 1 |
| [references/config-plugins.md](references/config-plugins.md) | Resolution and errors, local plugin form, imports per SDK, typed/text/dangerous mods, mod order, idempotence, options, run-once, lifecycle code in modules | Workflow step 3 |
| [references/testing-and-proof.md](references/testing-and-proof.md) | The four proof levels, `runMod` recipes, Jest reach, introspection and the snapshot, prebuild checks, blind spots, CI | Workflow steps 4 and 6 |
| [references/pitfalls-and-rules.md](references/pitfalls-and-rules.md) | Symptoms with causes and fixes, every rule of both checkers, options, config keys, expected lines | Workflow step 8, whenever something fails |
| `templates/plugins/with-android-optional-features.ts` | A complete local plugin: optional Android hardware features (`<uses-feature required="false">`) | Workflow step 3 |
| `templates/plugins/with-android-optional-features.test.ts` | Its tests: through `runMod`, iOS untouched, replace and keep, idempotent, bad options | With the plugin |
| `templates/test/config-plugins/run-mod.ts` | Runs the mods queued for one native file the way prebuild does, on test contents | Workflow step 2 |
| `templates/test/config-plugins/run-mod.test.ts` | Its tests, including mod order and the mutation of the config | With `run-mod.ts` |
| `templates/contracts/native-generation.json` | The native-folder model: `generated` (default) or `committed` with reason and date | Workflow step 2; leaving CNG |
| [examples/app-config-plugin-list.ts](examples/app-config-plugin-list.ts) | The shape of a Tasks app's plugin list, one owner comment per line | Workflow step 5 |
| [examples/with-gradle-jvm-args.ts](examples/with-gradle-jvm-args.ts) | A gradle.properties plugin shape (replace by key) | Workflow step 3 |
| [examples/native-change-report.md](examples/native-change-report.md) | A report with real snapshot drift and proof output | Workflow step 10 |
| `scripts/check-native-generation.mjs` | The CNG checker: config, plugin list, local plugin sources and tests, native folders, scripts, contract | Workflow step 8 |
| `scripts/check-native-snapshot.mjs` | Introspects the native config, compares it with `contracts/native-config.json`; `--write` regenerates it | Workflow steps 2 and 6 |
| `scripts/lib/cng-scan.mjs` | Helpers: ignore-file matcher, config evaluation, plugin entries, Jest reach | Only when changing a checker |
| `scripts/lib/source-scan.mjs` | Dependency-free source lexer (synced from the library; do not edit here) | Only when changing a checker |
| `scripts/check-lib.mjs` | Shared checker library (synced from the library; do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Runs both checkers against the fixtures | After changing a checker, a template or the data |
| `tests/bases/` | The fixture base apps: a Tasks app (with planted decoys added by the suite) and a saved introspection with its snapshot | When adding a fixture |
| `tests/fixtures/` | Overlay cases per checker: good, one bad case per rule, pass and error cases | When adding a rule |
| `assets/auto-plugins.json` | The packages SDK 57's prebuild applies on its own, the exempt ones, observed defaults, and the permissions only `blockedPermissions` removes | When the SDK moves |
| `assets/shared.json` | The shared files this skill syncs in | When adding a shared file |

## Related skills

- `app-variants-and-environment` - `app.config.ts` identities per variant and the `APP_VARIANT` resolver this config calls.
- `dependency-management` - installing the native package before its plugin is listed.
- `native-modules-and-new-architecture` - native code in a local Expo module (Expo's official `expo-module` skill is the API reference).
- `unit-and-component-tests` - the Jest config whose test roots must include `plugins/`.
- `typescript-and-lint-rules` - the Node-code lint block and `.gitignore` for `app.config.ts` and `plugins/`.
- `naming-conventions` - `plugins/with-<capability>.ts` and the `with<Capability>` export.
- `architecture-and-boundaries` - app code never imports `app.config.ts` or `plugins/`.
- `expo-sdk-upgrade` - the SDK move (the import spelling flip, template changes plugins must survive).
- `ios-simulator-build`, `android-emulator-build` - clean prebuild and builds on named devices.
- `eas-build-and-submit` - EAS profiles; EAS runs prebuild when the folders are absent.
- `ota-updates` - runtime versions from the fingerprint, which hashes this config and its plugins.
- `permissions-and-system-prompts`, `privacy-and-store-declarations` - purpose-string wording and the store declarations the native snapshot feeds.
- `expo-project-bootstrap` - a new app's first config and `.gitignore`.

Files in this skill

  • SKILL.md17.6 KB
  • assets/auto-plugins.json3.6 KB
  • assets/shared.json155 B
  • examples/app-config-plugin-list.ts2.3 KB
  • examples/native-change-report.md4.3 KB
  • examples/with-gradle-jvm-args.ts1.8 KB
  • references/app-config.md14.1 KB
  • references/cng-and-native-folders.md10.3 KB
  • references/config-plugins.md10.6 KB
  • references/pitfalls-and-rules.md17.3 KB
  • references/testing-and-proof.md8.9 KB
  • scripts/check-native-generation.mjs65 KB
  • scripts/check-native-snapshot.mjs17 KB
  • scripts/lib/cng-scan.mjs14.9 KB
  • scripts/lib/source-scan.mjs35.1 KB
  • scripts/selftest.mjs1.2 KB
  • templates/contracts/native-generation.json524 B
  • templates/plugins/with-android-optional-features.test.ts3.9 KB
  • templates/plugins/with-android-optional-features.ts3.3 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.