TL;DR: Most CLI tools end up writing your API key into a plaintext config file, because it’s the path of least resistance and nobody stops them. go-tool-base offers three storage modes instead — an environment-variable reference (the default), the OS keychain (opt-in), and a literal value (legacy, and refused outright when it detects a CI environment). At runtime it resolves them in a fixed precedence, and the doctor command nags you if a plaintext secret is still sitting in your config.
The config file that quietly becomes a liability
Your CLI tool needs the user’s API key. It has to come from somewhere, and it has to survive between invocations, so the obvious move is to ask once and write it into the tool’s config file. ~/.config/yourtool/config.yaml, a nice api_key: line, done.
It works on the first afternoon. It keeps working. And then, slowly, it becomes a problem nobody decided to create.
The config file gets committed to a dotfiles repo. It gets caught in a tar of someone’s home directory that lands in a backup bucket. It scrolls past in a screen share. It sits, world-readable, on a shared build box. None of these are exotic. They’re just Tuesday. The plaintext key was fine right up until the file went somewhere the key shouldn’t, and config files go places.
I didn’t want go-tool-base handing every tool built on it that same slow-motion liability by default. So credential handling got rebuilt around a simple idea: the config file should usually hold a reference to the secret, not the secret.
Three modes, and which one you get
go-tool-base supports three ways to store a credential.
Environment-variable reference — the default. The config records the name of an environment variable, not the value:
anthropic:
api:
env: ANTHROPIC_API_KEY
The secret itself lives in your shell profile, your direnv setup, or your CI platform’s secret store, wherever you already keep that sort of thing. The config file now contains nothing sensitive. You can commit it, back it up, paste it into a bug report. The reference is inert on its own.
OS keychain — opt-in. The config holds a <service>/<account> reference and the actual secret goes into the operating system’s keychain: macOS Keychain, GNOME Keyring or KWallet via the Secret Service, Windows Credential Manager.
anthropic:
api:
keychain: mytool/anthropic.api
This one is opt-in by design, because the keychain backend carries dependencies that some deployments are not allowed to ship. (That opt-in mechanism turned out to be an interesting little problem of its own, and it has its own post.)
Literal value — legacy, and grudging. The old behaviour. The secret sits in the config in plaintext:
anthropic:
api:
key: sk-ant-...
It still works, because breaking every existing tool’s config on an upgrade would be its own kind of vandalism. But it’s the last resort, it’s documented as the last resort, and the setup wizard puts a warning in front of you when you pick it.
The one place literal mode is not allowed
There’s a single hard “no” in all of this. If go-tool-base detects it’s running in CI (CI=true, which every major CI platform sets), the setup flow will refuse to write a literal credential and exit non-zero.
The reasoning is that a plaintext secret written during a CI run is a plaintext secret written onto an ephemeral, often shared, frequently-logged machine, by an automated process that no human is watching. That’s the exact situation where the slow-motion liability becomes a fast one. CI environments inject secrets as environment variables already; there is no good reason for a tool to be writing one to disk there, so go-tool-base simply won’t.
How it decides at runtime
A credential can be configured more than one way at once — you might have an env reference and an old literal key still lurking. So resolution follows a fixed precedence, highest to lowest:
- The
*.envreference — if that env var is set, use it. - Otherwise the
*.keychainreference — if a keychain entry resolves, use it. - Otherwise the literal
*.key/*.value— the legacy path. - Otherwise a well-known fallback env var (
ANTHROPIC_API_KEYand friends), so a tool still picks up the ecosystem-standard variable with no config at all.
The useful property here is that adding a more secure mode transparently wins. Drop an env reference next to an old literal key and the next run uses the env var. You can migrate a credential to a better home without first removing it from its worse one, which makes the migration safe to do incrementally instead of as one nervous big-bang edit.
The tool tells on itself
A precedence rule is no use if nobody knows their config still has a plaintext key three layers down. So the built-in doctor command grew a check for exactly that. Run doctor, and if any literal credential is sitting in your config it reports a warning, names the offending keys (the key names, never the values), and points you at how to migrate.
It’s not an error. Literal mode is still legal. But the tool will quietly keep reminding you that you left the campsite messier than you could have, until you tidy it.
The gist
A CLI tool that writes your API key into a plaintext config file isn’t doing anything wrong, exactly. It’s just handing you a liability that activates later, when the file travels somewhere the key shouldn’t. go-tool-base’s answer is three storage modes — an env-var reference by default, the OS keychain on request, and a plaintext literal only as a documented last resort that CI environments can’t use at all. Runtime resolution runs in a fixed precedence so a more secure mode always wins, which makes migrating a credential safe to do gradually. And doctor keeps an eye on the config so a stray plaintext secret doesn’t get to hide forever.
The secret should live in a secret store. The config file should just know its name.