Plenty of tools these days want an API token or some other secret in an environment variable.
The path of least resistance is to echo 'export SOME_SECRET="a_secret"' >> ~/.zshrc and move on, and
that works, until you commit your dotfiles, back them up somewhere, or share your screen with the file open.
If you’re a Mac user, macOS has an encrypted secret store that unlocks when you log in: the keychain.
The security command lets you use it from the shell; your secret lives in the keychain and
your dotfiles only have a lookup.
storing a secret
security add-generic-password -a "$USER" -s github-token -w
-ais the account name. For portability,$USERis a sensible default.-sis the service name. Think of it as the secret’s name; it’s what you’ll use to look it up later.-wwith nothing after it, as the last option, makessecurityprompt for the value.
You can pass the value directly (-w ghp_abc123), but it ends up in your shell history,
and is briefly visible in the process list; it’s better to let it prompt you.
If the item already exists, add-generic-password fails. Add -U to update it in place
instead, which makes rotating a token a safe, repeatable command:
security add-generic-password -U -a "$USER" -s github-token -w
reading it back
security find-generic-password -a "$USER" -s github-token -w
Here -w means “print only the password.” Without it you get a pile of metadata and no secret.
using it as an env var
The simple version, in .zshrc:
export GITHUB_TOKEN="$(security find-generic-password -a "$USER" -s github-token -w)"
But… now every shell (and every process started from it) has the token in its environment, and each new shell pays for a keychain lookup at startup. If you only need a secret for one tool, you can scope it to that command only, using a function with the same name:
gh () {
GITHUB_TOKEN="$(security find-generic-password -a "$USER" -s github-token -w)" command gh "$@"
}
Now the token only exists for the lifetime of that gh process.
If you’ve got several of these, a small helper keeps it readable:
get-key () {
security find-generic-password -a "$USER" -s "$1" -w
}
export OPENAI_API_KEY="$(get-key openai-api-key)"
cleaning up
security delete-generic-password -a "$USER" -s github-token
You can also see and edit these items in Keychain Access; search for the service name.
things to know
- It’s tied to your login keychain. It’s unlocked when you’re logged in at the console. Over
SSH it may be locked, and lookups will fail until you run
security unlock-keychain. - Access control is per app. By default the app that created the item (here,
/usr/bin/security) can read it without prompting. Other apps asking for it will trigger a keychain dialog. - It doesn’t protect you from yourself. Once the value is in an environment variable, any process running as you can read it. The win here is keeping secrets out of plaintext files, git history, and backups, not sandboxing them at runtime.
- It’s macOS only. If your dotfiles are shared with Linux machines, guard the lookup with
command -v security >/dev/nullor aunamecheck.