# 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 not repeated here; they live in the phpboyscout skills.
## What this crate is
`rtb-telemetry` records **product analytics about a user's tool** — which command they
ran, whether it worked — behind a two-level opt-in. It is not a logging or tracing
layer: no `tracing` dependency, no spans, no subscriber, and `OtlpSink` emits
OpenTelemetry *log records* only (`src/otlp_sink.rs:106-115`). Nothing is collected
automatically, and no environment variable or config file is read — `consent` persists a
decision to a path you hand it, and `rtb-cli` supplies the layer above that. Collection
is off at both levels: a tool author opts in at compile time by depending on the crate,
a user opts in at runtime via `CollectionPolicy`, and a `Disabled` context returns
before constructing an event and never derives a machine ID (`src/context.rs:69-75`,
`src/context.rs:184-194`).
`src/lib.rs:31` ships `#![forbid(unsafe_code)]`; `Cargo.toml:20` sets `unsafe_code =
"deny"` at package level so a test file can `#![allow]` where it genuinely must. Both
hold *here* — do not carry the claim across the toolkit, where at least one crate
deliberately uses `deny` at its crate root.
## The redaction seam
Be precise about coverage here: overstating it tells the next contributor they may
record something they may not. `Event::redacted()` (`src/event.rs:113-126`) clones the
event and passes **`args` and `err_msg`, and only those two**, through
`rtb_redact::string`. `FileSink` (`src/sink.rs:136`), `HttpSink`
(`src/http_sink.rs:108`) and `OtlpSink` (`src/otlp_sink.rs:97`) call it before
serialising. `MemorySink` (`src/sink.rs:80`) and `NoopSink` (`src/sink.rs:36`) do not,
because neither lets the event leave the process — so a `MemorySink` assertion shows
what the caller passed, not what a collector would receive. `attrs`, `name`, `tool`,
`tool_version` and `machine_id` are never scrubbed by anything.
**The trap.** `TelemetryContext` exposes only `record` and `record_with_attrs`
(`src/context.rs:69`, `src/context.rs:79`), and neither sets `args` or `err_msg` — so
the one free-form field reachable through the context API is the one that is *not*
redacted, and reaching the two that are means building an `Event` with `with_args` /
`with_err_msg` and emitting it through a sink yourself. Keep `attrs` an enumerated
vocabulary — command name, outcome, duration bucket — or scrub the value before
inserting it.
A pattern set, not a proof — measured against the locked `rtb-redact` 0.6.1:
| `postgres://app:hunter2@db/mydb` | `postgres://[redacted]@db/mydb` |
| `password=` or `client_secret=` with any value | `…=[redacted]` |
| `{"password":"hunter2"}` | unchanged — nothing parses JSON |
| `?key=…`, `secret_key=…`, `private_key=…` | unchanged — no bare `key` rule, and `_` blocks the `secret` branch |
| `Cookie: session=abc123; theme=dark` | unchanged — `string` never reads `SENSITIVE_HEADERS` |
| `glpat-` followed by ten characters | unchanged — a named prefix redacts only at 20 characters or more |
| `glrt-` followed by twenty-four characters | unchanged — `glrt-` is not a known prefix, and it is under the 40-character opaque-run bound |
`docs/explanation/redaction-boundary.md` argues the split, and [what rtb-redact does not
do](https://redact.rust.phpboyscout.uk) inventories the gaps. Read one before widening what an event carries.
## Where it sits, and the gate
`rtb-redact` is the only phpboyscout crate this one depends on: requested as `"0.6.0"`
(`Cargo.toml:58`), resolved to 0.6.1 in `Cargo.lock`, while crates.io is at 0.6.3 — whose
changes since 0.6.1 are CI and branding only, no rule moved. **Inter-repo dependencies
resolve by published crates.io version, not by path,** so a redaction fix reaches this crate
only when someone bumps here, and a change here reaches a consumer only when it is released.
Two crates in the estate depend on this one, both elsewhere: `rtb-cli`
(`rust/cli/crates/rtb-cli/Cargo.toml:25`, unconditional) and the `rust-tool-base`
umbrella `rtb` (`crates/rtb/Cargo.toml:62`, optional behind a `telemetry` feature that
is *not* in that crate's `default` set at line 29). Turning that feature off does not
drop this crate from an `rtb` build, though: the default `cli` feature pulls `rtb-cli`,
which requires it unconditionally.
`just ci` runs `fmt-check`, `lint`, `docs`, `test` and `audit`. On `main` as of this
writing, `cargo test --all-features` passes 25 unit tests, 11 consent tests, 35 BDD steps
and one doctest, and `cargo clippy --all-targets --all-features -- -D warnings` is clean.
Releases run through **colophon** (`.colophon.yaml`), with cicd's `rust-release` publishing
the tag; MSRV is `1.88`, toolchain pinned exactly in `rust-toolchain.toml`.
## Traps
- **OTLP/HTTP does not work** (re-tested on 0.7.4 with opentelemetry 0.33.0). An
`http(s)://` endpoint not on `:4317` panics inside the SDK's exporter thread and
exports nothing, while `emit` still returns `Ok(())`. Use `grpc://`, `grpcs://` or `:4317`; detail in `docs/reference/sinks.md`.
- **`MachineId::derive` reads `/etc/machine-id`** and falls back to a fresh UUID
when it is absent, so the "same salt, same ID" test fails on a host without
one. CI seeds a stable value (`.gitlab-ci.yml:16`); a bare container needs it too.
- **The salt must be unique and stable per tool** — the same literal in two tools
gives them identical machine IDs (`src/context.rs:125-154`).
- **`remote-sinks` is off by default,** so plain `just test` and `just lint` never
compile `HttpSink` or `OtlpSink`. Run both with `--all-features` as well.
## Which skills apply here
> The estate has no Rust-specific skills yet, so this is the language-agnostic
> set; revisit once that gap is filled. Skills ship from 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.
| Touching the redaction seam, or a field that reaches a sink | `centralize-security-footguns` (Go examples; its third section is this seam) |
| Starting a feature, a component, or any non-trivial change | `spec-driven-development` |
| Calling a change finished | `definition-of-done` |
| Drawing a conclusion from code, a test run or a checkout | `verify-dont-trust` |
| Writing anything others will read and check | `checkable-claims` |
| Writing or restructuring this crate's `docs/` | `diataxis-docs` |
| Working in a repo other than the one you were invoked in | `cross-repo-worktree` |
| Judging a Renovate bump of a pinned CI component | `assess-before-bumping` |
| Writing a commit message or a merge request description | `conventional-commits`, `pre-1-0-release-safety` |
| Committing, branching, or opening a merge request | `forge-publish-workflow` |
| Releasing several toolkit crates together | `release-train` |
## 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. Never cut one yourself; that is the maintainer's call.
- No AI attribution in anything published, and never at-mention anyone.