All posts

The Developer's Guide to Credential Hygiene

Real Incident

December 2024. A developer at a fintech startup committed a .env file to a public GitHub repo. It 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 processed $14,000 in fraudulent charges. The AWS key spun up crypto miners across three regions.

The developer had a .gitignore entry. Added months ago. But they ran git add . from a new machine where the gitignore wasn't in place. 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% were still valid 5 days after detection. Developers aren't just leaking secrets — they're not rotating them after the leak.

12.8M
secrets exposed on GitHub in 2024
70%
still valid 5 days after detection
11s
time to detection by bots

The fix isn't better security tools. It's better habits.

7 habits that leak your credentials

1. Storing secrets in .env files

We ran find ~/projects -name ".env" | wc -l on a team member's machine last year. The answer was 47. Forty-seven plaintext files, zero authentication, sitting in project directories. Some contained the same Cloudflare API token. Some had credentials for services nobody remembered signing up for.

The .env file was a Ruby convention from 2012. Designed for convenience, not security. No encryption, no access control, no audit trail. Every process running as your user can read every .env file on your machine — including AI coding assistants that treat your .env 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 shared their screen to debug a failing API call and typed curl -H "Authorization: Bearer sk_live_4eC39HqLyjWDarjtT1zdp7dc" https://api.stripe.com/v1/charges. That token was now in their shell history (~/.zsh_history), visible in ps aux output, stored in the terminal's scrollback, and captured in the screen recording they were 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?" We've seen this exact message in public Discord servers. But even in private conversations with ChatGPT or Claude, that secret is now on someone else's servers. In conversation history. Possibly in training data. Backed up, replicated, and stored in ways you can't control or audit.

Your message hits an API endpoint, gets logged for abuse detection, stored in a conversation database, potentially queued for human review, and retained per the provider's data retention policy. You just handed your production database URL to a system with more copies of it than you can count.

That's the best case — 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 an AI agent needs a secret, it should flow through the OS — not through the conversation.

4. Sharing secrets via Slack and email

We searched a team 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.

Rotating these credentials doesn't clean up the Slack messages. The old values sit there forever — 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

One Cloudflare API token in 6 different .env files. When we rotated it, we updated the two active projects and forgot the other four. Three months later, a deploy failed on a dormant project still using the old token. That's the best outcome. The worst is not noticing — assuming the old token is revoked when it's still live 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

We audited our API keys last quarter. One Stripe key had been active since 2023. Same key, full access, never rotated. It had been in shell history, in at least two deleted .env files (still in filesystem backups), and in a Slack DM sent 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 rotated because nothing visibly broke.

Fix: Set expiry dates on secrets. Get warnings when they approach expiry. Use scoped, short-lived tokens where services support them. Run noxkey ls periodically to see what you have — if you don't recognize a key, rotate or revoke it.

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, it makes 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. We've watched agents construct valid API calls using credentials inherited from the shell environment. The agent wasn't malicious. It was doing exactly what we asked. It just used 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.

5-minute credential security audit

Run these commands right now. They 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

We ran grep -r 'sk_live' ~/projects and found 14 matches. Fourteen places a production Stripe key sat on disk. Some in .env files, some in test fixtures, one in a README with an unsanitized example. That number should be zero.

The minimum viable credential management 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 expire.

Single Source of Truth

One secret, one location. No copies, no sync, no drift.

Six things. Most developers do zero of them.

The credential hygiene 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 — download 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-secrets or gitleaks).
  • 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

Getting started today

You won't 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 NoxKey — download from https://noxkey.ai

# 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.

Key Takeaway

Credential leaks aren't sophisticated attacks — they're habits. Seven common habits account for nearly all developer secret exposure. You don't need an enterprise 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. Come back for the next one.