---
title: CredentialRef configuration keys
description: The four config keys a tool exposes to operators, their types, defaults, required subfields and error behaviour.
tags:
- reference
- configuration
---
# `CredentialRef` configuration keys
`CredentialRef` is the shape a tool author embeds in a typed config
struct. It is what an operator actually writes in a config file, so its
four keys are the crate's real configuration surface.
```rust
use rtb_credentials::CredentialRef;
#[derive(serde::Deserialize)]
struct MyConfig {
anthropic: CredentialRef,
}
```
## The four keys, in resolution order
Every key is optional and defaults to absent. Each carries
`#[serde(default)]`, so omitting it is always valid.
| `env` | string — the *name* of an environment variable | absent | 1st |
| `keychain` | table — `service` and `account` | absent | 2nd |
| `literal` | string — the secret itself | absent | 3rd |
| `fallback_env` | string — the *name* of an environment variable | absent | 4th |
A fully populated reference in YAML:
```yaml
anthropic:
env: MYTOOL_ANTHROPIC_KEY
keychain:
service: mytool
account: anthropic
literal: sk-ant-...
fallback_env: ANTHROPIC_API_KEY
```
Nothing requires all four. A reference with only `fallback_env` set is
valid and common; so is an entirely empty one, which always fails to
resolve.
## `env` and `fallback_env` hold variable names, not values
Both keys take the *name* of an environment variable. Writing the secret
itself into `env` does not work — the resolver calls
`std::env::var(name)` with whatever string is supplied and treats a
missing variable as a miss.
The practical failure this produces: a config with
`env: sk-ant-abc123` silently resolves nothing, because no variable
called `sk-ant-abc123` exists. The chain then continues to the next leg
and usually ends in `NotFound`. The secret has also now been written into
a config file in plain text. The key for an inline secret is `literal`.
The two keys behave identically at resolution time. The distinction is
one of intent: `env` is the variable this tool defines for itself, and
`fallback_env` is an ecosystem-wide name such as `ANTHROPIC_API_KEY` or
`GITHUB_TOKEN` that the tool did not invent and shares with other
software.
## `keychain` requires both `service` and `account`
The `keychain` key deserialises into a `KeychainRef`, which has two
fields and **neither is optional**:
| `service` | string | yes |
| `account` | string | yes |
```yaml
keychain:
service: mytool
account: anthropic
```
Omitting either field is a deserialisation error, not a resolution-time
miss — the config fails to load. `KeychainRef` carries
`#[serde(deny_unknown_fields)]`, so a misspelled subkey such as
`accont` is also a load-time error rather than a silently ignored entry.
The two strings are passed straight through to the platform keychain as
the service and account identifiers. `service` is conventionally the
tool's name or a provider identifier; `account` is conventionally a user
login or a fixed label such as `default`. Neither is interpreted by this
crate.
## Unknown keys are a load-time error
`CredentialRef` carries `#[serde(deny_unknown_fields)]`. A key that is
not one of the four documented above fails deserialisation.
```yaml
anthropic:
environment: MYTOOL_KEY # error: unknown field `environment`
```
This is deliberate: a mistyped key on a credential would otherwise mean
the credential silently resolves from a lower-precedence source, or not
at all, with no signal that the config said something the tool did not
understand.
The trade-off is that config files cannot carry extra annotations —
comments describing a credential must be file comments, not extra keys.
## `CredentialRef` cannot be serialised back to config
`CredentialRef` derives `Debug`, `Clone`, `Default` and `Deserialize`.
It deliberately does **not** derive `Serialize`.
A tool that loads config, changes an unrelated setting and writes the
whole struct back cannot compile if that struct contains a
`CredentialRef`. This is the intended barrier: it makes "resolved secret
accidentally written to disk in plain text" a compile error rather than
a runtime accident. Writing a credential back has to be a deliberate,
separate code path.
The underlying reason is `secrecy`. `SecretString` is
`SecretBox<str>`, and `secrecy` only provides
`impl Serialize for SecretBox<T>` where `T: SerializableSecret + Sized`
— an opt-in marker trait. `str` is unsized and does not implement the
marker, so `SecretString` has no `Serialize` impl and neither can any
struct containing one.
`SecretString` *does* implement `Deserialize`, which is why `literal`
can be read from config in the first place. The direction is
deliberately asymmetric: in, but not out.
## `KeychainRef` *is* serialisable
Unlike its parent, `KeychainRef` derives `Serialize` as well as
`Deserialize`:
```rust
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct KeychainRef { /* service, account */ }
```
This is consistent rather than contradictory — a `KeychainRef` holds no
secret, only the two identifiers used to look one up. A tool can safely
serialise a `KeychainRef` into config, a status report or a log line.
The asymmetry is worth knowing because a struct holding a `KeychainRef`
can derive `Serialize`, while one holding a whole `CredentialRef`
cannot.
## `Default` gives an empty reference that never resolves
`CredentialRef::default()` sets all four keys to `None`. Resolving it
returns `CredentialError::NotFound` with the name
`<unnamed credential>`.
This matters when a config struct derives `Default`: a credential the
operator never configured is not an error at load time, only at the
point something tries to use it.