mcptest docs GitHub

Secret redaction policy

mcptest runs against live MCP servers, which means it routinely sees bearer tokens, API keys, and OAuth secrets while it works. Any of those values can leak into a reporter line, a verbose log, a cassette dump, a doctor report, or a typed error message. To stop that from happening in eight slightly-different ways, every CLI surface that prints user data goes through one library: mcptest_core::redaction::Redactor.

This page documents the policy that library enforces.

Run this example. examples/security-tools-list.json is a captured catalog you can scan; any secret values are redacted in every reporter, including the SARIF output below.

mcptest security examples/security-tools-list.json --format sarif

The seven secret categories

A value is redacted when it falls into any of these buckets:

  1. Token prefix. Starts with sk-, pk-, ghp_, gho_, ghs_, xoxb-, xoxp-, or eyJ (the leading bytes of a JWT header).
  2. Hex blob. 32 or more characters, every character [0-9a-f]. Catches SHA-256 digests used as opaque tokens, hex-encoded HMAC keys, and most "session" identifiers.
  3. High-entropy base64-ish blob. 32 or more characters, drawn from the base64 / base64url alphabet, with at least 2 digits, 2 uppercase letters, and 2 lowercase letters present. Catches generated API keys without a distinctive prefix.
  4. Secret-named JSON key. Any non-empty string under a JSON key matching (case-insensitive) password, token, secret, api_key, client_secret, access_token, refresh_token, or a generic *_key / *-key.
  5. Known secret env var. A value registered with Redactor::with_secret_env. This is how the runner promotes bearer_token_env references so the resolved bearer never appears in cassettes or logs.
  6. Sensitive HTTP header by name. Authorization, Cookie, Proxy-Authorization, and Set-Cookie. Always redacted regardless of value.
  7. Sensitive HTTP header by pattern. Any header whose name matches (case-insensitive) *-Token, *-Key, *-Secret, or *-Auth*. So X-Api-Token, X-Service-Key, X-Custom-Secret, and X-Authentication all redact.

The redaction format

Redacted output uses the shape [REDACTED:<hint>] so two distinct redactions can still be told apart on screen:

HintSource
[REDACTED:token]Category 1 (token prefix)
[REDACTED:length=N]Categories 2 and 3 (hex / high-entropy)
[REDACTED:hint=KEY]Category 4 (matched JSON key)
[REDACTED:env]Category 5 (registered env value)
[REDACTED:header]Categories 6 and 7 (HTTP headers)

Hints never reveal the secret's value. Lengths leak only the number of characters, which is well below what is useful to an attacker but useful to a developer debugging "is the long secret or the short one in this field?"

The --show-secrets escape hatch

When a developer is debugging a misconfiguration locally and wants to see the raw value, they can pass --show-secrets. Every redaction-aware surface honors it. The flag is unsafe by design and prints a banner before dumping values; do not pass it in CI.

How the policy is enforced

The contract that protects users is end-to-end: "no CLI surface leaks a secret-shaped value." Unit tests cover the library, but only an integration test that runs the real binary can verify the surfaces are wired correctly.

That suite lives at crates/mcptest/tests/redaction_audit.rs. It:

When a new surface lands (JSON reporter, cassette dump, etc.) the contributor adds a test to this file before merging.

Surfaces audit

Every output surface that may print user-supplied data is either wired through Redactor or documented as unable to leak. Rows that say "future runner" are honest deferrals: the surface does not exist yet, so the wiring lands when the surface lands.

SurfaceStatus
--verbose protocol tracefuture runner
Verbose variable summarywired through resolver redaction
Doctor report (dotenv values, var listing)wired through resolver redaction
Pretty reporterwired via Report::redacted (+ with_redactor)
JSON reporterwired via Report::redacted (+ with_redactor)
JUnit XML reporterwired via Report::redacted (+ with_redactor)
Markdown reporterwired via Report::redacted (+ with_redactor)
HTML reporterwired via Report::redacted (+ with_redactor)
SARIF reporterwired via Report::redacted (+ with_redactor)
GitLab Code Quality reporterwired via Report::redacted (+ with_redactor)
Upload reporter (HTTPS body)future runner (payload redaction lands with the runner)
Cassette dumpfuture runner
Cache key / cache dumpfuture runner

Each wired reporter row is covered by a unit test next to the reporter (*_redacts_token_shaped_test_name) and an integration test in the audit suite (crates/mcptest/tests/redaction_audit.rs, report_<format>_does_not_leak_token_shaped_test_name).

Two integration patterns

The Redactor library can be wired into a reporter in two ways:

  1. Report::redacted adapter (recommended). Build the redactor at the dispatch site, call report.redacted(&r) once, and hand the redacted copy to any reporter. Every reporter sees the same view, the wiring is one line, and adding a new format does not require touching redaction code.
  2. Per-reporter with_redactor builder. Each reporter struct accepts an optional Arc<Redactor> via a builder method. Useful when a single reporter is driven directly (an embedded scenario, a test), or when a future surface wants its own redaction strategy. Reporters render verbatim when no redactor is attached.

Both patterns share the same Redactor library, so adding a category (new prefix, new env name) updates every wired surface at once.

On --no-redaction

The CLI deliberately ships no opt-out flag that disables redaction globally. The escape hatch is --show-secrets, which is the resolver-level opt-in for a human running locally. A general --no-redaction flag would invite copy-pasted CI snippets that silently suppress the policy, which defeats the audit. If a contributor needs unredacted output for debugging, they pass --show-secrets and accept that it is unsafe for shared logs.

Reference