Skip to content
Back to skills

Permissions And System Prompts

ASecurity

Builds OS permission asks - on the press, pre-prompt, denied vs blocked, Settings link, purpose strings, Android rules. Use when adding a camera, photo, location or contacts ask, or a dialog misbehaves. Not for ATT (admob-ads-and-consent).

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

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 permissions-and-system-prompts --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Permissions And System Prompts?

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

Security grade badge for Permissions And System Prompts
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-permissions-and-system-prompts/badge)](https://www.skillsdirectory.com/skills/zakariaf-permissions-and-system-prompts)

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: permissions-and-system-prompts
description: Builds OS permission asks - on the press, pre-prompt, denied vs blocked, Settings link, purpose strings, Android rules. Use when adding a camera, photo, location or contacts ask, or a dialog misbehaves. Not for ATT (admob-ads-and-consent).
---

# Permissions and system prompts

Makes every OS permission in an Expo app (camera, microphone, photos, location, contacts, calendar,
motion, notifications, Bluetooth) asked at the moment a person presses the control that needs it,
through one gate that knows the seven states, explains only when the control cannot, respects a
refusal, leads a blocked permission to the Settings app, and re-reads the state on every return to
the foreground; with a translated, specific purpose string in the build and a test for every outcome
on iOS and Android. App Tracking Transparency is `admob-ads-and-consent`'s: it asks through the
ConsentPort, and this skill leaves it alone.

## Rules that must hold

1. **Ask from the press that needs the permission.** A request runs in the press handler of the
   control (`gate.requestAccess(onAllowed)` in the feature's model), never at launch, in the root
   layout or root navigator, in an effect, or at module scope. Why: a dialog without context is
   refused far more often, and on iOS that one refusal is final. (`ask-from-action`)
2. **Every permission is in `contracts/permissions.json`.** One entry per kind: the feature it serves,
   `askedFrom` (the model that asks), platforms, the purpose-string key, `shouldExplainFirst` and the
   sheet copy keys; no code reads a kind the contract lacks, no entry outlives its code. Why: the
   inventory is what the reviewer, the store privacy forms and every other rule check against.
   (`contract`, `undeclared-permission`, `contract-unused`)
3. **Only service adapters touch the OS, and they map every answer to the seven states.** `granted`,
   `limited`, `provisional`, `undetermined`, `denied`, `blocked`, `unavailable`: `canAskAgain` tells
   denied from blocked, `accessPrivileges` and location accuracy give limited, a thrown call answers
   unavailable (never rejects). Why: `status: 'denied'` alone cannot say whether a dialog can still
   appear, and partial access is neither yes nor no. (`request-boundary`, `can-ask-again`,
   `photos-limited`)
4. **Ask through `usePermissionGate`, never a module's permission hook.** The gate reads the state
   fresh at the press, runs one ask at a time and returns the sheet to show; `useCameraPermissions`
   and friends ask with none of that. Why: Android throws on a second request while one is open, and
   a cached state misleads the decision. (`system-hook`)
5. **Explain first only when the control does not explain the need, with exactly one "Continue".**
   The explain sheet opens the system dialog and has no close, cancel, "Not now" or "Allow" button,
   no incentive and no picture of the alert; Android's rationale (after one refusal) has "Continue"
   and "No thanks" (the contract's `explain.dismiss` key is that "No thanks": only the rationale
   uses it, never the explain sheet). Why: Apple rejects pre-alert screens that offer a way around the alert or mimic
   it; Android requires a way out of the rationale. (`asked-from`, `copy-keys`)
6. **Respect a refusal.** After "Don't allow" nothing follows: no sheet, no Settings link, no retry;
   the control says the feature is off and the feature keeps a path without the permission. Why:
   Android's guidance forbids pushing people to Settings after a no, and App Review 5.1.1(iv) asks for
   alternatives.
7. **A blocked permission leads to the Settings app.** On a later press the settings sheet says what
   is off and opens `Linking.openSettings()`; the app never re-requests a dialog that cannot appear.
   Why: without it, a blocked feature is a dead button. (`open-settings`)
8. **The state on screen is re-read on every return to the foreground.** Through the status mirror
   (`useSyncExternalStore` over AppState), never `useState` plus an effect or a stored flag. Why:
   people change permissions in Settings, Android resets unused apps and expires one-time grants, and
   neither OS reliably restarts the app. (`recheck-on-active`)
9. **Every asked kind has its purpose string in the build, specific and translated.** One active
   sentence (what the feature does with it, where the data goes) from the catalogs through
   `sourceText` and `nativeLocales`; plugin options are `false` only for permissions never asked. Why:
   iOS terminates an app that requests without the key, and a plugin's English default is vague in
   every language. (`purpose-string`, `copy-keys`)
10. **Android never blocks a permission the app asks, and asks in steps.** No asked permission in
    `blockedPermissions` or behind an expo-image-picker option set to `false`; background location
    only after foreground, in its own step; notification channels before the notification ask. Why:
    a blocked permission is refused without a dialog on every phone, and Android ignores combined requests.
    (`android-blocked`, `location-foreground-first`)
11. **Ask for the least.** The system photo picker (`launchImageLibraryAsync`) needs no permission;
    add-only photo access to save, approximate before precise, when-in-use before always. Why: every
    permission is a dialog, a store declaration and a reason to say no. (`picker-no-permission`)
12. **Every asking model is tested for every outcome on both platforms.** Through
    `createFakePermissionAccess`: granted, allowed, limited, declined, Android's rationale, blocked,
    unavailable, a change in Settings; E2E flows set permissions explicitly. Why: the outcomes people
    hit most (refused, blocked) are the ones a happy-path test never reaches. (`asked-from`)

## Workflow

1. **Decide the inventory.** Read [references/asking-flow.md](references/asking-flow.md). For each
   permission: which press asks it, whether the control explains the need (`shouldExplainFirst`), what
   works without it, the copy. Prefer a permissionless API first (rule 11,
   [references/platform-behaviour.md](references/platform-behaviour.md) section 4).
2. **Copy the templates** (first line names each repo path): `templates/src/core/permissions/`,
   `templates/src/services/permissions/`, `templates/src/hooks/use-permission-gate.ts`, their tests,
   and `templates/contracts/permissions.json` + `permissions.schema.json`. They import
   `src/core/async/latest-request.ts` (async-safety) and the error-log port (architecture-and-boundaries):
   set those up first. Set `PermissionKind` in `permissions-port.ts` to the kinds you ask; with
   local-notifications or push-notifications, add `notifications` and bind it to their port
   (asking-flow section 12).
3. **Write one adapter per kind** like `examples/src/services/permissions/camera-permission-adapter.ts`
   (`createExpoPermissionAccess` over the module's get/request pair). Classify the module in
   `contracts/architecture.json` `vendorSdks` and wire `permissions` into `Services`
   ([references/config-and-wiring.md](references/config-and-wiring.md) sections 6-7).
4. **Config.** Give the plugin option `sourceText(projectRoot, '<purposeKey>')`, set `false` for
   everything never asked, keep asked permissions out of `blockedPermissions`, list the key in
   `NATIVE_TEXT_KEYS`; regenerate the native snapshot (config-and-wiring sections 2-5).
5. **Copy keys.** Add `purposeKey`, `explain.*` and `settings.*` to every catalog (asking-flow section
   13; the example catalogs show the Tasks camera texts), and fill the contract entry
   (`examples/contracts/permissions.json`).
6. **Ask from the model.** Call `usePermissionGate` in the feature's model hook and
   `gate.requestAccess(onAllowed)` in the press handler (`examples/src/features/task-photo/use-task-photo-model.ts`);
   render the sheet from the app's dialog component (`examples/.../components/permission-sheet-view.tsx`).
7. **Test every outcome** with the fake ([references/testing-permissions.md](references/testing-permissions.md)
   sections 1-3; `use-task-photo-model.test.tsx` is the shape).
8. **Prove it on devices** you name: a fresh install on a simulator and an emulator, Maestro flows with
   explicit `permissions:` (testing-permissions sections 4-7).
9. **Run the checks**: `node ${CLAUDE_SKILL_DIR}/scripts/check-permissions.mjs .`, fix, rerun until
   `RESULT: PASS`. What each rule reads: [references/checker-rules.md](references/checker-rules.md);
   symptoms: [references/pitfalls.md](references/pitfalls.md).

## Definition of done

- [ ] Every permission the app asks is a `contracts/permissions.json` entry whose `askedFrom` model asks
      from a press through `usePermissionGate`; no request in an effect, at module scope or at launch.
- [ ] Only `src/services/**/*-adapter.ts` files import permission functions; answers map through
      `toPermissionState` (canAskAgain, limited); no module permission hook anywhere.
- [ ] The explain sheet (where used) has one "Continue"; Android's rationale has "Continue" and "No
      thanks"; the settings sheet opens `Linking.openSettings()`; nothing follows a refusal.
- [ ] Each asked kind's purpose string is in the production Info.plist (snapshot regenerated), in every
      language; unused plugin permissions are `false`; no asked permission is blocked on Android;
      with a snapshot, `expo-prebuild-and-config-plugins`' native-snapshot check passes (it is current).
- [ ] The asking model's test covers granted, declined, rationale (Android), blocked and unavailable;
      `npx jest --ci` passes; the manual pass of testing-permissions section 7 is done on named devices.
- [ ] A passing run may print only these SKIP lines while the app is being set up:
      `SKIP app.config.ts [purpose-string] not yet created (expo-project-bootstrap creates it)`,
      `SKIP src/i18n/catalogs/en.json [copy-keys] not yet created (i18n-strings-and-catalogs creates it)`.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-permissions.mjs .` prints `RESULT: PASS`.

## Anti-patterns

- **Asking at launch "to get it out of the way".** The person has no reason to say yes yet; ask from
  the press that needs it.
- **A pre-prompt with "Allow" and "Not now".** Apple rejects it; the explain sheet has one "Continue",
  and most controls need no explain sheet at all.
- **`if (!granted) requestPermissionsAsync()` on every press.** Once blocked, the request resolves at
  once and nothing happens; read `canAskAgain` and show the settings sheet.
- **Linking to Settings right after a refusal.** It reads as nagging; wait for the next press.
- **`useCameraPermissions()` in a screen.** It asks without the explain, blocked and re-read rules;
  use the gate.
- **Requesting the photo library to pick one photo.** The system picker needs no permission.
- **`cameraPermission: false` "to remove the English default".** It deletes the key the app needs and,
  in expo-image-picker, blocks Android's `CAMERA`; give it the translated text instead.
- **Treating `limited` as refused**, or approximate location as off. Both are a yes.
- **A permission flag in a store or `useState` read once.** It lies after a change in Settings; use the
  status mirror.
- **Testing only the granted path, or E2E flows that start with a bare `launchApp`.** Maestro grants
  everything by default; script refusals with the fake and `permissions:`.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| `references/asking-flow.md` | The flow: when to ask, the seven states, the decision table, Apple's and Android's sheet rules quoted, refusals, blocked, limited, copy | Workflow step 1, designing a sheet |
| `references/platform-behaviour.md` | iOS and Android per permission, Expo's answers read in source, dialog counts, changes behind the app's back, quirks | Step 1, a platform question |
| `references/config-and-wiring.md` | Plugin options and `false`, blocked permissions, purpose strings in every language, the native snapshot, wiring `Services`, SDK classification, multi-package | Steps 3-4 |
| `references/testing-permissions.md` | The outcome matrix, fake and hook tests, `simctl privacy`, `adb pm`, Maestro `permissions:`, the manual pass | Steps 7-8 |
| `references/checker-rules.md` | Every rule of check-permissions, the contract fields, SKIP and NOT APPLICABLE, limits | A FAIL line, changing the checker |
| `references/pitfalls.md` | Symptom, cause and fix for crashes, missing dialogs, lying states, review and test trouble | Something misbehaves |
| `templates/src/core/permissions/` | `permission-state.ts` (the seven states, `isPermissionAllowed`, `nextAskStep`, pure) and its test | Step 2 |
| `templates/src/services/permissions/` | `permissions-port.ts` (`PermissionAccess`, `PermissionKind`, `PermissionsPort`); `expo-permission-access.ts` (`toPermissionState`, `createExpoPermissionAccess`, never rejects); `system-settings-adapter.ts` (`Linking.openSettings()`); `permission-status-mirror.ts` (the OS mirror that re-reads on the foreground); `fake-permissions.ts` (scripted iOS and Android dialog rules); each with its test | Steps 2-3, 7 |
| `templates/src/hooks/` | `use-permission-gate.ts` (fresh read, one ask at a time, the sheet, respect for a refusal) and its test | Steps 2, 6 |
| `templates/contracts/` | `permissions.json` (the empty contract) and `permissions.schema.json` | Steps 2, 5 |
| `examples/src/services/permissions/camera-permission-adapter.ts` | A per-kind adapter over expo-image-picker (device-only) | Step 3 |
| `examples/src/features/task-photo/` | `use-task-photo-model.ts` (asks the camera from "Take photo") with its outcome tests, `components/permission-sheet-view.tsx` (explain, rationale, settings and unavailable sheets as a pure view, one-button explain sheet) | Steps 6-7 |
| `examples/contracts/permissions.json` | A filled contract entry (camera, with the optional unavailable copy) | Step 5 |
| `examples/src/i18n/catalogs/` | The camera purpose string and sheet copy in `en` and `ar` | Step 5 |
| `scripts/check-permissions.mjs` | The checker (16 rules) | Step 9, definition of done |
| `scripts/lib/` | `permission-scan.mjs` (finds permission uses, effects, module scope, local imports); `source-scan.mjs` (shared lexer, synced, never edited here) | Changing the checker |
| `scripts/selftest.mjs` | Runs the checker against every fixture | After changing the checker |
| `scripts/check-lib.mjs` | Shared checker library (synced, never edited here) | Never by hand |
| `assets/permission-apis.json` | Kinds (iOS keys, Android permissions, plugin options) and the SDK 57 permission functions | Adding a module or kind |
| `assets/shared.json` | The shared files this skill syncs | Syncing |
| `tests/bases/app/` | The base app the fixtures build on | Changing fixtures |
| `tests/fixtures/` | good, pass-*, one bad-* per rule, error-* cases | Changing the checker |

## Related skills

- `admob-ads-and-consent` - App Tracking Transparency through the ConsentPort.
- `async-safety` - the latest-wins helper the status mirror uses.
- `push-notifications`, `local-notifications` - their ports are the `notifications` kind's `PermissionAccess`.
- `i18n-strings-and-catalogs` - purpose strings and sheet copy in every language.
- `expo-prebuild-and-config-plugins` - plugin options, auto-applied plugins, the native snapshot.
- `architecture-and-boundaries` - the composition root and SDK classification.
- `media-images-and-files` - picking and capturing media once the permission allows it.
- `privacy-and-store-declarations` - store answers for every permission the contract lists.
- `ui-component-library` - the dialog or sheet the permission sheets render in.
- `unit-and-component-tests`, `e2e-maestro` - the test setup and flows with explicit permissions.

Files in this skill

  • SKILL.md15.5 KB
  • assets/permission-apis.json7.3 KB
  • assets/shared.json155 B
  • examples/contracts/permissions.json1.2 KB
  • examples/src/features/task-photo/components/permission-sheet-view.tsx4.2 KB
  • examples/src/features/task-photo/use-task-photo-model.test.tsx3 KB
  • examples/src/features/task-photo/use-task-photo-model.ts2.4 KB
  • examples/src/i18n/catalogs/ar.json1.2 KB
  • examples/src/i18n/catalogs/en.json923 B
  • examples/src/services/permissions/camera-permission-adapter.ts1.5 KB
  • references/asking-flow.md15.3 KB
  • references/checker-rules.md9.3 KB
  • references/config-and-wiring.md9.5 KB
  • references/pitfalls.md6.4 KB
  • references/platform-behaviour.md10 KB
  • references/testing-permissions.md7.4 KB
  • scripts/check-permissions.mjs29.9 KB
  • scripts/lib/permission-scan.mjs7.8 KB
  • scripts/lib/source-scan.mjs35.1 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.