You opened Windsurf, asked Cascade to add a quick share sheet to your iOS app, and twenty minutes later the App Privacy Report showed microphone, contacts, and photo library access for a feature that only renders a button. Cascade edited Info.plist along the way, and now you cannot tell which usage descriptions actually correspond to features the app reaches.
Short answer
Cascade infers permission keys from the imports it sees in surrounding code rather than from the API calls the feature wires up. According to Apple's Information Property List reference for NSCameraUsageDescription, every permission you declare must correspond to a real use. The audit is two passes on the compiled IPA: list every NS UsageDescription key in Info.plist, then prove each one matches a call in the binary. Orphans are permissions Cascade added that the shipped app never reaches.
What you should know
- Cascade reads the file it is editing, not the call graph, so an SDK import that never runs can still produce an Info.plist key.
- Windsurf does not surface why a permission was added; the rationale lives in chat memory at
~/.codeium/windsurf/memories/, not in your commit history. - Apple does not auto-reject orphan permissions, but Guideline 5.1.1 makes them a follow-up risk during App Review.
- A compiled IPA is the honest record, because Cascade can remove a feature and leave the Info.plist key behind in the same commit.
- The Privacy Manifest is a separate audit from purpose strings, and Cascade has been observed to skip it entirely.
Why does Cascade add permissions you never asked for?
The honest answer is that Cascade's agent loop is rewarded for making the next build run, not for producing a minimal manifest. When you ask it to wire up a screen that imports expo-image-picker, the planner notices that the library throws at runtime without NSPhotoLibraryUsageDescription and adds the key before you reach the test step. Cascade has no live read on whether the resulting code path is ever invoked from a user-visible button.
Two patterns repeat in real audits. First, the planner edits Info.plist as part of a multi-file change and never flags the edit in the chat header, so the diff scrolls past the developer at review time. Reports of Cascade hallucinating imports, described in the Windsurf Cascade documentation, trace to the same mechanism: the agent infers what the file should contain from training data, then writes it. The hallucinated import gets caught by the compiler; the hallucinated permission does not.
Second, Cascade's memory system at ~/.codeium/windsurf/memories/ carries the prior decision forward. Per the Windsurf memories documentation, Cascade auto-generates memories that influence later prompts. Once a memory entry says the project requests microphone access, a later refactor that removes the audio feature often leaves the Info.plist key in place because the memory still flags it as relevant.
Where do Windsurf-added permissions hide in an iOS build?
Four places, in order of how often the audit catches an orphan.
Info.plist carries the user-facing usage description keys: NSCameraUsageDescription, NSMicrophoneUsageDescription, NSLocationWhenInUseUsageDescription, NSContactsUsageDescription, NSPhotoLibraryUsageDescription, NSBluetoothAlwaysUsageDescription, and NSCalendarsUsageDescription. Per Apple's NSCameraUsageDescription reference, each key triggers a runtime permission prompt the first time the matching API is reached. A key with no matching call is an orphan.
PrivacyInfo.xcprivacy declares the required-reason API categories: file timestamp, system boot time, disk space, active keyboards, and user defaults. Cascade rarely writes this file when it adds an Info.plist key, which means a build can declare camera access in Info.plist and still fail Apple's upload check for a separate required-reason call elsewhere in the binary.
Entitlements.plist carries capability requests: aps-environment for push, com.apple.developer.associated-domains for universal links, com.apple.developer.in-app-payments for Apple Pay. Cascade has been observed to enable App Groups for a single shared-defaults call and then leave the entitlement in place after the call is removed.
Third-party SDK manifests merge into the final report App Store Connect shows. An entry under a category you never wrote often traces back to a Pod or Swift Package Cascade added during the session, not to your own code.
How do you audit a compiled IPA for orphan permissions Cascade added?
Five commands, run against the build you ship to TestFlight, not the dev build. The compiled artifact is the only record Cascade cannot rewrite.
| Step | Command | What it shows |
|---|---|---|
| 1. Extract the IPA | unzip MyApp.ipa -d ipa/ | The Payload/MyApp.app/ bundle with Info.plist and binary. |
| 2. List Info.plist keys | plutil -p ipa/Payload/MyApp.app/Info.plist | Every NS*UsageDescription key the build declares. |
| 3. List Privacy Manifest entries | plutil -p ipa/Payload/MyApp.app/PrivacyInfo.xcprivacy | Required-reason API categories declared. |
| 4. Inspect linked frameworks | otool -L ipa/Payload/MyApp.app/MyApp | Frameworks (AVFoundation, CoreLocation, Contacts) actually linked. |
| 5. Grep symbols against the binary | `nm ipa/Payload/MyApp.app/MyApp | grep -iE 'AVCapture | CLLocation |
The artifact you keep is the diff between step 2 and step 5. If plutil returned NSContactsUsageDescription but nm shows no CNContactStore or ABAddressBook symbol, the key is an orphan. Per OWASP MASTG-TEST-0069 on testing app permissions, the test procedure for iOS app permissions cross-references purpose strings against the actual API calls in the binary, which is the same comparison the five steps automate.
For builders shipping from FlutterFlow or a wrapper project where Cascade only edits the outer codebase, the IPA-side pass still applies. The wrapper compiler emits the final Info.plist, and plutil reads it the same way regardless of whether the upstream source was TypeScript, Dart, or visual nodes. PTKD.com (https://ptkd.com) automates this pass against an uploaded IPA, returns the orphan list, and maps each declared key to whether the binary reaches the matching framework. For teams that switched to Windsurf for the speed and now want a second opinion on the build before pressing Submit, it is the path that avoids setting up the Apple toolchain on a CI runner.
How do you stop Cascade from adding permissions in the first place?
Three controls, in order of how much they reduce future drift.
Add a project-level rule. Per the Windsurf Cascade documentation, Cascade respects workspace rules that constrain its planner. A rule like "never edit Info.plist or PrivacyInfo.xcprivacy without a chat-visible note explaining which call required it" puts the planner in a position where the orphan becomes visible at review time. The rule does not stop the edit; it forces the rationale into the diff window where you can catch it.
Add Info.plist and PrivacyInfo.xcprivacy to .codeiumignore. The same documentation describes .codeiumignore at the workspace root as the way to prevent Cascade from viewing, editing, or creating files in given paths. For an app where you want to own the manifest by hand, this is the cleanest cut. The trade-off is that an SDK that genuinely requires a new key now needs a manual edit, which is the right cost when the alternative is silent drift.
Review the Cascade diff window before accepting. The official guidance is that every change appears in a diff view before commit. In practice, manifest edits scroll past during a large multi-file change. A simple habit (search for UsageDescription in the diff before clicking Accept All) catches most of the orphans the agent introduces.
Does Windsurf touch the Privacy Manifest the same way?
The Privacy Manifest is a separate file with separate failure modes. Cascade rarely edits PrivacyInfo.xcprivacy, because the file is newer than most of the agent's training data, which is why an audit on a Windsurf-built app often finds an Info.plist with seven usage descriptions and a Privacy Manifest with zero entries. A build that reads UserDefaults.standard.set and ships with no Privacy Manifest entry for NSPrivacyAccessedAPICategoryUserDefaults is rejected with ITMS-91053 at upload time, regardless of how clean the Info.plist looks.
The practical implication is that two checklists run in parallel. The Info.plist audit catches orphans Cascade added. The Privacy Manifest audit catches calls Cascade made (often through an imported SDK) that have no matching declaration. The first is a Guideline 5.1.1 risk; the second is a deterministic upload rejection. Treating the two as one audit is the most common reason a Cascade-built app still fails at upload after the developer thought the manifest was clean.
What to watch out for
Four patterns recur in audits of Cascade-built iOS apps.
First, Cascade has been observed to copy the entire purpose string from a public tutorial verbatim. A literal "This app needs camera access to take photos" in a fintech app that does identity capture reads to App Review like template text, and a follow-up question about the actual use is common. Replace SDK-default strings before submission.
Second, removing a feature in Cascade rarely removes the matching Info.plist key. The planner that added the key during the original prompt is not re-run when you delete the screen, so the entry persists. Per the Windsurf common issues documentation, planner state and file state can drift, and Info.plist is one of the files where the drift is hard to spot.
Third, Cascade's memory layer can re-add a permission you just removed. A subsequent prompt that mentions the camera feature can prompt the planner to re-introduce NSCameraUsageDescription based on a memory entry from an earlier session. Clearing the relevant memory at ~/.codeium/windsurf/memories/ is the only durable fix once the entry exists.
Fourth, the Privacy Manifest is not a substitute for Info.plist. An app that declares NSPrivacyAccessedAPICategoryFileTimestamp still needs NSCameraUsageDescription if it reaches the camera. The two files cover different surfaces, and one does not absolve the other.
Key takeaways
- A permission Cascade added based on an import is not a permission the user feature actually reaches. Audit the compiled IPA, not the Windsurf project tree.
.codeiumignoreand a project-level Windsurf rule are the two controls that reduce future drift; everything else is reactive.- The Info.plist audit (orphan purpose strings) and the Privacy Manifest audit (missing required-reason entries) are different checks, and a Windsurf-built app usually needs both.
- Cascade's memory layer at
~/.codeium/windsurf/memories/can re-add permissions across sessions, so a one-shot fix in Info.plist is not durable until the memory entry is cleared. - Some teams hand the pre-submission IPA scan to platforms like PTKD.com (https://ptkd.com) when the Windsurf codebase no longer matches the binary it produced.


