Set Up Canonical Secrets
Centralize every scattered API key into one protected user-level store at ~/.config/dev-secrets/ — audited, permission-hardened, and wired to every tool that needs it.
Why canonical secrets
Every project eventually accumulates .env files. They breed. There's one in the project root, one in the backend, one in the frontend, one for staging, one the intern created last week that nobody knows about. Keys rot in git history, get pasted into Slack, and show up in screenshots.
A canonical secrets store fixes this. One directory. Two files. Every personal API key in one place, permission-hardened, backed up by Time Machine exclusion, never committed. Project-specific secrets in a sibling path. Deployment secrets stay in their local .env.local where the framework expects them. The rule is simple: if it's a key tied to you, it lives in ~/.config/dev-secrets/. If it's tied to the deployment, it stays in the project.
What you'll build
- A canonical secrets directory at
~/.config/dev-secrets/with mode 0700, excluded from backups. - A personal-everywhere file —
secrets.env— holding keys you reuse across all projects. - A project-isolated directory —
~/.config/dev-secrets/<project>/secrets.env— for billing-scoped keys. - An audit script that finds existing
.envfiles, classifies what's in them, and flags duplicates. - A revocation checklist — because moving secrets is also a good time to rotate them.
Prerequisites
- macOS or Linux (Windows/WSL works but commands assume bash/zsh).
- Terminal access and permission to create directories in
~/.config. - A list of your active API keys and which services they belong to.
Step 1 — Create the canonical directory
mkdir -p ~/.config/dev-secrets
chmod 700 ~/.config/dev-secrets
Exclude it from Time Machine:
tmutil addexclusion ~/.config/dev-secrets
Create the personal-everywhere file:
touch ~/.config/dev-secrets/secrets.env
chmod 600 ~/.config/dev-secrets/secrets.env
Step 2 — Audit existing .env files
Before moving anything, find all the keys already scattered across your machine:
# Find every .env, .env.local, and .env.tools on your machine
find ~ -name ".env*" -type f 2>/dev/null | grep -v node_modules | grep -v .git
For each file, classify the keys inside:
| Layer | What goes here | Example |
|---|---|---|
| Personal-everywhere | Keys tied to your identity, reused across projects | ANTHROPIC_API_KEY, OPENAI_API_KEY, GITHUB_TOKEN, ELEVENLABS_API_KEY |
| Project-isolated | Keys scoped to one billing account or project cluster | ANTHROPIC_API_KEY for the caseproof-studio cluster |
| Deployment-local | Keys tied to the running deployment, not the person | DATABASE_URL, ADMIN_API_KEY, CRON_SECRET |
| Infrastructure | High-blast-radius secrets | AWS root keys, Vercel deploy tokens, SSH private keys |
Copy only the personal and project-isolated keys into the canonical store. Leave deployment-local keys in their project's .env.local — the framework needs them at boot.
DATABASE_URL to the canonical store, the Next.js dev server won't find it. Deployment secrets stay in .env.local per existing convention. The canonical store is for personal credentials.
Step 3 — Populate secrets.env
Edit ~/.config/dev-secrets/secrets.env:
# Personal-everywhere keys
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...
ELEVENLABS_API_KEY=...
HEYGEN_API_KEY=...
GITHUB_TOKEN=ghp_...
RESEND_API_KEY=...
For project-isolated keys, create a subdirectory:
mkdir -p ~/.config/dev-secrets/caseproof-studio
chmod 700 ~/.config/dev-secrets/caseproof-studio
touch ~/.config/dev-secrets/caseproof-studio/secrets.env
chmod 600 ~/.config/dev-secrets/caseproof-studio/secrets.env
Project keys override personal keys for overlapping names. Source both when working in that project:
set -a
source ~/.config/dev-secrets/secrets.env
source ~/.config/dev-secrets/caseproof-studio/secrets.env
set +a
Step 4 — Wire tools to read from the canonical store
The goal is that no tool ever asks you to paste a key in chat. Claude Code, shell scripts, and agent SDKs should all read from the file silently.
Claude Code
Add a SessionStart hook in ~/.claude/settings.json that sources the canonical file:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'set -a && source ~/.config/dev-secrets/secrets.env && set +a && echo {\"systemMessage\": \"Secrets loaded.\"}'",
"timeout": 5
}
]
}
]
}
}
Shell scripts
Any script that needs a key should source the file instead of hardcoding:
#!/usr/bin/env bash
set -a && source ~/.config/dev-secrets/secrets.env && set +a
# Now $ANTHROPIC_API_KEY is available
curl -H "x-api-key: $ANTHROPIC_API_KEY" ...
Python / Node
Use python-dotenv or dotenv pointed at the canonical path:
# Python
from dotenv import load_dotenv
load_dotenv('~/.config/dev-secrets/secrets.env')
# Node
require('dotenv').config({ path: require('os').homedir() + '/.config/dev-secrets/secrets.env' })
~/.config/dev-secrets/, never prompt the user to paste.
Step 5 — Verify in a new terminal
Open a completely fresh terminal window (no prior source calls):
echo $ANTHROPIC_API_KEY
# Should print nothing — the file hasn't been sourced yet
set -a && source ~/.config/dev-secrets/secrets.env && set +a
echo $ANTHROPIC_API_KEY
# Should print your key, truncated
If the second command works, the canonical store is correctly set up.
Step 6 — Strip migrated values from old locations
Once a key is in the canonical store, delete it from the old .env files. Don't comment it out — delete it. Then commit the removal.
# Example: clean a project's .env.local
sed -i '/^ANTHROPIC_API_KEY/d' ~/my-project/.env.local
sed -i '/^OPENAI_API_KEY/d' ~/my-project/.env.local
If the old file had the key in git history, consider it leaked. Proceed to Step 7.
Step 7 — Revoke anything that might have leaked
Any key that ever lived in a .env file in a repo, in a screenshot, or in a shell history should be rotated. This is annoying but non-optional.
- Anthropic: Console → API Keys → Revoke → Create new
- OpenAI: Platform → API Keys → Revoke → Create new
- GitHub: Settings → Developer settings → Personal access tokens
- ElevenLabs: Dashboard → API Keys → Revoke → Create new
Update the canonical store with the new keys immediately after rotation. Don't leave a gap where old .env files have the new keys.
What this gets you
- One source of truth. Every personal key has exactly one canonical home. No more wondering which .env is current.
- Permission-hardened by default.
chmod 700on the directory,0600on the files. Other users on the machine cannot read them. - Backup-safe. Time Machine skips the directory. Cloud sync tools skip it. The keys stay local.
- No more pasted keys in chat. Agents and scripts read from the file. The user never types a secret into a text box.
High-blast-radius secrets
AWS root keys, Vercel deploy tokens, and SSH private keys are too dangerous for a plaintext file, even a permission-hardened one. Put them in the macOS Keychain instead:
security add-generic-password -s "aws-root-key" -a "$USER" -w "AKIA..."
# Retrieve later:
security find-generic-password -s "aws-root-key" -w
This is slower to access but the right tradeoff for infrastructure root access.
What's next
- Auto-source on shell startup. Add the
sourceline to~/.zshrcor~/.bashrcso every new terminal has the keys loaded. Be aware this exports them to all child processes. - Project-specific wrapper scripts. A
run.shin each project that sources the right personal + project files before starting the dev server. - Key expiration reminders. A cron that checks creation dates of keys in the canonical store and warns when they're older than 90 days.
Seth Shoultes builds at garagedoorscience.com and writes about it at sethshoultes.com/blog.