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 through a sanctioned channel (an MCP-aware secrets manager, or your shell sourcing a one-shot encrypted handoff) — not from files sitting 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. When storing a new secret, use a tool that pulls the value from your clipboard or a guarded prompt (NoxKey's noxkey_set(clipboard: true) or the menu bar "Add Secret" sheet) so the value never lands in an argv slot. 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. If an AI agent needs a secret, deliver it via an encrypted handoff into the agent's shell environment — 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: Hand people the secret through an end-to-end-encrypted channel, not as a Slack message. NoxKey ships a .noxkey share file for exactly this — right-click a secret, pick the recipient, send the file through iMessage, AirDrop, or Signal. The recipient double-clicks it into their Keychain with their own Touch ID, and the share is single-open-per-device, so a leaked file cannot be re-opened on a third machine. For higher-risk handoffs, wrap the share under a passphrase you tell the recipient out-of-band — a leaked file alone is opaque ciphertext. No secret value ever transits a messaging platform as plain text, and the value never touches a NoxKey server because there isn't one.

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: Use scoped, short-lived tokens where services support them. Open the NoxKey menu bar app 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.

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:

  1. Install NoxKey from the Mac App Store. The menu bar app and its bundled MCP server are both ready on first launch.
  2. Drag .env onto the menu bar icon. NoxKey shows you every key it found (values masked), waits for you to confirm, and writes the batch to the Keychain under one Touch ID.
  3. Delete the .env file. Actually delete it.
  4. Wire your agent up. Claude Code, Cursor, and other MCP-aware agents already see the noxkey_get tool. Have them call noxkey_get(account: "myorg/project/API_KEY") and run the returned source command — the value lands in the agent's shell env, never in the conversation.

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.