Summary
renderUsageDescriptions() writes all eleven NS*UsageDescription keys into Info.plist for every generated project, regardless of the corresponding enable* flags. An app that enables none of them still ships a plist asking for speech recognition, the microphone, the camera, the photo library, location, contacts, calendars, Bluetooth, motion, Face ID and Apple Music.
There is no supported way to remove one. usageDescriptions only replaces a key's text.
Why it matters
Declaring permissions an app does not use, with non-specific purpose strings, is a documented App Review rejection reason. The generated strings are also what iOS shows a person at the permission prompt, so they are a privacy claim the app is making on the developer's behalf:
Bench Review asks for your contacts only in a feature that needs it.
That sentence explains nothing, and the app in question has no contacts feature at all.
Cause
In dist/ios/src/index.js (v0.0.187), the disabled branch falls back to a placeholder instead of omitting the key:
const own = config.usageDescriptions ?? {};
return entries.map(([enabled, key, value]) => {
const custom = own[key]?.trim();
if (custom)
return [key, custom];
if (enabled)
return [key, value];
const linked = LINKED_PURPOSES[key];
return linked
? [key, `${config.appName} asks for ${linked} only in a feature that needs it.`]
: null;
}).filter(entry => entry !== null)The inconsistency is the giveaway: entries has 15 permissions, but LINKED_PURPOSES holds only 11 of them. The four that are absent from the table —
NSLocationAlwaysAndWhenInUseUsageDescription
NSLocalNetworkUsageDescription
NSHealthShareUsageDescription
NSHealthUpdateUsageDescription— hit : null, get filtered, and are correctly omitted when disabled. So the generator already does the right thing, and LINKED_PURPOSES defeats it for the other eleven. That reads like an oversight rather than a deliberate choice: there is no obvious reason Health and Local Network should be omitted when unused while Contacts and Apple Music are not.
Reproduce
Generate any iOS project with none of the permission flags enabled — e.g. a config declaring only camera and filePicker capabilities — then:
grep -c UsageDescription Info.plist # 11Suggested fix
Omit the key when the capability is not enabled and no custom string was supplied:
if (custom)
return [key, custom];
if (enabled)
return [key, value];
return null;LINKED_PURPOSES then has no role in the disabled path. If the generic sentence is meant as a fallback for an enabled capability that lacks specific wording, it belongs in the enabled branch instead.
Workaround
Patching the generated plist post-generation works and survives Xcode Cloud, since restoreShell() rewrites only Sources/*.swift and never touches Info.plist.
Version
craft-native@0.0.187