Vibe Coding Security: How to Build AI Apps Without Leaking Your API Keys
Vibe coding — letting an AI assistant write most of your code while you describe intent in plain language — has changed how apps get built. It has also changed how API keys leak. When the code is generated by a model that doesn't reason about security by default, hardcoding a secret straight into the file it's editing is the path of least resistance.
This post digs into why vibe coding and security clash so often, and what to actually do so your next AI-generated app doesn't ship your Stripe, OpenAI, or database credentials straight into a public repo.
Why vibe coding is a real risk to your secrets
Ask an AI assistant to "connect this to the Stripe API" and you'll likely get something like:
const stripe = require('stripe')('sk_live_51Hxxxxxxxxxxxx');
It works on the first try. That's exactly the problem. The model has no idea whether that code is heading into a commit, a public repo, an error log, or an unencrypted local chat history your editor keeps on disk. It's not malice — the model's goal is code that runs, not code that's safe.
AI assistants default to hardcoding keys
This is the most common pattern. You ask for a function, the assistant solves it with the key baked in because that's the simplest way to make the example compile. If you're not reviewing every line — and in vibe coding, by definition, you're reviewing less — that key ends up in the commit.
The prompt itself is a leak vector
A lot of vibe coding workflows paste entire .env files, service tokens, or database credentials directly into the prompt "for context." That text can persist in the model provider's logs, in your editor's local history, or in a screenshot shared on a forum when you're asking for debugging help.
AI-generated repos scale bad habits
If you spin up ten projects in an afternoon with AI assistance, and each one gets a hardcoded key, you now have ten exposure surfaces instead of one. The sheer volume vibe coding produces means old mistakes repeat faster than before. We go deeper on this dynamic in /ai-security.
Vibe coding security ground rules before you open the editor
Before you start prompting:
- No secret ever enters your code or your prompt. Code references a name (
STRIPE_SECRET_KEY), never a value. - No secret goes into a committed
.env. If your assistant suggests creating a.envwith real values and committing it "just to test," don't accept it. - Production and dev secrets are separated by environment. An assistant generating throwaway test code shouldn't have access to production keys.
- Someone — or something — controls who can read each secret, and when. Not a shared folder in Slack.
Referencing secrets by name instead of by value
This is where a secrets manager like Koove fits: your code never sees the real value, only a name. Store the secret once from the CLI:
npm install -g @koove/secrets-cli
koove set STRIPE_SECRET_KEY sk_live_51Hxxxxxxxxxxxx --env prod
koove set OPENAI_API_KEY sk-xxxxxxxxxxxx --env dev
From there, when you tell your AI assistant "wire up Stripe payments," the code it writes only needs the name:
// The AI assistant never sees or writes the real value
const stripeKey = await getSecret('STRIPE_SECRET_KEY');
You can check what's stored per environment without ever exposing the values:
koove list --env prod
And if you're migrating a .env left over from an AI-generated prototype, you import it in one shot instead of leaving it in the repo:
koove import .env --env dev
Each secret is encrypted with envelope encryption (X25519 + AES-256-GCM, HKDF-SHA256 key derivation) using open-source primitives (@koove/crypto). Koove's server only ever stores ciphertext — there's no plaintext version of your Stripe key sitting on any Koove backend.
From mobile: decryption behind biometrics, not a loose token
If your app consumes those secrets from a mobile client (React Native/Expo, dev build), the SDK verifies the device is real before anything decrypts:
import { KooveClient } from '@koove/sdk';
const client = new KooveClient({
apiUrl: process.env.KOOVE_API_URL,
appId: process.env.KOOVE_APP_ID,
appToken: process.env.KOOVE_APP_TOKEN,
});
await client.init(); // attestation (App Attest / Play Integrity) + device registration
const stripeKey = await client.decryptSecret(envelope); // requires biometrics
Attestation is verified end-to-end on real hardware on both iPhone and Pixel, with no developer bypass: if the device fails the OS-level check, there's no secret to decrypt. Full architecture details live at /security, and the complete command and SDK reference is at /docs/sdk.
The limits you should know about
Being upfront about these is part of how we operate:
- Koove's encryption has no forward secrecy (no ratchet, unlike messaging protocols). It's envelope encryption with X25519 + AES-256-GCM — solid for secrets management, but not equivalent to the guarantees of a messaging-grade encrypted protocol.
- Revoking a device (
koove device-revoke) stops that device from requesting secrets again, but it doesn't erase a value that device already decrypted before revocation. If a secret is compromised, rotate it (koove setwith a new value, thenkoove rewrap) — don't rely on revocation alone. - Koove doesn't make your app "unhackable." It sharply reduces the secrets-exposure surface, which is where most AI-generated code fails today.
More answers in the /faq, and pricing details at /plans.
In practice
Vibe coding isn't going away, and it doesn't have to be insecure. The issue isn't that AI writes code — it's that the code sometimes needs secrets, and by default it asks for them the least secure way possible. Fixing the habit is straightforward: get the assistant to write getSecret('NAME') instead of a real value, and make that name resolve to an encrypted secret that only decrypts after verifying who's asking.
If you're ready to stop pasting real keys into your prompts, sign up for Koove and store your first secret in under a minute.