In December 2024, a developer at a mid-stage fintech startup committed a .env file to a public GitHub repository. The file contained a Stripe live key, a Postgres connection string, and an AWS secret access key. A bot found it in 11 seconds. The Stripe key was used to process $14,000 in fraudulent charges before anyone noticed. The AWS key spun up crypto miners across three regions.
The developer had a .gitignore entry. They'd added it months ago. But they'd also run git add . from a new machine where the repo hadn't been cloned with the gitignore intact. One command. Fourteen thousand dollars.
This isn't rare. GitGuardian's 2024 State of Secrets Sprawl report found 12.8 million secrets exposed in public GitHub repositories that year. The part that should keep you up at night: 70% of those secrets were still valid 5 days after detection. People aren't just leaking secrets — they're not rotating them after the leak.
The fix isn't better security tools, though those help. It's better habits.
The 7 habits that leak secrets
1. Storing secrets in .env files
I ran find ~/projects -name ".env" | wc -l on my machine last year. The answer was 47. Forty-seven plaintext files, no authentication, sitting in project directories across my laptop. Some contained the same Cloudflare API token. Some had credentials for services I'd forgotten I'd signed up for.
The .env file was a Ruby convention from 2012. It was designed for convenience, not security. There's no encryption, no access control, no audit trail. Every process running as your user can read every .env file on your machine. That includes AI coding assistants, which treat your .env file as just another project file.
Fix: Move secrets to your OS credential store. On macOS, that's the Keychain — encrypted, backed by the Secure Enclave, protected by Touch ID. Load secrets at runtime with eval, not from files on disk.
2. Passing secrets as CLI arguments
A colleague once asked me to help debug a failing API call. He shared his screen and typed curl -H "Authorization: Bearer sk_live_4eC39HqLyjWDarjtT1zdp7dc" https://api.stripe.com/v1/charges. That token was now in his shell history (~/.zsh_history), visible in ps aux output to any process on his machine, stored in his terminal app's scrollback, and displayed on the screen recording he was making for a bug report.
Four copies of a production Stripe key from one command.
Fix: Read credentials from environment variables or stdin. Use --clipboard for storing secrets. Never type or paste a secret value as a command-line argument. If you need to pass auth to curl, use -H @- and pipe it in.
3. Pasting secrets into AI chats
"Here's my .env file, can you help me debug this deployment?" I've seen this exact message in public Discord servers. But even in private conversations with ChatGPT or Claude, that secret is now sitting on someone else's servers. It's in conversation history. It might be in training data. It's definitely backed up, replicated, and stored in ways you can't control or audit.
Here's what actually happens to that data: your message hits an API endpoint, gets logged for abuse detection, stored in a conversation database, potentially queued for human review if it triggers safety filters, and retained for a minimum period defined in the provider's data retention policy. You just gave your production database URL to a system with more copies of it than you can enumerate.
And that's the best case — where you're talking to a legitimate provider. Wrapper apps and browser extensions that proxy AI conversations add their own logging layers.
Fix: Never paste credentials into any chat. Use a DLP guard that scans outbound text for known secret patterns. If you need an AI agent to use a secret, it should flow through the OS — not through the conversation.
4. Sharing secrets via Slack and email
I searched my team's Slack workspace for "API_KEY" last month. 23 results. Database passwords in DMs, Stripe keys in channel messages, SSH credentials in thread replies. All searchable by anyone in the workspace. All stored on Slack's servers indefinitely unless someone has an enterprise retention policy.
The worst part is that rotating these credentials doesn't clean up the Slack messages. The old values sit there forever, creating a historical record of every credential your team has ever used.
Fix: Give people access to the credential store, not the credentials. If someone needs the staging database URL, add them to the org's prefix in your secrets manager. They pull it themselves with their own Touch ID. No secret value ever transits a messaging platform.
5. Duplicating secrets across projects
I had one Cloudflare API token in 6 different .env files. When I rotated it, I updated the two projects I was actively working on and forgot the other four. Three months later, a deploy failed on a project I hadn't touched in weeks because it was still using the old token. That's the best outcome — the worse outcome is not noticing and assuming the old token is revoked when it's still active in four locations.
Fix: One secret, one location. Reference by path: shared/CLOUDFLARE_API_TOKEN. Every project pulls from the same source. Rotate once, it's rotated everywhere. No copies, no sync, no drift.
6. Never rotating credentials
I audited my API keys last quarter. One Stripe key had been active since 2023. Same key, full access, never rotated. It had been in my shell history, in at least two .env files that were since deleted (but still in filesystem backups), and in a Slack DM from when I'd sent it to a contractor.
The median age of a leaked secret on GitHub is over 2 years, according to GitGuardian. These aren't abandoned test keys. They're production credentials that nobody ever thought to rotate because nothing visibly broke.
Fix: Set expiry dates on secrets. Get warnings when they approach expiry. Use scoped, short-lived tokens where the service supports them. And run noxkey ls periodically to see what you actually have — if you don't recognize a key, it's time to rotate or revoke.
7. No separation between human and agent access
Your AI coding assistant uses the same API token you do. Same permissions. Same access scope. When Claude Code runs a curl command to debug your API integration, it's making that request with your full production credentials. If it hallucinates a destructive API call, it works — because it has your identity.
This isn't hypothetical. I've watched agents construct valid API calls using credentials they inherited from the shell environment. The agent wasn't being malicious. It was doing exactly what I asked — debugging the API. It just happened to include my live credentials in the request.
Fix: Detect when an agent is requesting secrets. Give agents encrypted, scoped access. Block bulk export commands. The agent gets what it needs to function, but never raw credential values in its text context.
Security audit in 5 minutes
Run these commands right now. They take less than five minutes and will tell you exactly where you stand.
# How many .env files do you have?
find ~/projects -name ".env" -o -name ".env.local" -o -name ".env.production" 2>/dev/null | wc -l
# Any live Stripe keys on disk?
grep -r "sk_live_" ~/projects --include="*.env" --include="*.env.*" -l 2>/dev/null
# Any AWS keys on disk?
grep -r "AKIA" ~/projects --include="*.env" --include="*.env.*" -l 2>/dev/null
# Secrets in shell history?
grep -E "(sk_live|sk_test|AKIA|ghp_|glpat-)" ~/.zsh_history ~/.bash_history 2>/dev/null | wc -l
# Old keys in git history? (run inside a repo)
git log --all -p | grep -E -c "sk_live|AKIA|ghp_" 2>/dev/null
I ran grep -r 'sk_live' ~/projects and found 14 matches. Fourteen places a production Stripe key was sitting on my disk. Some in .env files, some in test fixtures, one in a README where I'd pasted an example without sanitizing it. That number should be zero.
The minimum viable security stack
You don't need an enterprise security platform. For an individual developer or small team:
OS Credential Store
macOS Keychain or Linux Secret Service. Not files.
Biometric Auth
Touch ID on every read. Not a master password you type once.
CLI Workflow
Must work in terminals, scripts, and CI.
Agent Detection
Different behavior when an AI tool requests secrets.
Expiry Tracking
Know when your tokens are about to die.
Single Source of Truth
One secret, one location. No copies, no sync, no drift.
The checklist
Print this. Tape it to your monitor. Fix one per week.
- Run the 5-minute audit above. Count your .env files.
- Install a Keychain-backed secrets manager (
brew install no-box-dev/noxkey/noxkey). - Import your most-used project's .env:
noxkey import org/project .env - Delete the .env file. Actually delete it.
- Search Slack/email for credentials you've shared. Rotate them.
- Check shell history for secrets:
grep -E "(sk_live|sk_test|AKIA|ghp_|glpat-)" ~/.zsh_history ~/.bash_history - Set up a pre-commit hook that blocks secrets (
git-secretsorgitleaks). - Consolidate duplicated keys to a single
shared/path. - Review token ages. Rotate anything older than 90 days.
- Set up a DLP guard for AI agent output.
Start with one
You're not going to fix all seven habits today. You don't need to. The highest-impact change is moving from .env files to your OS credential store. On macOS:
# Install
brew install no-box-dev/noxkey/noxkey
# Import your existing .env
noxkey import myorg/project .env
# Verify
noxkey ls myorg/project/
# Delete the .env
rm .env
# Use secrets in your shell
eval "$(noxkey get myorg/project/API_KEY)"
Three minutes. All your secrets are in the Keychain, encrypted, behind Touch ID. The .env file is gone.
Credential leaks aren't caused by sophisticated attacks — they're caused by habits. Seven common habits account for nearly all developer secret exposure. You don't need an enterprise security platform to fix them. Move secrets from files to your OS credential store, add biometric auth, detect agent access, and fix one habit per week. Start by deleting your .env files today.
Pick one habit. Fix it today. Then come back for the next one.