---
title: rtb-credentials
description: OS keychain credential storage with precedence-aware resolution.
---
# `rtb-credentials`
Secret-handling layer for CLI tools: the `CredentialStore` async trait,
four built-in stores, a `Resolver` that walks the canonical precedence
chain, and `CredentialRef` — the deserialise-only config shape tool
authors embed in their typed configs.
Secrets cross every boundary as
[`secrecy::SecretString`](https://crates.io/crates/secrecy): `Debug`
renders `[REDACTED]`, memory is zeroed on drop. The crate treats
`&str`/`String` for a secret as a type error, not a style preference.
Part of the [phpboyscout Rust toolkit](https://rust.phpboyscout.uk);
extracted from — and battle-tested by —
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base).
## Storage modes
Three storage modes are supported:
| Env var | Process environment (shell profile, CI secret injection) | **Recommended default.** |
| OS keychain | Platform-native (macOS Keychain / Linux keyutils / Windows Credential Manager) | Linux default is pure-Rust kernel keyutils; the `linux-persistent` feature adds D-Bus Secret Service. |
| Literal | Config file | Legacy. Refused under `CI=true`. |
`Resolver` walks all three in order, plus an ecosystem-default env-var
fallback (`ANTHROPIC_API_KEY`, `GITHUB_TOKEN`, …). The precedence chain
is normative — do not reorder:
1. `cref.env` → `std::env::var(name)`.
2. `cref.keychain` → `store.get(service, account)`.
3. `cref.literal` — refused via `CredentialError::LiteralRefusedInCi`
when `CI=true`.
4. `cref.fallback_env` → `std::env::var(name)`.
## Public API
```rust
use rtb_credentials::{CredentialRef, CredentialStore, KeyringStore, Resolver};
use std::sync::Arc;
#[derive(serde::Deserialize)]
struct MyCfg {
anthropic: CredentialRef,
}
let cfg: MyCfg = /* ... */;
let store: Arc<dyn CredentialStore> = Arc::new(KeyringStore::new());
let resolver = Resolver::new(store);
let api_key = resolver.resolve(&cfg.anthropic).await?;
use secrecy::ExposeSecret;
request.header("authorization", format!("Bearer {}", api_key.expose_secret()));
```
| `CredentialStore` | Async trait: `get` / `set` / `delete`, all in `SecretString`. |
| `KeyringStore` | Platform-native keychain via the [`keyring`](https://crates.io/crates/keyring) crate. Blocking calls wrapped in `tokio::task::spawn_blocking`. |
| `EnvStore` | Read-only lookup against the process environment. |
| `LiteralStore` | Read-only single in-memory secret; test harnesses. |
| `MemoryStore` | `HashMap`-backed store behind `RwLock` — exported test fixture. |
| `CredentialRef`, `KeychainRef` | Deserialise-only config shapes (`Serialize` deliberately not derived — no blind round-trip leaks). |
| `Resolver` (+ `probe`, `ResolutionSource`, `ResolutionOutcome`) | Walks `env > keychain > literal > fallback_env`; `probe` reports which leg would win without exposing the secret. |
| `CredentialBearing` | Introspection seam: a typed config enumerates its `CredentialRef`s for `credentials list`-style tooling. Object-safe; blanket impl for `()`. |
| `CredentialError` | `thiserror` + `miette::Diagnostic`, `Clone`-able (`Io` wraps `Arc<std::io::Error>`). |
| Re-exports | `SecretString`, `ExposeSecret` from `secrecy`. |
Full API reference: [docs.rs/rtb-credentials](https://docs.rs/rtb-credentials).
## Platform behaviour and the keychain opt-in
On Linux the **default** keyring backend is kernel keyutils
(`keyring/linux-native`) — pure Rust, no system deps, **session-scoped**
persistence. This keeps builds hermetic: no `libdbus-1-dev` /
`pkg-config` required.
Tools that need reboot-persistent Linux storage enable the
`linux-persistent` Cargo feature, which extends `keyring` with the
freedesktop Secret Service (D-Bus) backend. Building with it requires
`pkg-config` and `libdbus-1-dev` on the host.
macOS Keychain and Windows Credential Manager store cross-session by
default; no feature flag needed.
Keychain support is an opt-in story for downstream tools: enable the
crate (or a wrapping framework feature) only where it is wanted —
regulated downstreams omit it, and link-time dead-code elimination keeps
`keyring` and its transitive deps out of the binary.
## Security
- `#![forbid(unsafe_code)]` at the crate root.
- Every public fn that touches a secret takes or returns `SecretString`.
- `Debug` of `SecretString` renders `[REDACTED]`.
- Literal-in-CI refusal is a **policy** check, not a technical one —
tools that want stricter enforcement set `CI=true` themselves.
- See the rust-tool-base
[Engineering Standards §1.2](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/engineering-standards.md)
for the full secret-handling rules.
## Design record
The authoritative contract is the crate's v0.1 spec, retained in the
rust-tool-base
[spec series](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-04-22-rtb-credentials-v0.1.md).
Design rationale highlights:
- **`SecretString` everywhere.** `LiteralStore::get` returns
`self.secret.clone()` so the secret never bounces through a bare
`String`; `secrecy` 0.10+ deliberately omits `Serialize` for
`SecretString`, and `CredentialRef` inherits that limitation.
- **CI detection via `CI=true` only** — the convention shared by GitHub
Actions, GitLab CI, CircleCI, Buildkite and others; broader detection
produces false positives in developer shells.
- **Keyring blocking calls in `spawn_blocking`** — platform keyring APIs
are synchronous; wrapping keeps the async trait honest on any runtime.
- **`Arc<std::io::Error>` for the `Io` variant** — so `CredentialError`
can derive `Clone` and fan out to multiple consumers without
re-allocating.