The mess had been there for some time before the engineer at the desk noticed it. He was switching between Claude Code projects — great-minds, great-authors, great-filmmakers, garagedoorscience — and a script written for one project would not run in another because the API key it expected was named differently in the second project's .env file. The fix took a minute. The realization took longer.

The realization was that he did not know where his API keys lived. He knew they lived in .env files. He did not know how many .env files. He did not know whether the Anthropic key in the project he had open on Tuesday was the same Anthropic key in the project he had open on Friday, or whether one of them was three rotations old, or whether either had been quietly copied into a planning directory during a hurried mv two months earlier. He decided to check.


The audit

The scan was one command:

find ~/Local\ Sites ~/Dropbox ~/Downloads ~/sethshoultes.github.io \
     -type f \( -name ".env" -o -name ".env.*" -o -name "*.env" \) \
     -not -path "*/node_modules/*" -not -path "*/.next/*" -not -path "*/.git/*"

It returned 66 env files. Most were files a framework writes for you — example envs, Vercel runtime envs, test fixtures — and contained nothing secret. Of the 66, seventeen contained user-personal credentials: the keys tied to the engineer's billing and identity, the ones he reused across projects. Seventeen files. Nine projects.

The next pass was a hash audit. For each env file, a short script computed a SHA-256 hash of each key's value, truncated to twelve characters, and reported which files shared which hashes. Same hash meant same value. Different hashes meant different keys, which might be intentional or might be drift.

The numbers came back like this:

Different keys for the same service across projects can be intentional — a developer with two billing accounts will deliberately keep them separate so usage lands on the right invoice. That accounted for some of the differences. But when the same Anthropic key turned up in three files in the same project group — caseproof-studio, orchestrator-v2, and quilting-queen, all consuming the same account — that was not intent. That was duplication. If the engineer rotated the key in one of the three files and forgot the other two, the other two would go stale at unpredictable times, and a script written six months earlier would fail in a way that did not announce its cause.

The audit also surfaced two files in .archived directories that no one was reading. They had been gitignored once and copied unintentionally during a folder reorganization. The keys in them were live.


Three layers, two homes

Pen-and-ink illustration of a single canonical file at the center of a fan of project folders, New Yorker style, crosshatch shading.

The design that resolves this kind of mess is not a tool. It is a sorting principle: personal credentials and project credentials are different things and belong in different files. Three layers, two homes.

Layer 1a — personal-everywhere credentials. Keys tied to the engineer's person and billing, reused across every project. Anthropic, OpenAI, Gemini, ElevenLabs, HeyGen, Resend, GitHub. These belong in one canonical file: ~/.config/dev-secrets/secrets.env. Mode 0600. Outside any git repository. Excluded from Time Machine.

Layer 1b — project-isolated credentials. Keys billed to a specific account, intentionally not shared with the rest of the engineer's work. These belong in per-project subdirectories: ~/.config/dev-secrets/<project>/secrets.env. Same permissions. Same exclusions.

Layer 2 — project deployment credentials. DATABASE_URL, ADMIN_API_KEY, CRON_SECRET. The keys a deployed application needs at runtime. These stay in <project>/.env.local per the convention that Next.js and Vercel already expect.

Layer 3 — project tooling config. Feature flags, paths, anything that is not a credential. These stay in <project>/.env.tools.

The split lets each kind of secret live where it should. Personal keys consolidate. Deployment keys stay where the framework expects them. The two kinds stop being confused for each other, which is the confusion that produced the seventeen-file fan-out in the first place.


How Claude Code finds the file

For the canonical file to be useful in an agent-driven workflow, the agent has to know where it is and how to read it. Three pieces accomplish that.

The first is a global rule in ~/.claude/CLAUDE.md. Every Claude Code session loads this file at startup. It names the canonical path, names the source pattern, and forbids the workaround of pasting keys into chat. The source pattern is the standard one:

set -a && source ~/.config/dev-secrets/secrets.env && set +a

set -a exports every variable assigned until set +a turns the behavior off. The file becomes environment.

The second piece is a skill at ~/.claude/skills/load-secrets/SKILL.md, triggered whenever a script needs credentials. It instructs the agent to source the canonical file, names the variables it can expect to find, and forbids accepting a key pasted into the conversation. Pasted keys are how keys end up in chat logs.

The third is a runbook in the engineer's brain vault, at runbooks/Migrate Project Envs to Canonical Secrets.md. It is the procedure below, written so that any developer with the same scattered envs can follow it once and be done.


The migration

The procedure took an hour. It runs in seven steps.

One. Create the canonical home.

mkdir -p ~/.config/dev-secrets
chmod 700 ~/.config/dev-secrets
touch ~/.config/dev-secrets/secrets.env
chmod 600 ~/.config/dev-secrets/secrets.env
tmutil addexclusion ~/.config/dev-secrets/secrets.env

The directory is mode 700 so no other user can list it. The file is mode 0600 so no other user can read it. The tmutil addexclusion call keeps the file out of Time Machine snapshots, which would otherwise carry copies of every credential into a backup volume the engineer is not auditing.

Two. Audit existing env files. The scan above, then the hash audit. The output is a table of which keys appear in which files with which values — what is duplication, what is drift, what is intentional.

Three. Migrate Layer 1 keys. A short Python script reads each source env file, classifies each key as 1a, 1b, or 2, and writes the Layer 1 keys to the appropriate canonical file. It leaves the source files in place and adds a banner comment at the top pointing at the new home. Deleting the stale Layer 1 lines is a separate step done by hand, after the canonical file has been verified.

Four. Update ~/.claude/CLAUDE.md. Name the canonical file. Name the source command. Forbid paste-in-chat.

Five. Install the load-secrets skill. A small SKILL.md at ~/.claude/skills/load-secrets/. Skills live at the user level, so any Claude Code session in any project picks it up automatically.

Six. Verify. Open a fresh shell. Run the source command. Run a script that previously needed GEMINI_API_KEY. It reads the variable from the environment and runs without complaint. If it does not, the canonical file has a typo or a stray space, and the verification step is where that gets caught.

Seven. Rotate exposed keys. Any key that appeared in a chat transcript, a screen-share, a screenshot, or a committed file gets rotated at the provider's dashboard. The new value is written into the canonical file and only the canonical file. Every project picks up the new value on its next session, because every project now reads from the same place.


Where things sit now

Nine keys live in ~/.config/dev-secrets/secrets.env: Anthropic, OpenAI, Gemini, ElevenLabs, HeyGen, LiveAvatar, Resend, GitHub, and one cross-service identifier (HEYGEN_ELEVENLABS_SECRET_ID) that ties an ElevenLabs voice to a HeyGen avatar.

Three keys live in ~/.config/dev-secrets/caseproof-studio/secrets.env — Layer 1b, billed to a different account, intentionally not shared.

Two keys, for services no longer in use, sit in ~/.config/dev-secrets/.archived-revoked/. They have been rotated at their providers and archived for record only.

More than fifteen project-level env files remain on disk, kept clean. They contain Layer 2 deployment credentials and nothing else. The source command has been added to render scripts and runbooks across the projects. The same line at the top of every script. One line.


The lesson the brain vault now holds

The note the engineer wrote into the learning file reads, in part, that scattered credentials are not a security problem until they are. The drift between three copies of an Anthropic key is invisible until someone rotates one and the other two go stale at unpredictable times. The leak is silent: a key in a .env.bak inside a planning directory, gitignored once but copied during an unwary mv of an old folder.

The consolidation is not about adding security through encryption or some new tool. It is about removing the ambient drift. One file. One source command. One rotation point. The piece on the wrong probe argued that the brain vault is the part of the system the engineer cares about most when he is writing about a mistake; the canonical secrets file is the part he cares about most when he is not writing about anything at all, because it is what every other piece reads from at startup.

On the disk, in the home directory, mode 0600, excluded from Time Machine, sits a single file with nine lines in it.

Seth Shoultes builds things at garagedoorscience.com and writes about them occasionally.