1use std::sync::LazyLock;
2
3static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
4 regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
5});
6
7/// Whether a given environment variable name should have its value redacted
8pub fn should_redact(env_var_name: &str) -> bool {
9 const REDACTED_SUFFIXES: &[&str] = &[
10 "KEY",
11 "TOKEN",
12 "PASSWORD",
13 "SECRET",
14 "PASS",
15 "CREDENTIALS",
16 "LICENSE",
17 ];
18 REDACTED_SUFFIXES
19 .iter()
20 .any(|suffix| env_var_name.ends_with(suffix))
21}
22
23/// Redact a string which could include a command with environment variables
24pub fn redact_command(command: &str) -> String {
25 REDACT_REGEX
26 .replace_all(command, |caps: ®ex::Captures| {
27 let var_name = &caps[1];
28 let value = &caps[2];
29 if should_redact(var_name) {
30 format!(r#"{}="[REDACTED]""#, var_name)
31 } else {
32 format!("{}={}", var_name, value)
33 }
34 })
35 .to_string()
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn test_redact_string_with_multiple_env_vars() {
44 let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
45 let result = redact_command(input);
46 let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
47 assert_eq!(result, expected);
48 }
49}