TL;DR: Secrets management means keeping your API keys, database passwords, and tokens out of your source code. Use .env files to store secrets locally, add .env to your .gitignore so they never get committed, and access them through process.env in your code. If you've already committed a secret, revoke it immediately and generate a new one — removing it from your code isn't enough because Git remembers everything. This is one of the most common mistakes AI makes, and one of the easiest to fix once you know the pattern.
Why AI Coders Need to Know About Secrets
Here's something that happens every single day in the vibe coding world: someone asks Claude or ChatGPT to build an app, gets working code back, pushes it to GitHub, and within minutes their Stripe API key is compromised. Their OpenAI key starts racking up charges. Their database is wide open.
This isn't a hypothetical. GitGuardian's 2024 report found 12.8 million new secrets exposed in public GitHub repositories in a single year. Automated bots scan every public commit, looking for patterns that match API keys, passwords, and tokens. When they find one, it gets exploited — often within minutes.
The reason this hits vibe coders especially hard is that AI coding tools almost always hardcode secrets by default. When you ask Claude to connect your app to a database, it writes the password directly into the code. When you ask it to add Stripe payments, the secret key goes right in the source file. The code works perfectly — and it's also a security disaster waiting to happen.
Secrets management is the system that prevents this. It's not complicated. It's not something you need a security degree for. It's a pattern — a way of organizing your project so that sensitive credentials stay on your machine (or your server) and never, ever end up in your Git history.
AI writes code that works. It doesn't write code that's safe to share. Every time AI generates a connection string, an API key usage, or a password — check whether it hardcoded the value or used an environment variable. This single habit will save you from the most common security breach in AI-generated code.
Real Scenario: You Asked AI to Add Stripe Payments
You're building a SaaS app with AI. Things are going great — you've got a landing page, user auth, a dashboard. Now you need to accept payments. You give your AI this prompt:
Add Stripe payment processing to my Node.js Express app. I need a checkout endpoint that creates a Stripe session for a $29/month subscription, and a webhook endpoint that listens for successful payments and updates the user's subscription status in my PostgreSQL database.
Your AI generates clean, functional code. The checkout flow works. The webhook fires. Users can subscribe. You commit everything, push to GitHub, and deploy. Life is good.
Except buried in that working code are your Stripe secret key, your database password, and your webhook signing secret — all hardcoded as plain text strings. And now they're in your Git history forever.
What AI Generated (And What's Wrong With It)
Here's what AI typically produces when you ask for Stripe integration. This code works — but it contains a critical security flaw that most beginners won't catch:
❌ What AI Often Generates — server.jsconst express = require('express');
const stripe = require('stripe')('sk_live_abc123def456ghi789jkl012mno345');
const { Pool } = require('pg');
const pool = new Pool({
host: 'db.myapp.com',
database: 'myapp_production',
user: 'admin',
password: 'MyDatabase$ecret99!',
port: 5432
});
app.post('/create-checkout', async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: 'price_abc123', quantity: 1 }],
success_url: 'https://myapp.com/success',
cancel_url: 'https://myapp.com/cancel',
});
res.json({ url: session.url });
});
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body, sig, 'whsec_xyz789abc012def345'
);
// ... handle event
});
Three secrets are hardcoded directly in this file: the Stripe secret key (sk_live_...), the database password, and the webhook signing secret (whsec_...). If this file gets committed to Git — even a private repository — those credentials are exposed.
Here's what this code should look like with proper secrets management:
✅ The Correct Pattern — server.js (dotenv 16.4, Node.js 20 LTS)require('dotenv').config(); // Load .env file at startup
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432'),
});
app.post('/create-checkout', async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
success_url: `${process.env.APP_URL}/success`,
cancel_url: `${process.env.APP_URL}/cancel`,
});
res.json({ url: session.url });
});
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
);
// ... handle event
});
✅ Your .env File (stays on your machine — never committed)
# .env — DO NOT COMMIT THIS FILE
STRIPE_SECRET_KEY=sk_live_abc123def456ghi789jkl012mno345
STRIPE_WEBHOOK_SECRET=whsec_xyz789abc012def345
STRIPE_PRICE_ID=price_abc123
DB_HOST=db.myapp.com
DB_NAME=myapp_production
DB_USER=admin
DB_PASSWORD=MyDatabase$ecret99!
DB_PORT=5432
APP_URL=https://myapp.com
✅ Your .gitignore File (tells Git to never track .env)
# .gitignore
.env
.env.local
.env.production
node_modules/
Install the dotenv package: npm install dotenv. That single line at the top — require('dotenv').config() — reads your .env file and loads every key-value pair into process.env. Your code never sees the actual secret values. They exist only in the .env file on your machine.
Understanding Each Part
Secrets management has a few moving pieces. None of them are complicated on their own — the power is in how they work together.
What Counts as a "Secret"?
A secret is any piece of information that, if someone else got it, could let them access your stuff or pretend to be you. The most common ones:
- API keys — Stripe, OpenAI, Mailgun, Twilio, SendGrid. These are like passwords that let your code talk to other services.
- Database credentials — The username and password your app uses to connect to PostgreSQL, MySQL, or MongoDB.
- JWT secrets — The key used to sign authentication tokens. If someone gets this, they can forge login sessions for any user.
- Webhook signing secrets — Used to verify that incoming webhooks are actually from the service that claims to send them.
- Encryption keys — Keys used to encrypt/decrypt sensitive data like credit card numbers or personal information.
- OAuth client secrets — Used in "Login with Google/GitHub" flows. Lets someone impersonate your app.
The .env File
A .env file is just a plain text file that sits in your project's root directory. Each line is a key-value pair: KEY_NAME=value. No quotes needed (unless the value contains spaces). No export statements. Just simple pairs.
The dotenv package (version 16.4, tested March 2026) reads this file when your app starts and loads every pair into Node.js's process.env object. Your code then accesses values with process.env.KEY_NAME instead of hardcoding strings.
The beauty of this pattern: your code is safe to share, commit, and push to GitHub. The secrets stay in the .env file, which stays on your machine.
The .gitignore File
The .gitignore file tells Git which files to completely ignore — pretend they don't exist. When .env is listed in .gitignore, Git won't track it, won't show it in diffs, and won't include it in commits. It's invisible to version control.
This is the critical link in the chain. Without .gitignore, your .env file gets committed like any other file — and your secrets end up in Git history.
Environment Variables in Production
On your local machine, dotenv loads secrets from your .env file. But when you deploy to production (Vercel, Railway, Render, Heroku, AWS), you set environment variables directly in your hosting platform's dashboard. The variables are injected into your app at runtime — no .env file needed on the server.
This is why process.env.STRIPE_SECRET_KEY works everywhere: locally it comes from your .env file via dotenv, and in production it comes from the platform's environment variable settings.
The .env.example File
One more piece: create a .env.example file that shows the structure of your environment variables without the actual values:
# .env.example — commit this to Git (no real values!)
STRIPE_SECRET_KEY=sk_live_your_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_secret_here
DB_HOST=localhost
DB_NAME=myapp_dev
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_PORT=5432
APP_URL=http://localhost:3000
This file does get committed. It tells anyone (or any AI) working on the project exactly which environment variables are needed, without exposing real credentials.
What AI Gets Wrong About Secrets
AI doesn't just make one mistake with secrets — it makes a pattern of related mistakes. Here are the three most common, and each one can compromise your entire project.
Hardcoding Keys Directly in Source Code
The most common AI security mistake
This is the big one. When you ask AI to "connect to Stripe" or "add email sending with SendGrid," it writes the API key as a literal string in your JavaScript file. It does this because the training data is full of tutorials and Stack Overflow answers that do the same thing — those examples optimize for clarity, not security.
Why it's dangerous: If your code is in a public repo, bots find it in minutes. If it's in a private repo, anyone with repo access (including future collaborators, or an account compromise) sees it. The key is also visible in your Git history forever — even after you delete it from the current code.
How to catch it: Search your codebase for patterns like sk_live_, sk_test_, key-, Bearer , and long random-looking strings. If any appear as literal strings in .js, .ts, or .py files — that's a hardcoded secret.
The prompt fix: Always include "use environment variables for all API keys and credentials" in your AI prompts. Better yet, add it to your AI tool's system prompt or project instructions.
Generating .env But Forgetting .gitignore
Half the fix is worse than no fix
Sometimes AI does the right thing and creates a .env file with your secrets. Great! But then it doesn't create a .gitignore — or creates one that doesn't include .env. You commit, push, and now your .env file (with all your real credentials) is sitting in your GitHub repo.
This is actually worse than hardcoding in some ways, because you think you've handled security. You see the .env file, you know what it's for, and you assume it's safe. But without the .gitignore entry, Git treats it like any other file.
How to catch it: After any AI generates your project structure, immediately check two things: (1) Does .gitignore exist? (2) Does it contain .env? If either answer is no, fix it before your first commit.
# .gitignore
.env
.env.*
!.env.example
node_modules/
The .env.* pattern catches .env.local, .env.production, and any other variant. The !.env.example exception ensures your template file still gets committed.
Exposing Secrets on the Client Side
Putting API keys in JavaScript that runs in the browser
This one is sneaky. You ask AI to build a feature that calls an API — maybe a weather widget, a map integration, or an AI chatbot. The AI puts the API key directly in your frontend JavaScript:
❌ Secret exposed in frontend code// script.js — this runs in the user's browser!
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
'Authorization': 'Bearer sk-abc123def456...', // ← visible to EVERYONE
'Content-Type': 'application/json'
},
body: JSON.stringify({ model: 'gpt-4', messages: [...] })
});
Anything in frontend JavaScript is visible to anyone who opens browser DevTools. Your OpenAI key, your Stripe publishable key (which is designed to be public — but not your secret key), any token or credential in client-side code is fully exposed.
The fix: API calls that require secret keys must go through your backend server. Your frontend calls your server, and your server (where .env variables live safely) calls the external API.
// Frontend: script.js — no secrets here
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage })
});
// Backend: server.js — secrets stay server-side
require('dotenv').config();
app.post('/api/chat', async (req, res) => {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: req.body.message }]
})
});
const data = await response.json();
res.json(data);
});
Now the OpenAI key lives only on your server. The browser never sees it. Users interact with your endpoint, and your server handles the authenticated API call.
I Already Committed a Secret — What Do I Do?
If you've already committed and pushed a secret to GitHub, removing it from your current code is NOT enough. Git stores the complete history of every file. Anyone can view previous commits and find your secret. Here's what to do right now:
Step 1: Revoke the Secret Immediately
Go to the service's dashboard (Stripe, OpenAI, AWS, whatever) and revoke or rotate the exposed key. Generate a new one. This is the single most important step — it makes the leaked key useless even if someone already copied it.
- Stripe: Dashboard → Developers → API Keys → Roll Key
- OpenAI: Platform → API Keys → Delete the key, create new one
- AWS: IAM → Users → Security Credentials → Deactivate Access Key
- Database: Change the password immediately via your database admin tool
Step 2: Add .env to .gitignore
If your .gitignore is missing or doesn't include .env, add it now:
# Add to .gitignore
.env
.env.*
!.env.example
Step 3: Remove the File from Git Tracking
Even after adding to .gitignore, Git still tracks files it already knows about. Remove it from tracking without deleting the local file:
git rm --cached .env
git commit -m "Remove .env from tracking"
git push
Step 4: Clean Git History (Optional but Recommended)
The secret is still in your Git history. For public repos, use BFG Repo-Cleaner to scrub it:
# Install BFG (requires Java)
brew install bfg
# Remove the .env file from all history
bfg --delete-files .env
# Clean up and force-push
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
Important: Force-pushing rewrites history. If others have cloned your repo, they'll need to re-clone. For personal projects this is fine. For team projects, coordinate first.
Step 5: Check for Damage
If the secret was exposed publicly:
- Check your Stripe dashboard for unauthorized charges
- Check your OpenAI usage for unexpected API calls
- Check your AWS billing for new resources you didn't create
- Review your database for unauthorized access or data changes
- Enable GitHub Secret Scanning to catch future leaks automatically
Beyond .env: Secret Rotation and Vault Services
The .env + .gitignore pattern handles 90% of what you need as a vibe coder. But as your projects grow — especially if you're deploying to production with real users — there are more robust tools:
Secret Rotation
Secret rotation means regularly changing your API keys and passwords on a schedule. If a key leaks but gets rotated every 90 days, the window of exposure is limited. Most major services (AWS, Google Cloud, Stripe) support automatic key rotation.
Managed Secret Services
- Doppler — Syncs secrets across environments. Great developer experience. Free tier available.
- Vercel Environment Variables — If you deploy on Vercel, their built-in env var system is all you need.
- AWS Secrets Manager — Enterprise-grade. Automatic rotation, audit logging, encryption at rest.
- HashiCorp Vault — The industry standard for secrets management at scale. Overkill for most vibe coders, but worth knowing it exists.
For most AI-enabled coders, start with .env + .gitignore. Move to a managed service when you have multiple environments (dev, staging, production) or a team that needs shared access to secrets.
Secrets Management Checklist
Run through this before every git push:
- ☐ All API keys and passwords are in
.env, not in source code - ☐
.envis listed in.gitignore - ☐
.env.exampleexists with placeholder values (committed to Git) - ☐ No secrets in frontend/client-side JavaScript
- ☐
require('dotenv').config()is at the top of your entry file - ☐ Different keys for development and production environments
- ☐ Hosting platform has production env vars set in its dashboard
What to Learn Next
Now that you understand secrets management, these topics build directly on what you've learned:
🔐 Security Basics for AI Coders
The five security mistakes AI makes most often — SQL injection, CORS, input validation, and more. The big picture that secrets management fits into.
🌍 What Are Environment Variables?
A deeper dive into how environment variables work across operating systems, hosting platforms, and deployment pipelines.
📦 What Is Git?
Understanding Git history, commits, and why "just deleting the file" doesn't remove it from your repository's past.
🛡️ API Security Guide
How to secure the API endpoints your AI builds — authentication, rate limiting, input validation, and HTTPS.
Frequently Asked Questions
What is secrets management?
Secrets management is the practice of keeping sensitive data — API keys, database passwords, encryption keys, and tokens — out of your source code and stored securely instead. It uses .env files, environment variables, and vault services to ensure your credentials never end up in a Git repository where anyone can find them. Think of it like keeping your house keys in your pocket instead of taping them to the front door.
What happens if I accidentally commit an API key to GitHub?
Automated bots scan every public GitHub commit for credential patterns and can find your key within minutes. Once exposed, attackers can use your API key to rack up charges on your account, access your data, or compromise your users. You must immediately revoke the key from your provider's dashboard, generate a new one, and use tools like BFG Repo-Cleaner to remove it from your Git history. Simply deleting the key from your current code is not enough — Git remembers every previous version of every file.
Is a .env file enough to keep my secrets safe?
A .env file is the essential first step — it separates secrets from code. But it's only safe if you also add .env to your .gitignore file so it never gets committed. For production apps with real users, consider managed secret services like Doppler, AWS Secrets Manager, or your hosting platform's built-in environment variable settings, which add encryption, access control, and automatic rotation.
Why does AI keep hardcoding my API keys?
AI models were trained on millions of code examples from the internet — tutorials, Stack Overflow answers, and demo projects that prioritize readability and simplicity over security. Hardcoded credentials are the simplest, most readable pattern, so AI defaults to them. Always include "use environment variables for all API keys and credentials" in your prompts, and check every AI output for hardcoded strings that look like keys or passwords.
Can I use the same API key in development and production?
You should not. Use separate API keys for development and production. This limits the blast radius if a development key leaks — it can't access production data or rack up production-level charges. Most API providers (Stripe, OpenAI, AWS) let you create multiple keys or have distinct test/live modes. Use your local .env file for development keys and your hosting platform's environment variable settings for production keys.
📅 Last updated: March 19, 2026. Tested with Node.js 20 LTS and dotenv 16.4.