# AGENTS.md
This file provides guidance to AI coding agents (Claude Code, agy, codex, etc.) when working with code in this repository.
Ways of working are deliberately not repeated here. They live in the phpboyscout
skills, and naming a skill tends to age better than restating it, since the
restatement goes stale while the skill does not.
## What this is
`rtb-credentials` is a crate in the
[phpboyscout Rust toolkit](https://rust.phpboyscout.uk), one of the small,
framework-free crates extracted from
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base). It answers one
question: *where does this secret live, and can you hand it to me?*
Two halves that compose rather than overlap. `CredentialStore` (`src/store.rs:18`)
is the backend seam — `get` / `set` / `delete`, everything in `SecretString`, with
`KeyringStore`, `EnvStore`, `LiteralStore` and `MemoryStore` shipped. `Resolver`
(`src/resolver.rs:22`) walks a `CredentialRef` through the fixed chain
`env > keychain > literal > fallback_env` and returns the first hit; `probe` reports
which leg would win without handing back the value. It holds nothing about identity,
validates nothing, caches nothing and expires nothing;
`docs/explanation/limitations.md` is the full list, and the first thing to read.
**The seam exposes no enumeration.** `CredentialStore` is exactly those three
methods; nothing here asks a keychain what it holds. Listing comes from the other
direction — `CredentialBearing` (`src/bearing.rs:52`), implemented by a downstream tool
on its own typed config so `rtb-cli`'s `credentials` subtree walks what the *config*
declares. Keep that precise in writing: `keyring-core` 1.0.0, under the pinned
`keyring` 4.1.5, does ship `Store::search`; this crate simply never reaches for it,
which is narrower and far more defensible than "keychains cannot be enumerated".
If you know the Go side, the shapes genuinely correspond:
[`go/credentials`](https://gitlab.com/phpboyscout/go/credentials) makes the same env /
keychain / literal decision behind a `Backend` and likewise stores nothing about
identity. Where they part company is already in the limitations page — the Go module's
registration seam lets a build omit keychain support, and this crate has no equivalent.
## Where it has got to
Pre-1.0 (0.6.3), so a public shape can still move, and `cargo-semver-checks` runs in
the release job to keep a move deliberate. Releases go through `release-plz`
(`release-plz.toml`) — **not** the releaser-pleaser flow the Go side uses, so do not
import its conventions.
Five crates depend on this one, all by published crates.io version rather than path,
all currently requiring `0.6.0`: `rtb-app` (`crates/rtb-app/Cargo.toml:23`),
`rtb-cli` (`crates/rtb-cli/Cargo.toml:24`) and `rtb-update`
(`crates/rtb-update/Cargo.toml:22`) unconditionally; `rtb-forge` (`rust/forge`
`Cargo.toml:121`) behind its default-on `git` feature; `rtb` (`rust-tool-base`
`crates/rtb/Cargo.toml:60`) behind its default-on `credentials` feature. `rtb-forge`
is the interesting one — its `git` feature exists so `Repo::clone` / `fetch` /
`push` can call `Resolver::resolve`, so a change to `CredentialRef` or `Resolver`
changes how every forge backend authenticates.
## The traps
**Nothing here reaches a sibling until it is released.** Dependents resolve this crate
from crates.io, not by path. "`main` does it" and "the crate does it" are different
claims — check the tag before telling a caller a capability exists.
**`unsafe_code` is `deny` here, not `forbid`, and the difference is load-bearing.**
`rust-tool-base` states `#![forbid(unsafe_code)]` as the production guarantee, and
`src/lib.rs:44` does ship exactly that. The *manifest* lint (`Cargo.toml:20`) is
`deny` on purpose, because `tests/bdd.rs:5` needs `#![allow(unsafe_code)]` for its
env-var mutation. Promote the manifest to `forbid` and the BDD binary stops
compiling — `forbid` cannot be lifted by an inner `allow`.
**`CI` is not a disjoint variable, whatever the comments say.** `tests/unit.rs:8`
claims "disjoint var names per test", but `t8_resolver_precedence` removes `CI`
while `t9_literal_refused_in_ci` sets it, and `cargo test` runs both on threads in
one process. `tests/steps/cred_steps.rs:180` claims more strongly that "scenarios
run sequentially in cucumber"; cucumber 0.23.0 defaults to
`max_concurrent_scenarios: Some(64)`. Neither has been observed to fail — 40
consecutive `cargo test --test unit` runs came back green — but nothing enforces
the ordering. Nextest's process-per-test isolation is what holds it together, and
`just test` is `cargo nextest run || cargo test`, so a nextest-only failure quietly
re-runs under the one mode lacking it. Run `cargo nextest run` alone when you touch
these tests.
**Two comments describe the monorepo, not this repo.** `rust-toolchain.toml` justifies
its exact-version pin with trybuild `.stderr` fixtures, of which there are none here,
and `src/lib.rs:41` / `tests/unit.rs:4` cite a
`docs/development/specs/2026-04-22-…` path this tree does not contain.
`just ci` runs `fmt-check`, `lint`, `docs`, `test` and `audit`, and passes clean on
`main`. CI adds cross-OS runs and a 45% coverage gate — a current-floor ratchet,
not the estate standard, because keychain paths need a keychain the container lacks.
## Which skills apply here
| Adding or moving any check a secret passes through | `centralize-security-footguns` |
| Deciding whether `CredentialStore` earns another method | `deep-modules` |
| Before writing code for anything more than a typo | `spec-driven-development` |
| Deciding whether a change is finished (docs ship with it) | `definition-of-done` |
| Writing or restructuring this crate's `docs/` tree | `diataxis-docs`, `checkable-claims` |
| Drawing a conclusion from a checkout you did not just fetch | `verify-dont-trust` |
| Sizing a Renovate bump on `keyring`, `secrecy` or the CI components | `assess-before-bumping` |
| Releasing this crate and its dependents together | `release-train` |
| Writing a commit message or a merge request description | `conventional-commits`, `pre-1-0-release-safety` |
| Committing, branching, merging, or opening a merge request | `forge-publish-workflow` |
> Skills are a Claude Code mechanism, shipped by the
> [phpboyscout marketplace](https://gitlab.com/phpboyscout/claude-code-plugins); an
> agent without them should treat a named skill as a topic to ask about rather than
> a file it can load. There are no Rust-specific skills yet, so this is the
> language-agnostic set — Go-shaped rows are left out rather than relabelled, and
> `releaser-pleaser-releases` does not apply because releases run on release-plz.
## House rules
- Linear history. Rebase and fast-forward; never squash-merge from the UI.
- Conventional Commits, and the type decides whether a release is cut. Only
`feat` and `fix` release. A change that repoints or removes a public interface
is `feat`, not `refactor`, or it lands and never ships.
- No AI attribution in anything published, and never at-mention anyone.
- Never cut a release yourself. That is the maintainer's call, every time.