Recipe · Security hygiene

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.

Time: ~30 min Difficulty: beginner Companion skill: set-up-canonical-secrets

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

Prerequisites


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:

LayerWhat goes hereExample
Personal-everywhereKeys tied to your identity, reused across projectsANTHROPIC_API_KEY, OPENAI_API_KEY, GITHUB_TOKEN, ELEVENLABS_API_KEY
Project-isolatedKeys scoped to one billing account or project clusterANTHROPIC_API_KEY for the caseproof-studio cluster
Deployment-localKeys tied to the running deployment, not the personDATABASE_URL, ADMIN_API_KEY, CRON_SECRET
InfrastructureHigh-blast-radius secretsAWS 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.

Gotcha — don't move deployment secrets. If you move 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' })
Never paste a key in chat. If a tool asks for an API key, point it to the file instead. The skill that accompanies this recipe enforces this rule: agents read from ~/.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.

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

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

Seth Shoultes builds at garagedoorscience.com and writes about it at sethshoultes.com/blog.