The reader who asked this question opened Cursor, asked the model to wire up a Stripe checkout endpoint, and watched it paste a live secret key directly into the route file. The question is whether that is normal Cursor behaviour, why the model does it, and what to change so the same prompt produces safe output next time.
Short answer
Yes. Cursor and other AI coding assistants regularly emit code with Stripe secret keys placed directly inline, because the public code they were trained on does the same thing. This is not a privacy problem inside Cursor, it is a pattern problem in the model's output. The fixes are project rules, a .cursorignore for .env files, Restricted API Keys, pre-commit secret scanning, and rotating any key the model has touched.
What you should know
- AI coding models learn from public examples. Tutorial code, blog posts, and Stack Overflow snippets routinely hardcode Stripe keys for brevity, so the model treats inline keys as the default shape.
- The model has no concept of "this string is sensitive". It produces the most statistically likely completion at the cursor position, including a key-shaped literal where the training data showed one.
- Stripe forbids hardcoding in its official guidance. The Stripe Keys page tells developers to remove hardcoded keys before going to live mode and to use environment variables or a secrets vault.
- AI-assisted commits leak secrets at a higher rate than the human baseline. GitGuardian's annual analysis of public GitHub commits has reported double-digit-million new exposures, with AI-assisted repositories disproportionately represented.
- Project-level instructions change behaviour. A
.cursorrulesfile with explicit never-hardcode-secrets guidance shifts the model toward environment variables for the rest of the session. - Rotation is the only safe response after exposure. Any Stripe key that has appeared in a repository, a chat transcript, or a remote build log must be rolled in the Stripe dashboard immediately.
Why does Cursor write Stripe secrets directly into the code?
The short answer is that the model is reproducing the patterns it was trained on. Large language models behind Cursor, Claude Code, and GitHub Copilot were trained on enormous quantities of public source: GitHub repositories, blog posts, tutorials, and Q&A sites. Inside that corpus, the example pattern for calling Stripe is almost always const stripe = new Stripe("sk_test_...") with the key written inline. The model has no separate signal that the key is sensitive, no semantic awareness that the string should come from an environment variable. When the autocomplete fires after new Stripe(, the most likely completion is the inline pattern it has seen thousands of times.
Truffle Security's research on LLM-generated code tested nine popular LLMs across two prompts: a Slack SDK call (where the official docs use environment variables) and a Stripe SDK call (where some quickstart pages historically used a hardcoded key for brevity). Most of the models reproduced the documentation pattern. For the Stripe prompt, that meant generated code with the key inline. The model is not making a security judgement; it is mirroring the shape of the most common example.
Does Stripe's own documentation make this pattern worse?
In practice, yes, at the margin. A subset of Stripe's historical quickstart pages showed a key written directly inside the SDK constructor for clarity. The model treats that as the canonical example. The official Stripe Keys page is explicit in the other direction: "Don't put keys in source code or configuration files checked into version control" and "Before you start using a live mode key in your backend application, remove any hardcoded API keys from your code. Instead, use a secrets vault or environment variable." Stripe also recommends moving to Restricted API Keys rather than full secret keys, so that if one slips into a repository the blast radius is bounded.
The gap between the quickstart shape and the production guidance is where the AI lives. The model has not internalised the warning paragraph as strongly as the constructor call. The result is generated code that compiles, works against a test account, and quietly contains the secret in plain text.
What does a .cursorrules file actually change in practice?
Cursor loads a repository-level .cursorrules file as a system message on every request inside that workspace. Adding short, blunt rules to that file changes the model's defaults for the rest of the session.
A useful baseline:
// Security defaults for this project
Never hardcode API keys, tokens, or passwords.
Always read secrets from process.env.
If a value looks like a Stripe key (starts with sk_, rk_, pk_), refuse to inline it.
Use Restricted API Keys for server-side Stripe calls when possible.
Never echo a secret in logs.
This works because the rules sit near the top of the context window every turn, and the model is biased toward complying with system-level instructions. It does not make the output guaranteed safe; it does shift the dominant pattern from inline to environment-variable, which is the single biggest win available without changing the model itself.
How do I stop my .env file from leaking through the AI?
A .cursorignore file at the project root tells Cursor not to index, send, or suggest changes for the listed paths. The syntax matches .gitignore. A defensive starting point:
.env
.env.*
*.pem
*.key
secrets/
config/credentials.*
This is independent of Privacy Mode. Even with Privacy Mode enabled, an indexed .env file is still being read by Cursor for context, and the model may quote it back in completions. .cursorignore removes those files from the AI's view entirely, which is a safer baseline than trusting the model to ignore obvious secrets. Combine it with .gitignore so the same files also never reach the remote repository.
How do hardcoded Stripe keys end up in a public repository?
The path from "Cursor wrote my key in the source" to "my key is on public GitHub" is short and entirely human. The model writes the key inline. The developer hits commit without reading the diff carefully (the file looks fine, the integration works). The push goes to a public repository, sometimes one created earlier for a tutorial and forgotten. Scanners pick it up within minutes. GitGuardian's State of Secrets Sprawl report consistently finds tens of millions of new secrets exposed on public GitHub each year, with AI-assisted repositories leaking at a measurably higher rate than human-only ones.
The risk for Stripe specifically is that a live secret key can authorise refunds, payouts, and customer data reads on the connected account. Public post-mortems from solo developers describe charges and refund storms running into thousands of dollars within hours of a leak, against accounts where the developer had no idea the key was in the public repository.
What is the correct rotation order after a Stripe key is exposed?
The rotation order matters because Stripe keys are referenced from multiple places at once. The wrong order can break the production app.
| Step | Action | Why it matters |
|---|---|---|
| 1 | Schedule the old key for revocation in the Stripe Dashboard | Sets the clock on the leaked credential |
| 2 | Generate the replacement key (prefer a Restricted API Key) | Stripe shows it once, store it in a secrets vault |
| 3 | Update the secret in your hosting platform's env var store | Vercel, Fly, Render, AWS Secrets Manager, Cloudflare Workers |
| 4 | Trigger a redeploy so the running app reads the new value | Old containers may still hold the old key in memory |
| 5 | Confirm health checks pass on the redeployed service | Catches a typo in the env var name early |
| 6 | Revoke the old key fully in the Stripe Dashboard | Closes the window where the leaked key was still live |
| 7 | Audit the Stripe Events log for unfamiliar API calls during the exposure window | Surfaces abuse before billing does |
The step labels differ on platforms with built-in expiry scheduling, but the principle is the same: a brief overlap window where both keys are valid, then a hard cutover once the new key is confirmed in production.
For builders shipping iOS or Android apps that include Stripe, the same scan that catches hardcoded Stripe keys catches hardcoded Supabase service-role keys, Firebase secrets, and AWS credentials inside the IPA or AAB. For developers who want an external pass before submitting to the store, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with OWASP MASVS, including secret detection inside the compiled binary.
What to watch out for
- Generated code that "looks right" in review. A hardcoded
sk_live_...is short, the line compiles, the tests pass. The diff reviewer scans the change for shape, sees a one-line config, and lets it through. Train yourself to read every Stripe call carefully, regardless of who wrote it. - Models that reword the same pattern. A model told "use an env var" sometimes returns
const stripe = new Stripe(process.env.STRIPE_KEY || "sk_live_...")with the literal as a fallback. That is still a hardcoded key, hidden inside an OR expression. - Background Agent and long-running sessions. Cursor's Background Agent (and similar features in other tools) can persist code and prompts to vendor servers. A key in the prompt is on the vendor's disk even after you delete the file locally. Rotate.
- Public gists and screenshots. Developers paste failing code into Discord, X, or a gist for help. A screenshot of the IDE with the key visible is the same exposure as the commit, just slower to be indexed.
- The myth that test-mode keys are safe to leak. A test-mode key cannot move real money, but it can read test customer data, fire webhook traffic at your endpoints, and reveal product structure. Rotate test keys too, just with less urgency.
Key takeaways
- Cursor writing a Stripe key into the code is the expected default for current AI coding tools, not a malfunction. The fix is project rules, ignore files, and environment variables.
- Set a
.cursorrulesfile with explicit never-hardcode-secrets guidance and a.cursorignorethat covers.env,.pem, and any credentials path. These are five-minute changes that shift the dominant pattern. - Prefer Restricted API Keys over full secret keys for any Stripe call that does not need full permissions. A leaked RAK is a contained blast.
- Treat any key the AI has touched as potentially exposed. Rotate first, ask questions second. Stripe's dashboard makes the rotation itself fast; the harder work is updating every place that referenced the old value.
- For builders shipping a mobile app, an external scan of the compiled binary catches secrets that survived the developer-side controls. Platforms like PTKD.com (https://ptkd.com) run that scan against IPA and AAB files and report findings against OWASP MASVS.




