fix(cli): mask credential env values in --help output (#3570)

clap renders live env var values in help text by default. Three args
carrying credentials were exposed this way:

- `BUZZ_PRIVATE_KEY` in `buzz-cli` (`crates/buzz-cli/src/lib.rs`)
- `BUZZ_AUTH_TAG` in `buzz-cli`
- `BUZZ_PRIVATE_KEY` in `buzz-acp` (`crates/buzz-acp/src/config.rs`)

Add `hide_env_values = true` to each. Env var names remain visible for
discoverability; only their runtime values are withheld from `--help`
output.

Also adds a regression guard in each crate's test module that walks the
clap command tree (recursing into subcommands for `buzz-cli`) and
asserts every arg whose env var name contains `KEY`, `SECRET`, `TOKEN`,
`PASSWORD`, `CRED`, or `AUTH` has `hide_env_values` set. This prevents
future credential-bearing args from being added without the masking in
place.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-07-29 12:23:02 -04:00
committed by GitHub
parent 9752b816a9
commit 9beb3b8c6e
2 changed files with 75 additions and 3 deletions

View File

@@ -82,11 +82,11 @@ struct Cli {
relay: String,
/// Nostr private key (hex or nsec). This is the CLI's identity.
#[arg(long, env = "BUZZ_PRIVATE_KEY")]
#[arg(long, env = "BUZZ_PRIVATE_KEY", hide_env_values = true)]
private_key: Option<String>,
/// NIP-OA auth tag JSON (owner attestation). Injected into every signed event.
#[arg(long, env = "BUZZ_AUTH_TAG")]
#[arg(long, env = "BUZZ_AUTH_TAG", hide_env_values = true)]
auth_tag: Option<String>,
/// Output format: 'json' (default, full fields) or 'compact' (reduced fields).
@@ -2075,4 +2075,46 @@ mod tests {
);
}
}
/// Collect all args (recursing into subcommands) whose env var name looks
/// like a credential but does NOT have `hide_env_values` set.
fn collect_unhidden_secret_args(cmd: &clap::Command) -> Vec<(String, String)> {
const SECRET_PATTERNS: &[&str] = &["KEY", "SECRET", "TOKEN", "PASSWORD", "CRED", "AUTH"];
let mut violations: Vec<(String, String)> = Vec::new();
for arg in cmd.get_arguments() {
if let Some(env_key) = arg.get_env() {
let env_name = env_key.to_string_lossy().to_uppercase();
let is_secret = SECRET_PATTERNS.iter().any(|pat| env_name.contains(pat));
if is_secret && !arg.is_hide_env_values_set() {
violations.push((cmd.get_name().to_string(), env_name));
}
}
}
for sub in cmd.get_subcommands() {
violations.extend(collect_unhidden_secret_args(sub));
}
violations
}
/// Every arg whose env var name contains KEY/SECRET/TOKEN/PASSWORD/CRED/AUTH
/// must set `hide_env_values = true` to prevent credential leakage in --help.
#[test]
fn secret_env_args_hide_their_values_in_help() {
let cmd = Cli::command();
let violations = collect_unhidden_secret_args(&cmd);
assert!(
violations.is_empty(),
"Found secret-bearing env args without hide_env_values=true. \
Add `hide_env_values = true` to each:\n{}",
violations
.iter()
.map(|(cmd, env)| format!(" command={cmd:?} env={env:?}"))
.collect::<Vec<_>>()
.join("\n")
);
}
}