Skip to content

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}$
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.