How to keep API keys out of .env files on a Mac

Updated

Why .env files leak

A .env file is plain text on disk. Every program running as you can read it, and so can every backup, sync folder and AI coding tool that indexes your project. Forget one line in .gitignore and it's committed; share your screen while it's open and it's on the recording. An export line in ~/.zshrc has the same problems, and also hands the key to every program you start from that shell.

The built-in way: the macOS Keychain

macOS ships a security command that stores secrets in your login keychain, encrypted on disk.

# store once (put -w last so you are prompted for the value and it stays out of shell history)
$ security add-generic-password -a "$USER" -s OPENAI_API_KEY -w

# pass it to one command
$ OPENAI_API_KEY="$(security find-generic-password -a "$USER" -s OPENAI_API_KEY -w)" npm run dev

That gets the key off disk in plain text. What it doesn't do: while you're logged in, the keychain is unlocked and any program running as you can run that same security command and get the key without a prompt. Nothing records who read it, and you wire it up by hand for every project.

With CopyBuffer: Touch ID for every request

  1. Put the key in the vault. Select the key anywhere, for example a line like OPENAI_API_KEY=sk-… in a web console or an old .env, and press ⌃⌘C. CopyBuffer opens a new vault entry with the variable name filled in. Check it and press ⌘S. Or edit any vault entry (⌘E) and type a name under Terminal environment variable (letters, digits and underscores, not starting with a digit).
  2. Install the command. Settings ▸ Terminal ▸ Install links ~/.local/bin/cbvault to the command inside the app. If ~/.local/bin isn't on your PATH, add export PATH="$HOME/.local/bin:$PATH" to ~/.zshrc.
  3. Run your command through it. Touch ID asks once, showing which process wants which names, then the values go to that command and its child processes only.
# inject into one command only (safest)
$ cbvault run OPENAI_API_KEY STRIPE_KEY -- npm run dev

# export into the current shell / print one value / list names
$ eval "$(cbvault export OPENAI_API_KEY)"
$ cbvault get OPENAI_API_KEY
$ cbvault list

# one line in ~/.zshrc adds cbload / cbunload
$ eval "$(cbvault shell-init)"
$ cbload OPENAI_API_KEY
Loaded: OPENAI_API_KEY
$ cbunload OPENAI_API_KEY

Most dotenv loaders don't overwrite variables that are already set, so you can delete the key from .env and keep the rest of the file.

What protects the key

  • Touch ID per request (or your Mac login password). The same names from the same terminal aren't asked again for 5 minutes, and locking the vault cancels that at once.
  • Only the real cbvault. The app accepts requests only over a socket readable by you (agent.sock, 0600), and checks through the kernel audit token that the caller runs as the same user and is a cbvault signed with the app's own certificate. Copied or modified binaries are refused.
  • An access log. The requesting process path, working folder and your decision are written to the vault's encrypted Access log.
  • Screen recording aware. While the screen is being recorded or shared, a locked vault won't open, and cbvault get or cbvault export without a pipe are refused because they would print the key on screen. cbvault run and eval "$(cbvault export …)" still work.
.env fileKeychain + securitycbvault run
On diskPlain textEncryptedEncrypted vault
Asks before handing outNeverNot while logged inTouch ID
Who receives itAnything that reads the fileThe command you wrapThe named command and its children
Log of who askedNoneNoneProcess, folder, time

Limits

  • A value loaded into a shell with export or cbload is visible to every program you start from that shell. Prefer cbvault run.
  • The program you run still receives the real key and could log or leak it.
  • CopyBuffer has to be running, and the vault lives on this Mac only. There is no team sharing and no sync.
  • It is a tool for your Mac, not for CI or servers. Use your CI's secret store there.

FAQ

Does cbvault work in CI or on a server?

No. It needs the CopyBuffer app and Touch ID on your Mac. Use your CI provider's secret store for pipelines.

Can I keep using tools that expect a .env file?

Yes. Remove only the secret lines from .env and start the tool with cbvault run NAME -- your-command. Most dotenv loaders keep variables that are already set in the environment.

What happens if I run cbvault while sharing my screen?

cbvault run and eval "$(cbvault export …)" work as usual. cbvault get and cbvault export without a pipe are refused, because they would print the key where the recording can see it.