AI-coded apps

    How do I hide .env keys in React Native when using Windsurf AI?

    A React Native developer reviewing a .codeiumignore file in Windsurf to exclude .env files from Cascade indexing, with the iOS and Android build folders open in the file tree

    The most common shape of this question on Windsurf forums goes like this: Cascade just dropped my OpenAI key from .env into the React Native code it generated, what should I do? The answer has two layers, and they are usually conflated. One layer is preventing Windsurf from reading the .env file into Cascade context at all. The other is keeping React Native from shipping any value to the device that you would mind a user seeing. Fixing only the first one feels safer than it actually is.

    Short answer

    You cannot hide an API key inside a React Native build, because anything imported at build time lands in the JavaScript bundle that ships with the binary. The realistic plan is two steps in parallel. Add a .codeiumignore file so Windsurf Cascade does not index your .env file, and move every long-lived secret behind a server endpoint that the app calls with a per-user token. The first layer protects the codebase from the AI assistant; the second layer protects the secret from the actual user of the app.

    What you should know

    • The .env file in a React Native project is build configuration, not a runtime secret store. Anything Metro inlines lands in the JavaScript bundle inside the APK or IPA.
    • Windsurf indexes your workspace by default. Without a .codeiumignore, Cascade can read .env and reuse those values in completions and chat answers.
    • EXPO_PUBLIC_ prefixed variables are explicitly public. Expo's own documentation states they are visible in plain text in the compiled application.
    • Server-side orchestration is the only real fix for third-party secrets. A serverless function holding the key, called by an authenticated user token, is what the React Native team itself recommends.
    • Per-user tokens belong in Keychain Services on iOS and Encrypted Shared Preferences on Android. Never in AsyncStorage.
    • OWASP flags hardcoded keys as a recognized weakness, tracked as MASWE-0005. Static scanners look for the obvious patterns first.

    Where does the leak actually happen, Windsurf or the React Native bundler?

    Both, in different ways. Windsurf's Cascade agent indexes the workspace for autocomplete and chat. The Windsurf documentation on .codeiumignore confirms that without an explicit ignore rule, source files are indexed and embedded by the service. That includes pasting fragments of .env into chat answers, generating code that contains a literal process.env.OPENAI_API_KEY = "sk-...", or autocompleting a fetch call that bakes in your Supabase service role key.

    The React Native side is a different leak with a different fix. Metro, the React Native bundler, performs build-time string substitution on imports from packages such as react-native-dotenv, react-native-config, and Expo's EXPO_PUBLIC_ variables. The result is that the literal value of the secret sits in the JavaScript bundle inside the IPA or APK. Anyone with apktool, unzip, or a text editor on the decoded bundle can read it. Apple does not check for hardcoded secrets at App Review, and Google Play's static scans pick up only a small subset of obvious patterns.

    The two leaks need two fixes. An ignore file makes Cascade behave at edit time. Server-side architecture is what stops the secret from reaching the user's device at all.

    How do I stop Windsurf from reading my .env into Cascade?

    Drop a .codeiumignore file at the repo root. The syntax matches .gitignore. Per the Windsurf context-awareness docs, files listed in .codeiumignore are not indexed and do not count against the workspace size limit. Enterprise users can place a global file in ~/.codeium/ so the rules apply across every workspace on the machine.

    A minimal .codeiumignore for a React Native or Expo repo looks like this:

    .env
    .env.*
    *.pem
    *.p8
    *.p12
    secrets/
    ios/*.mobileprovision
    android/app/*.keystore
    android/app/google-services.json
    ios/GoogleService-Info.plist
    

    Two cautions. Files listed in .gitignore cannot be edited by Cascade, which is also documented behavior. And the default ignore rules cover hidden files (those starting with a dot), but real projects often end up with env.production, config/secrets.json, or .env.development.local that the dot rule does not catch reliably. Listing them explicitly in .codeiumignore is safer than assuming defaults cover the case.

    For Cursor users the equivalent file is .cursorignore. For Windsurf's rules system, a project-level rule that says never paste values from .env files into generated code, and reference them only through environment lookup, makes Cascade hesitate before inlining a key into a diff.

    Why does .env not hide anything in the compiled React Native bundle?

    The React Native security page is direct about this. React Native's official security documentation states that tools like react-native-dotenv and react-native-config are suitable for environment-specific variables such as API endpoints, not for secrets and API keys. The same page recommends an orchestration layer between the app and the resource, typically a serverless function that holds the secret and forwards the call.

    Expo is even more explicit. The Expo environment variables guide instructs developers not to store sensitive information in EXPO_PUBLIC_ variables because they will be visible in plain text in the compiled application. There is no flag, no build mode, and no production setting that changes this behavior. The prefix is the contract.

    The deeper reason is mechanical. Metro reads the dotenv file at build time, substitutes the values into the JavaScript modules, minifies the result, and writes the output into main.jsbundle on iOS or index.android.bundle on Android. The bundle sits inside the signed app package, which is what gets distributed. A signature does not encrypt the bundle. Anyone can download the IPA from an iOS device with Apple Configurator, or pull the APK off an Android device with adb, and read the bundle as text in a few seconds.

    What should I do with secrets I cannot move server-side?

    Sort the secrets by lifetime and by who they belong to. The table below maps the categories that show up in almost every React Native or Expo project.

    Secret typeSafe in .env?Production locationWhy
    Third-party API key (OpenAI, Stripe secret, AWS access key)NoServer-side proxy (AWS Lambda, Google Cloud Functions, Cloudflare Worker)Single shared secret, must never reach the device
    Per-user OAuth or session tokenNoiOS Keychain Services, Android Encrypted Shared PreferencesIssued at runtime, scoped to one user, revocable
    Supabase or Firebase anon / public keyYes, as EXPO_PUBLIC_Inlined in the bundle, paired with Row Level Security or App CheckDesigned to be public; security lives in the policy layer
    Code signing identities and keystore passwordsNoBuild server keychain, CI secret storeNever read by the app; only by the build pipeline
    Sentry DSN, analytics IDsYesInlined in the bundlePublic by design

    The honest answer for the first row is that there is no library that can keep a static secret safe on a device. Packages such as react-native-keys offer better protection than plain dotenv, by storing values in native code rather than in the JavaScript bundle, but a determined attacker can still extract them through reverse engineering. The React Native docs put it plainly: there is no bulletproof way to handle security on the client.

    For per-user tokens, the right libraries on bare React Native are react-native-keychain for iOS Keychain Services and Android Keystore, and on Expo, expo-secure-store. Both wrap the operating system's encrypted storage, which is what OWASP MASVS-STORAGE-2 calls for. AsyncStorage is plain text on disk and does not satisfy this control.

    How do I keep AI generated code from re-introducing hardcoded keys?

    Three habits matter more than any single tool.

    First, write the rule down. Windsurf supports project-level rules files. A short instruction like never write a literal API key into generated code, always read from environment at runtime, and never quote values from .env files in chat answers sets the expectation before Cascade drafts anything.

    Second, scan every diff before it commits. gitleaks and trufflehog are the two common open-source secret scanners and run as Git pre-commit hooks. They catch the obvious shapes (AWS access keys, Stripe sk_live_, OpenAI sk-, GitHub tokens, Google Cloud service account JSON) before the value reaches the remote.

    Third, scan the compiled artifact before submission. The reason scanning the source code is not enough: AI assistants sometimes paste secrets into native files (AndroidManifest.xml, Info.plist, Gradle build scripts) or into bundled assets, and a scanner that only looks at .js and .ts will miss those. For builders who want an automated read of the compiled APK, AAB, or IPA against OWASP MASVS controls, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning of AI-coded mobile builds, including hardcoded credentials and missing privacy manifests.

    What to watch out for

    • .gitignore is not .codeiumignore. Adding .env to .gitignore may keep it out of version control, but Cascade still reads any file that exists on disk during indexing unless it appears in .codeiumignore as well.
    • Source maps leak everything. If you upload source maps to Sentry, Bugsnag, or Crashlytics, the inlined values are visible to anyone with access to those dashboards.
    • AsyncStorage is plain text. Storing access tokens there fails OWASP MASVS-STORAGE-2 and is trivially readable on a rooted or jailbroken device.
    • Putting the key in native code is not a fix. Java strings in an APK and Objective-C strings in an IPA both survive in the binary. strings and class-dump are still the first reverse-engineering tools anyone reaches for.
    • Server-side proxies need their own auth. A Lambda that forwards calls to OpenAI without checking who is calling becomes a free OpenAI gateway for anyone who finds the URL.

    Key takeaways

    • Treat .env in a React Native or Expo project as build configuration, not a vault. The bundle ships the values.
    • Add a .codeiumignore listing .env, .env.*, *.pem, *.p8, secrets/, and any platform credentials. Do not rely on .gitignore alone for AI indexing rules.
    • A server-side proxy is the only realistic way to keep a third-party API key off the device. The React Native team recommends this directly on their security page.
    • Use Keychain Services on iOS and Encrypted Shared Preferences on Android for short-lived per-user tokens, via react-native-keychain or expo-secure-store.
    • For an external read of what a compiled build actually contains, some teams scan the APK or IPA before submission using platforms such as PTKD.com (https://ptkd.com), aligned to OWASP MASVS and MASWE control IDs.
    • #windsurf
    • #react-native
    • #env-files
    • #secrets
    • #ai-coding
    • #cascade
    • #expo

    Frequently asked questions

    Will Windsurf still read my .env file if I add it to .gitignore but not .codeiumignore?
    Yes. Cascade indexes any file present on disk unless a .codeiumignore rule matches it, regardless of Git status. The Windsurf documentation does mention that .gitignore paths are skipped by default, but real-world repos often have .env.local, .env.production, or secrets.json that slip past the default rules. Listing them explicitly in .codeiumignore is the safe answer.
    Is react-native-keys actually secure or just obfuscation?
    It is stronger than plain react-native-dotenv because the values live in native code rather than the JavaScript bundle, but the React Native team's own security page is clear that no client-side library can keep a static secret safe from a determined reverse engineer. Treat it as one more layer for low-value keys; it does not replace a server-side proxy for high-value secrets like OpenAI or Stripe.
    Do I need a backend if my app only talks to Supabase or Firebase?
    Not for the public anon key, which is designed to ship in the client. The thing you do need is Row Level Security policies on Supabase, or Security Rules and App Check on Firebase, because the client key alone offers no protection. If you also use a service role key or admin SDK, that one must live on a server function and never enter the React Native bundle.
    What happens if Windsurf accidentally commits .env values into my repo?
    Rotate every key in that file immediately. Removing the commit and force-pushing does not erase it from forks, GitHub's API, or scanner indexes that already crawled the public repo. Then add .env* to both .gitignore and .codeiumignore, and run gitleaks detect over the full history to confirm no other secrets slipped in. Rotation is faster than trying to scrub Git history.
    Can EAS Secrets in Expo solve this entirely?
    Partially. EAS Secrets keeps values out of your repository and out of Cascade's view by injecting them only at build time, which removes the AI exposure. The catch is that any EXPO_PUBLIC_ variable still ends up inlined into the JavaScript bundle even when sourced from EAS Secrets. Non-public secrets from EAS are useful for native build steps but should not be referenced in client-side React code.

    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