Redact command environment variables from log output (#32985)

Peter Tripp created

Before/After (linebreaks added for readability)
```log 
# before
INFO  [project::context_server_store::extension]
loaded command for context server mcp-server-github:
Command { 
  command: "/Users/peter/Library/Application Support/Zed/extensions/work/mcp-server-github/github-mcp-server-v0.5.0/github-mcp-server", 
  args: ["stdio"], 
  env: [("GITHUB_PERSONAL_ACCESS_TOKEN", "gho_WOOOOOOOOOOOOOOO")] 
}

#after
INFO  [project::context_server_store::extension]
loaded command for context server mcp-server-github:
Command {
  command: "/Users/peter/Library/Application Support/Zed/extensions/work/mcp-server-github/github-mcp-server-v0.5.0/github-mcp-server",
  args: ["stdio"],
  env: [("GITHUB_PERSONAL_ACCESS_TOKEN", "[REDACTED]")]
}
```

Release Notes:

- Redact sensitive environment variables from MCP logs

Change summary

crates/context_server/src/context_server.rs | 19 ++++++++++++++++++-
crates/extension/src/types.rs               | 19 ++++++++++++++++++-
crates/util/src/redact.rs                   |  8 ++++++++
crates/util/src/util.rs                     |  1 +
4 files changed, 45 insertions(+), 2 deletions(-)

Detailed changes

crates/context_server/src/context_server.rs 🔗

@@ -16,6 +16,7 @@ use gpui::AsyncApp;
 use parking_lot::RwLock;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use util::redact::should_redact;
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct ContextServerId(pub Arc<str>);
@@ -26,13 +27,29 @@ impl Display for ContextServerId {
     }
 }
 
-#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
 pub struct ContextServerCommand {
     pub path: String,
     pub args: Vec<String>,
     pub env: Option<HashMap<String, String>>,
 }
 
+impl std::fmt::Debug for ContextServerCommand {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let filtered_env = self.env.as_ref().map(|env| {
+            env.iter()
+                .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v }))
+                .collect::<Vec<_>>()
+        });
+
+        f.debug_struct("ContextServerCommand")
+            .field("path", &self.path)
+            .field("args", &self.args)
+            .field("env", &filtered_env)
+            .finish()
+    }
+}
+
 enum ContextServerTransport {
     Stdio(ContextServerCommand),
     Custom(Arc<dyn crate::transport::Transport>),

crates/extension/src/types.rs 🔗

@@ -5,6 +5,8 @@ mod slash_command;
 
 use std::ops::Range;
 
+use util::redact::should_redact;
+
 pub use context_server::*;
 pub use dap::*;
 pub use lsp::*;
@@ -14,7 +16,6 @@ pub use slash_command::*;
 pub type EnvVars = Vec<(String, String)>;
 
 /// A command.
-#[derive(Debug)]
 pub struct Command {
     /// The command to execute.
     pub command: String,
@@ -24,6 +25,22 @@ pub struct Command {
     pub env: EnvVars,
 }
 
+impl std::fmt::Debug for Command {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let filtered_env = self
+            .env
+            .iter()
+            .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v }))
+            .collect::<Vec<_>>();
+
+        f.debug_struct("Command")
+            .field("command", &self.command)
+            .field("args", &self.args)
+            .field("env", &filtered_env)
+            .finish()
+    }
+}
+
 /// A label containing some code.
 #[derive(Debug, Clone)]
 pub struct CodeLabel {

crates/util/src/redact.rs 🔗

@@ -0,0 +1,8 @@
+/// Whether a given environment variable name should have its value redacted
+pub fn should_redact(env_var_name: &str) -> bool {
+    const REDACTED_SUFFIXES: &[&str] =
+        &["KEY", "TOKEN", "PASSWORD", "SECRET", "PASS", "CREDENTIALS"];
+    REDACTED_SUFFIXES
+        .iter()
+        .any(|suffix| env_var_name.ends_with(suffix))
+}

crates/util/src/util.rs 🔗

@@ -4,6 +4,7 @@ pub mod command;
 pub mod fs;
 pub mod markdown;
 pub mod paths;
+pub mod redact;
 pub mod serde;
 pub mod shell_env;
 pub mod size;