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.envand 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 type | Safe in .env? | Production location | Why |
|---|---|---|---|
| Third-party API key (OpenAI, Stripe secret, AWS access key) | No | Server-side proxy (AWS Lambda, Google Cloud Functions, Cloudflare Worker) | Single shared secret, must never reach the device |
| Per-user OAuth or session token | No | iOS Keychain Services, Android Encrypted Shared Preferences | Issued at runtime, scoped to one user, revocable |
| Supabase or Firebase anon / public key | Yes, as EXPO_PUBLIC_ | Inlined in the bundle, paired with Row Level Security or App Check | Designed to be public; security lives in the policy layer |
| Code signing identities and keystore passwords | No | Build server keychain, CI secret store | Never read by the app; only by the build pipeline |
| Sentry DSN, analytics IDs | Yes | Inlined in the bundle | Public 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
.envto.gitignoremay keep it out of version control, but Cascade still reads any file that exists on disk during indexing unless it appears in.codeiumignoreas 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.
stringsandclass-dumpare 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
.envin a React Native or Expo project as build configuration, not a vault. The bundle ships the values. - Add a
.codeiumignorelisting.env,.env.*,*.pem,*.p8,secrets/, and any platform credentials. Do not rely on.gitignorealone 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-keychainorexpo-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.



