Skip to content

Why the precedence chain is ordered this way

Resolver tries four sources in a fixed order: the tool's own environment variable, the OS keychain, a literal in config, then an ecosystem-default environment variable. The order is not arbitrary, and it is not configurable.

The ordering principle: most specific and most immediate first

Each step down the chain is a step towards less specific to this invocation and harder for the operator to override in the moment.

cref.env names a variable this tool defined for itself. Setting it is an unambiguous statement about this tool, and a variable can be changed per-shell, per-command, without editing anything. That makes it the strongest signal of intent available, so it wins.

The keychain comes next. It is deliberate, durable configuration — the operator stored that secret on purpose — but it is machine-wide state rather than something scoped to this run. It should beat what is written in a config file, and lose to an explicit variable set for this invocation.

A literal in config is third because it is the weakest form of configuration on offer. It is checked into a file, it is easy to forget about, and it is the one source that can leak by being read.

fallback_env is last because it is the least specific thing in the chain. ANTHROPIC_API_KEY belongs to the ecosystem, not to this tool. A machine may have it exported for entirely unrelated software, so it should never quietly override a decision the operator made for this tool — but when nothing else is configured, using it is better than failing.

Why both environment legs exist instead of one

env and fallback_env do the same thing mechanically: read a named variable. They sit at opposite ends of the chain because they mean different things.

Collapsing them into one key would force a choice between two bad outcomes. If the single key ranked first, a stray ANTHROPIC_API_KEY on a developer's machine would silently outrank a secret they had deliberately stored in their keychain for this tool. If it ranked last, the tool's own variable would lose to a config-file literal, and the usual "override it for one command" workflow would stop working.

Two keys at two positions keeps "this tool, right now" at the top and "whatever the ecosystem happens to have set" at the bottom, which is what both are actually for.

Why "first hit wins" rather than "most secure source wins"

The chain returns the first source that produces a value, without ranking sources by how safe they are.

Preferring the most secure available source sounds better and behaves worse. It would mean a keychain entry silently overriding the variable an operator just exported to test something, which makes the tool feel broken in exactly the moment someone is debugging it. Predictability is the more useful property: the operator can reason about what will win without knowing what is stored where.

The security question is handled separately, by refusing the literal source outright under CI rather than by demoting it.

Why the order is fixed in code

There is no configuration key that reorders the chain, and this is a deliberate refusal rather than an unimplemented feature.

Precedence is the kind of thing that has to be the same everywhere to be worth anything. Once it is configurable, "where does this tool get its API key" stops having a single answer, and support becomes a matter of first asking which order this particular deployment chose. Every tool built on this crate resolving credentials identically is the point.

It also keeps the failure modes finite. Four sources in one fixed order produce a small set of outcomes that can be documented completely, including the awkward ones like a literal being refused in CI. An operator-supplied ordering multiplies those combinations and makes it impossible to state what happens when a given configuration is wrong.

The escape hatch is not configuration but code: CredentialRef's fields are public, so a tool with genuinely different needs can walk them itself. That puts the cost on the one tool that wants to differ rather than on everyone reading the docs.

Why a keychain error stops the walk instead of falling through

A missing keychain entry is a miss and the chain continues. Any other keychain failure aborts resolution and returns the error.

The distinction is between "this is not here" and "I could not tell whether this is here". A missing entry is a definite answer, and moving on is correct. A locked keyring or an unreachable D-Bus session is not an answer at all, and treating it as "not found" would mean quietly falling back to a lower-precedence source — potentially a stale literal in config — on a machine whose keychain was merely locked. The operator would never learn that the secret they carefully stored was not the one being used.

Failing loudly makes the real problem visible. The cost is the one described in what this crate does not do: a configured fallback does not rescue a broken keychain, and the error names the backend failure rather than pointing at the fallback that went unused.