The problem it exists for
Every tool that talks to a service needs a token, and the easy place to put one is the config file. That’s the threat go/credentials is written against: “a plaintext secret committed to, or left in, a config file. Config files get checked into git, copied between machines, attached to bug reports, and synced to backups.”
The second problem is quieter. Once a token can come from several places (an environment variable, the OS keychain, a value in the file), something has to decide which one wins. When every library decides for itself, a program ends up with several precedence orders stacked inside each other, and nobody can say which token it’s actually using. The forge library had exactly that, and its spec put it plainly: “Two precedence systems in one path is how configuration becomes unpredictable” (forge spec 0005).
How it works
A credential is a source, not a string
A library that needs a secret takes a function it can call when it needs one, never a string handed over at start-up. The secret is looked up at the moment of use rather than carried around, and the library doesn’t care where it came from. Returning nothing means “not configured”, and returning an error means “configured but broken”, so a keychain that won’t unlock never reads as a missing token. Git and forges and go/nats take credentials this way, and the reason is in nats’ own words: “precedence is the consumer’s decision, not a library’s.”
The order is decided once, by the application
go-tool-base states its order in one place: an environment variable it’s been told the name of, then the keychain, then a value in the file, then the variable a vendor’s own tools would use. Both the code that supplies a token and the doctor command that reports on it build from that one statement, so the two can’t disagree about which source wins (spec 0189). doctor shows what’s stored, where each credential resolves from and what’s being shadowed, and none of that report is a credential value. Each source is only consulted when it’s needed, so a repository reached over SSH never triggers a keychain prompt for a token it doesn’t need.
When a keychain entry is configured but can’t be read, it refuses rather than falling back to a plaintext value in the file. The default storage choice reads the environment too. It uses the keychain only outside CI, at a real terminal, and after a test write succeeds. Everywhere else it records the name of an environment variable.
Where to keep it
go/credentials is the storage half. A config file records one of three things: the name of an environment variable, a keychain reference, or, as a last resort, the secret itself. Storing the secret itself isn’t offered in CI, and is refused there if it’s chosen anyway. The OS keychain is opt-in: leave it out and the keychain libraries never enter the binary, which you can prove from the build’s software bill of materials. The module deliberately has no resolver and no precedence order, because “the order in which your tool consults those sources at runtime is your tool’s policy, and it is written in your tool.”
config treats secrets stores as layers marked sensitive. The secrets managers (Vault, AWS, Google Cloud and Azure) are read-only, and a write that would copy one of their values into a plain file is refused. The keychain layer is the one sensitive layer you can write to: a token goes into the keychain, and one already there can never be written to the file beneath it. A shared test suite holds every sensitive backend to that rule.
go/vaultclient builds a Vault client four ways, from your own client to the ambient environment, and never logs the token at any level. A test sets a token and fails if it turns up in the output.
Scrubbed on the way out
go/redact masks secrets in text before it leaves the process: passwords in URLs, name=value pairs and JSON fields with credential names, authorisation headers, JWTs, known token prefixes from GitHub, GitLab, AWS, Google, OpenAI and Slack, and long opaque tokens. Its rule is “Sanitise where data leaves the host, not everywhere.” A second pass changes nothing, so an outer boundary can scrub again “without coordination”, even when an inner one already has (threat model). transit runs every outbound URL it logs through it, and always masks sensitive header values, even when a caller asked for them to be logged. The rules can’t be switched off, because “a redactor whose rules can be weakened by the environment is a redactor that can be switched off by accident.”
And in the pipeline
gitleaks runs in every language track’s security component, scanning only the commits a merge request adds, with its own output redacted: go-security on 99 projects, rust-security on 12, tofu-security on 7, and the svelte and skills components. 119 of the 139 projects with a pipeline run at least one.
Decisions and what they cost
- No ladders in libraries. forge’s built-in precedence was removed and the order moved into the application (forge spec 0005). What it cost: config reads every backend when it starts, so a program built on config alone unlocks every credential at start-up whether it uses them or not. go-tool-base avoids that by making each source lazy.
- A function, not an interface. Every real credential source is a one-liner, and an interface would make each one declare a type. What it cost: go/nats and go/forge each define the same function shape rather than sharing one.
- The caller’s deadline. A slow keychain is abandoned when the caller gives up, and there’s no default timeout because “no single value is defensible” (spec 0001). What it cost: an abandoned save can still finish after you’ve moved on.
- Fixed redaction rules. What it cost: false positives such as
sort-key=***, and short or bespoke secrets get through.
Proof in use
- go-tool-base, keryx, krites and phpbotscout store credentials through go/credentials. go/redact is used directly by go-tool-base, phpbotscout, forge, transport and transit, and by 16 more modules through them.
- 7 releases of go/credentials since July 2026, 6 of go/redact, and 14 of the keychain config layer.
- The guarantees are tests: the keychain stays out of the core’s dependency graph, Vault’s token never reaches a log, and redaction is fuzz-tested.
Use it when, and when not to
Use it if you’re building a Go tool other people install, and you want their tokens out of config files and out of your logs, without picking a credential order on their behalf.
It isn’t a secrets manager. go/credentials doesn’t rotate, cache, retry or audit, and holds one keychain backend per process. go/redact recognises shapes, not randomness, so a short secret with no telling name gets through, and it doesn’t parse YAML or JSON. A Vault token doesn’t renew itself, so a long-running process fails once it expires. go/errors doesn’t redact on purpose: keeping values out of errors is a convention, and go/redact is applied at the boundary. One place doesn’t follow the rule yet: go/chat still looks a provider’s API key up through its own fixed order.
Where it’s going
Holding a credential that expires and has to be renewed in a long-running process (go/credentials#5), OAuth and SSH credentials in go-tool-base (go-tool-base#121), keryx moving its secrets into the keychain layer (keryx#7), go/chat handing its key order to the application (go/chat#35), and transit’s docs catching up with the masking it already does (transit#7).
The modules beside it
- redact Strips credential-like content from free-form strings before they reach logs or telemetry.
- vaultclient Resolve the Vault client an adapter talks to. Vault is the provider where the client *is* the connection prerequisite, carrying the address, namespace, token and retry policy together, so there is no separate config object to hand on.
Last reviewed .