Warn callers not to log resolved secrets
The global tracing subscriber has no redaction layer, so the plain
String returned by SecretRegistry::resolve must not reach tracing or
Sentry. Document the contract at both API entry points and pin the
SecretRegistry Debug redaction with a regression test.

Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
change utozqypwvsnzpxlrulsyrupzxtqzxpxp
commit aa56206dff3f1c1237184c875f63b762f7195f1f
author Alpha Chen <alpha@kejadlen.dev>
date
parent vwyrktsp
diff --git a/src/ci/runtime.rs b/src/ci/runtime.rs
index 704520b..5f75d10 100644
--- a/src/ci/runtime.rs
+++ b/src/ci/runtime.rs
@@ -194,6 +194,12 @@ impl Runtime {
     /// Resolve a declared secret by name, caching it for redaction.
     /// Errors if the name isn't declared or the secret's source
     /// can't be read.
+    ///
+    /// The returned `String` is the plain, revealed value — never
+    /// trace or log it directly. See [`SecretRegistry::resolve`] for
+    /// the full caveat.
+    ///
+    /// [`SecretRegistry::resolve`]: crate::secret::SecretRegistry::resolve
     pub(super) fn secret(&self, name: &str) -> super::error::Result<String> {
         self.registry.borrow_mut().resolve(name).map_err(Into::into)
     }
diff --git a/src/secret.rs b/src/secret.rs
index 40efeb3..512618a 100644
--- a/src/secret.rs
+++ b/src/secret.rs
@@ -219,6 +219,13 @@ impl SecretRegistry {
     /// common short strings like "true" or "yes" is too high. A warn
     /// is emitted so an operator can see why a short token is showing
     /// up unredacted in CI output.
+    ///
+    /// The returned `String` is the plain, revealed value. Do not pass
+    /// it to `tracing` or any other log sink — the global tracing
+    /// subscriber has no redaction layer, so a leaked value would
+    /// reach stderr and Sentry. Route it into a surface that goes
+    /// through [`redact`] (e.g. `sh` command args, ShOutput) or wrap
+    /// it in a type whose `Debug`/`Display` impl redacts.
     pub fn resolve(&mut self, name: &str) -> Result<String> {
         let secret = self
             .declared
@@ -289,6 +296,22 @@ mod tests {
         );
     }
 
+    #[test]
+    fn registry_debug_does_not_leak_revealed_values() {
+        let mut registry: SecretRegistry =
+            vec![("github_token", "abcdefghijklmnop_long_enough")].into();
+        let _ = registry.resolve("github_token").unwrap();
+        let debug_output = format!("{registry:?}");
+        assert!(
+            !debug_output.contains("abcdefghijklmnop_long_enough"),
+            "SecretRegistry Debug must not leak revealed values: {debug_output}"
+        );
+        assert!(
+            debug_output.contains("github_token"),
+            "SecretRegistry Debug should still surface declared names: {debug_output}"
+        );
+    }
+
     #[test]
     fn reveal_returns_plain_value() {
         let secret = SecretString::from("plain_value");