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:
- Token prefix. Starts with
sk-,pk-,ghp_,gho_,ghs_,xoxb-,xoxp-, oreyJ(the leading bytes of a JWT header). - 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. - 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.
- 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. - Known secret env var. A value registered with
Redactor::with_secret_env. This is how the runner promotesbearer_token_envreferences so the resolved bearer never appears in cassettes or logs. - Sensitive HTTP header by name.
Authorization,Cookie,Proxy-Authorization, andSet-Cookie. Always redacted regardless of value. - Sensitive HTTP header by pattern. Any header whose name matches (case-insensitive)
*-Token,*-Key,*-Secret, or*-Auth*. SoX-Api-Token,X-Service-Key,X-Custom-Secret, andX-Authenticationall redact.
The redaction format
Redacted output uses the shape [REDACTED:<hint>] so two distinct redactions can still be told apart on screen:
| Hint | Source |
|---|---|
[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:
- Plants secret-shaped values into env files and
--varflags. - Runs
mcptest doctorandmcptest --verbose run. - Asserts the raw value does not appear on stdout.
- Asserts the matching
[REDACTED:<hint>]token does appear. - Asserts
--show-secretsopts back into the raw value (so the escape hatch is also under test).
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.
| Surface | Status |
|---|---|
--verbose protocol trace | future runner |
| Verbose variable summary | wired through resolver redaction |
| Doctor report (dotenv values, var listing) | wired through resolver redaction |
| Pretty reporter | wired via Report::redacted (+ with_redactor) |
| JSON reporter | wired via Report::redacted (+ with_redactor) |
| JUnit XML reporter | wired via Report::redacted (+ with_redactor) |
| Markdown reporter | wired via Report::redacted (+ with_redactor) |
| HTML reporter | wired via Report::redacted (+ with_redactor) |
| SARIF reporter | wired via Report::redacted (+ with_redactor) |
| GitLab Code Quality reporter | wired via Report::redacted (+ with_redactor) |
| Upload reporter (HTTPS body) | future runner (payload redaction lands with the runner) |
| Cassette dump | future runner |
| Cache key / cache dump | future 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:
Report::redactedadapter (recommended). Build the redactor at the dispatch site, callreport.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.- Per-reporter
with_redactorbuilder. Each reporter struct accepts an optionalArc<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
- Library:
crates/mcptest-core/src/redaction.rs - Adapter:
mcptest_core::report::Report::redacted - Audit suite:
crates/mcptest/tests/redaction_audit.rs - Escape hatch flag:
--show-secrets