AI tools cause more secret leaks than any other single factor in modern development. ChatGPT hardcodes APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. keys in every code sample it generates. Copilot autocompletes console.log(process.env.SECRET_KEY) without hesitation. Claude will write a deployment script that puts your database password in a shell command visible in CI logs. These are not hypothetical risks, they are patterns that happen daily, and they are how startups end up with surprise $40,000 AWS bills from crypto miners exploiting leaked credentials.
What counts as a secret
Not everything in your .env file is equally sensitive. Knowing the difference helps you decide how carefully to protect each value.
| Category | Examples | Treat as secret? |
|---|---|---|
| Credentials | Database passwords, OAuth client secrets | Always |
| API keys | Stripe, OpenAI, SendGrid, AWS access keys | Always |
| Signing keys | JWT secrets, webhook signing keys, encryption keys | Always |
| Tokens | Session tokens, refresh tokens, deploy tokens | Always |
| Internal URLs | Private API endpoints, internal service addresses | Sometimes |
| Public config | Port numbers, log levels, feature flags, app name | No |
The test: could someone impersonate your app, access your data, or run up charges if they had this value? If yes, it is a secret.
How AI leaks your secrets, five real patterns
These are the five most common ways AI tools compromise your secrets. Learn to recognize them on sight.
Pattern 1: Hardcoded credentials in source code
// ChatGPT generated this "working" Stripe integration
const stripe = require('stripe')('sk_live_4eC39HqLyjWDarjtT1zdp7dc');
// Copilot autocompleted this database connection
const pool = new Pool({
connectionString: 'postgres://admin:SuperSecret123@db.example.com:5432/production'
});Pattern 2: Logging secrets to console
// Claude suggested this "debugging" code
console.log('Connecting with key:', process.env.STRIPE_SECRET_KEY);
console.log('JWT Secret:', process.env.JWT_SECRET);
// These appear in server logs, CI output, and monitoring dashboardsPattern 3: Exposing server secrets to the browser
// Copilot doesn't distinguish client vs server variables
// This puts your secret key in the JavaScript bundle served to users
const apiKey = import.meta.env.VITE_STRIPE_SECRET_KEY; // WRONG prefix!
// Anyone can see this in DevTools > SourcesPattern 4: Secrets in AI-generated CI/CDWhat is ci/cd?Continuous Integration and Continuous Deployment - automated pipelines that test your code on every push and deploy it when tests pass. configs
# ChatGPT generated this GitHub Actions workflow
- name: Deploy
run: |
curl -X POST https://api.cloudflare.com/client/v4/deploy \
-H "Authorization: Bearer cf_token_abc123" \ # Hardcoded in the workflow file!
-d '{"project": "myapp"}'Pattern 5: Missing .gitignore entries
# AI never mentions adding .env to .gitignore
# It generates the .env file, you commit everything, secret is in git forever
git add .
git commit -m "initial setup" # .env is now in historysk_, api_key, password, secret, token, and any string longer than 20 characters that looks random. This single habit prevents most secret leaks.The cardinal rule: fail if a secret is missing
It is tempting to provide a default fallback for secrets so your app starts in development without a complete .env. Resist that temptation:
// BAD - if API_KEY is missing in production, the app silently uses a fake key
// that fails at the worst possible moment
const API_KEY = process.env.API_KEY || 'dev-key-do-not-use';
// GOOD - if API_KEY is missing, the app refuses to start
const API_KEY = process.env.API_KEY;
if (!API_KEY) {
throw new Error('FATAL: API_KEY is required but was not set');
}For a production-grade validation pattern:
// secrets.js - validate all secrets at startup
function requireSecret(name) {
const value = process.env[name];
if (!value) {
console.error(`FATAL: Required secret ${name} is not set.`);
console.error('Set it in your .env file (dev) or platform dashboard (prod).');
process.exit(1);
}
return value;
}
module.exports = {
stripeKey: requireSecret('STRIPE_SECRET_KEY'),
jwtSecret: requireSecret('JWT_SECRET'),
databaseUrl: requireSecret('DATABASE_URL'),
};Platform secrets management
Every serious hosting platform stores secrets separately from your code, encrypted at restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources.. Use the platform's secrets system in production, never deploy with .env files.
Vercel
# Add a secret linked to an environment
vercel env add STRIPE_SECRET_KEY production
# Vercel prompts you to type the value securely
# Secrets are encrypted and scoped per environment
# Development, Preview, and Production each have their own valuesCloudflare Workers
# Wrangler prompts for the value - it never appears in shell history
wrangler secret put STRIPE_SECRET_KEY
# Access in your Worker code via the env binding
export default {
async fetch(request, env) {
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
}
};Railway
Railway auto-provisions and injects database credentials when you add a database service. For custom secrets, use the Variables tab in your project dashboard. Railway injects them automatically at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary..
AWS / GCP / Azure
For enterprise projects, use a dedicated secrets manager: AWS Secrets Manager, Google Secret Manager, or Azure Key Vault. They add versioning, automatic rotation, and detailed access audit logs.
| Platform | How to set secrets | Encryption | Scoping |
|---|---|---|---|
| Vercel | CLI or dashboard | At rest | Per environment |
| Cloudflare | wrangler secret put | At rest | Per Worker |
| Railway | Dashboard Variables | At rest | Per service |
| AWS Secrets Manager | CLI or console | At rest + in transit | Per secret, IAM policies |
| Google Secret Manager | CLI or console | At rest + in transit | Per secret, IAM policies |
Sharing secrets with teammates safely
The instinct when someone says "I need the dev database password" is to paste it in Slack. Do not do that, chat logs are kept forever and are often not encrypted at restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources..
| Method | Security | Convenience |
|---|---|---|
| Password manager (1Password, Bitwarden) | High, encrypted, access-controlled | Medium, requires team setup |
| Platform dashboard | High, encrypted, audit logged | High, self-service |
| Encrypted message (age, gpg) | High, end-to-end encrypted | Low, requires tooling |
| Slack / Discord DM | Low, stored in plaintext logs | High, but dangerous |
| Low, stored on mail servers | Medium, easy to search/leak |
For new teammates: document the process in your README. Copy .env.example, reach out to a specific person, get credentials through a secure channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue)..
Detecting and responding to leaked secrets
Prevention with git hooks
git-secrets prevents you from committing known secret patterns:
# Install and configure
brew install git-secrets # macOS
cd your-repo
git secrets --install # Adds pre-commit hook
git secrets --register-aws # Adds AWS key patterns
# Scan before committing
git secrets --scan # Current files
git secrets --scan-history # Entire git historyGitHub also runs automatic secret scanning on public repos and will alert you if it detects a credential pattern from a known providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually..
If a secret leaks
Act immediately, assume the secret is already being exploited:
- Rotate the credential in the service's dashboard (generate new key, revoke old one)
- Update the new value in your secrets manager and all deployment platforms
- Scrub git history with
git filter-repoif the secret was committed, then force-push - Audit access logs to check if the secret was used by unauthorized parties
- Notify your team that the old credential is compromised and the new one is deployed
Key rotation habits
Rotating secrets reduces the blast radius of any single leak:
- Rotate high-value keys quarterly (Stripe, AWS, database passwords)
- Rotate immediately when a team member with production access leaves the company
- Rotate immediately if a
.envfile is accidentally shared or committed - Use short-lived tokens where the service supports it (OAuth2What is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password., temporary AWS credentials)
- Keep a rotation log, document when each secret was last rotated and by whom
Quick reference
| Action | Tool / Method |
|---|---|
| Store production secrets | Platform dashboard (Vercel, Cloudflare, Railway) |
| Share dev secrets safely | Password manager (1Password, Bitwarden) |
| Prevent committing secrets | git-secrets pre-commit hook + .gitignore |
| Scan for leaked secrets | git secrets --scan-history, GitHub secret scanning |
| Respond to a leak | Rotate immediately, update platforms, scrub history |
| Validate at startup | requireSecret() function that exits on missing values |
| Audit AI-generated code | Search for hardcoded strings, console.log of secrets |