Security

    How do I find hidden permissions in my Windsurf iOS build?

    Hidden permissions Windsurf Cascade added to Info.plist on an iOS build, declared keys versus actual API calls

    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.

    StepCommandWhat it shows
    1. Extract the IPAunzip MyApp.ipa -d ipa/The Payload/MyApp.app/ bundle with Info.plist and binary.
    2. List Info.plist keysplutil -p ipa/Payload/MyApp.app/Info.plistEvery NS*UsageDescription key the build declares.
    3. List Privacy Manifest entriesplutil -p ipa/Payload/MyApp.app/PrivacyInfo.xcprivacyRequired-reason API categories declared.
    4. Inspect linked frameworksotool -L ipa/Payload/MyApp.app/MyAppFrameworks (AVFoundation, CoreLocation, Contacts) actually linked.
    5. Grep symbols against the binary`nm ipa/Payload/MyApp.app/MyApp | grep -iE 'AVCaptureCLLocation

    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.
    • .codeiumignore and 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.
    • #windsurf
    • #cascade
    • #permissions
    • #ai-coded apps
    • #info-plist
    • #privacy-manifest
    • #ios

    Frequently asked questions

    Why does Cascade add NSCameraUsageDescription when I only asked for a share button?
    Cascade reads the surrounding code, sees an import that touches a camera API in a code path you never call, and adds the matching Info.plist key defensively to keep the build from crashing at runtime. The planner has no live read of whether the user feature ever reaches the API. The fix is to remove the key, rebuild, and let the absence of a crash confirm that the binary does not need it.
    Will Apple reject my Windsurf app for declaring permissions it does not use?
    Apple does not automatically reject orphan declarations, but Guideline 5.1.1 requires that any permission requested be relevant to the feature it supports. Reviewers ask follow-up questions when a declared sensor does not appear in the demo, and a generic SDK-default purpose string raises the chance of the question being asked. Orphans are not a deterministic rejection on iOS, but they slow every App Review pass.
    Can I tell Cascade to stop editing Info.plist on its own?
    Yes. Add Info.plist and PrivacyInfo.xcprivacy to .codeiumignore at the workspace root, which prevents Cascade from viewing, editing, or creating files in those paths. A softer alternative is a project rule that requires Cascade to flag any manifest edit in chat before applying it. Both controls put the decision back where you can review it, instead of letting the planner edit the manifest as part of a larger multi-file change.
    Does the Privacy Manifest catch the orphan permissions Cascade adds?
    No. PrivacyInfo.xcprivacy covers a different surface (required reason APIs like file timestamp, system boot time, disk space, active keyboards, and user defaults) and Apple does not return a diff between Info.plist and the binary. A Windsurf-built app usually needs two parallel audits: Info.plist for orphan purpose strings, and PrivacyInfo.xcprivacy for missing required-reason entries that trigger ITMS-91053 at upload.
    How do I clear a Cascade memory that keeps re-adding a permission?
    Open the Cascade memories pane in Windsurf, or delete the relevant file in ~/.codeium/windsurf/memories/ on disk. The auto-generated memory entry is what carries the prior decision forward across sessions, so a one-shot Info.plist edit will get re-introduced on the next prompt that mentions the same feature. Clearing the memory is the only durable fix once the entry exists.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free