Choose a storage mode¶
You are wiring a setup flow and need to decide which of the three storage modes to offer, and which to default to. This guide gives you the decision and the code to enforce it.
The three modes¶
| Mode | What the config records | Where the secret lives | Offer it when |
|---|---|---|---|
ModeEnvVar |
the name of an env var | the process environment / shell profile / CI secret store | always — this is the default |
ModeKeychain |
a service/account reference |
the OS keychain | a keychain backend is registered and Probe succeeds |
ModeLiteral |
the secret itself | the config file (plaintext) | never under CI; only for throwaway / air-gapped hosts |
Default to ModeEnvVar. It keeps the secret out of your config file, works
identically in local dev and CI, and needs no extra dependency.
Let the package filter the options¶
Do not hand-roll the "is keychain available / are we in CI" logic — call
ModeChoices.
It includes keychain only when usable and hides literal under CI:
ctx, cancel := context.WithTimeout(ctx, credentials.KeychainOpTimeout)
defer cancel()
choices := credentials.ModeChoices(
credentials.IsCI(), // hides literal under CI=true
credentials.Probe(ctx), // includes keychain only if reachable now
"Environment variable reference (recommended)",
"OS keychain",
"Literal value in config file (plaintext)",
)
mode, err := prompter.SelectMode(ctx, "Credential storage", choices)
ModeChoices always puts ModeEnvVar first, so the first option is the safe
default when the user just presses Enter.
Enforce the CI-literal refusal¶
Hiding literal from the menu is a UX nicety, not a security control — a config file
or a scripted flow could still ask for ModeLiteral directly. Funnel every path
through RefuseLiteralUnderCI
as defence in depth:
if err := credentials.RefuseLiteralUnderCI(mode); err != nil {
return err // hinted error: "literal credential storage is refused under CI"
}
A literal write under CI almost certainly leaks the secret into build logs or
artefacts. This helper is the single source of truth for that rule — do not
re-derive mode == ModeLiteral && IsCI() inline.
Validate an env-var name¶
When the user picks ModeEnvVar, validate the name they give so it is a usable
POSIX variable:
name, err := prompter.InputEnvVarName(ctx, "Variable name", "MYTOOL_TOKEN",
credentials.ValidateEnvVarName) // enforces ^[A-Z][A-Z0-9_]{0,63}$
Recommended defaults by environment¶
| Deployment | Offer / default |
|---|---|
| Local desktop | env-var reference (default) or OS keychain |
| CI/CD pipeline | env-var reference only (literal hidden and refused) |
| Container / Kubernetes | env-var reference, populated by a mounted secret |
| Throwaway / air-gapped | literal accepted, with the plaintext-on-disk trade-off |
| Regulated / audited build | env-var reference only — do not import the keychain subpackage |
See the trust model for the reasoning behind each row.