use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, serde::Serialize)]
pub struct Stream {
pub harness: &'static str,
pub stream: &'static str,
pub on: bool,
pub destination: String,
pub off_with: &'static str,
pub foreign: bool,
}
fn home() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_default()
}
fn read_json(path: &Path) -> Option<serde_json::Value> {
serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()
}
fn setting(name: &str, file_env: &BTreeMap<String, String>) -> Option<String> {
std::env::var(name)
.ok()
.or_else(|| file_env.get(name).cloned())
}
fn truthy(value: Option<String>) -> bool {
matches!(
value.as_deref().map(str::trim),
Some("1") | Some("true") | Some("TRUE") | Some("yes")
)
}
fn ours(endpoint: &str) -> bool {
let host = endpoint
.split("://")
.nth(1)
.unwrap_or(endpoint)
.split(['/', ':'])
.next()
.unwrap_or("");
if matches!(host, "localhost" | "127.0.0.1" | "::1" | "[::1]") {
return true;
}
std::env::var("SUPERCODE_TEAMS_ORIGIN")
.ok()
.map(|origin| origin.contains(host) && !host.is_empty())
.unwrap_or(false)
}
fn claude(out: &mut Vec<Stream>) {
let settings = home().join(".claude").join("settings.json");
if !settings.exists() && !home().join(".claude").exists() {
return;
}
let env: BTreeMap<String, String> = read_json(&settings)
.and_then(|v| v.get("env").cloned())
.and_then(|v| v.as_object().cloned())
.map(|m| {
m.into_iter()
.map(|(k, v)| {
let text = v
.as_str()
.map(str::to_string)
.unwrap_or_else(|| v.to_string());
(k, text)
})
.collect()
})
.unwrap_or_default();
let quiet = truthy(setting("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", &env));
out.push(Stream {
harness: "claude-code",
stream: "operational metrics",
on: !(quiet || truthy(setting("DISABLE_TELEMETRY", &env))),
destination: "Anthropic and its third-party metrics logging".into(),
off_with: "DISABLE_TELEMETRY=1 in ~/.claude/settings.json \"env\"",
foreign: false,
});
out.push(Stream {
harness: "claude-code",
stream: "error reports",
on: !(quiet || truthy(setting("DISABLE_ERROR_REPORTING", &env))),
destination: "a third-party error tracker".into(),
off_with: "DISABLE_ERROR_REPORTING=1 in ~/.claude/settings.json \"env\"",
foreign: false,
});
let export = truthy(setting("CLAUDE_CODE_ENABLE_TELEMETRY", &env));
let endpoint = setting("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", &env)
.or_else(|| setting("OTEL_EXPORTER_OTLP_ENDPOINT", &env))
.unwrap_or_default();
out.push(Stream {
harness: "claude-code",
stream: "OpenTelemetry export",
on: export,
foreign: export && !endpoint.is_empty() && !ours(&endpoint),
destination: if endpoint.is_empty() {
"none configured".into()
} else {
endpoint
},
off_with: "remove CLAUDE_CODE_ENABLE_TELEMETRY from ~/.claude/settings.json \"env\"",
});
}
fn codex(out: &mut Vec<Stream>) {
let dir = std::env::var_os("CODEX_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".codex"));
if !dir.exists() {
return;
}
let config: toml::Value = std::fs::read_to_string(dir.join("config.toml"))
.ok()
.and_then(|text| text.parse().ok())
.unwrap_or(toml::Value::Table(Default::default()));
let analytics = config
.get("analytics")
.and_then(|t| t.get("enabled"))
.and_then(toml::Value::as_bool)
.unwrap_or(true);
out.push(Stream {
harness: "codex",
stream: "usage and health data",
on: analytics,
destination: "OpenAI".into(),
off_with: "[analytics] enabled = false in ~/.codex/config.toml",
foreign: false,
});
let otel = config.get("otel");
let exporter = otel.and_then(|t| t.get("exporter"));
let endpoint = exporter
.and_then(|e| e.get("otlp-http").or_else(|| e.get("otlp-grpc")))
.and_then(|e| e.get("endpoint"))
.and_then(toml::Value::as_str)
.unwrap_or("")
.to_string();
let on = match exporter {
Some(toml::Value::String(kind)) => kind != "none",
Some(toml::Value::Table(_)) => true,
_ => false,
};
out.push(Stream {
harness: "codex",
stream: "OpenTelemetry export",
on,
foreign: on && !endpoint.is_empty() && !ours(&endpoint),
destination: if endpoint.is_empty() {
"none configured".into()
} else {
endpoint
},
off_with: "[otel] exporter = \"none\" in ~/.codex/config.toml",
});
}
fn gemini(out: &mut Vec<Stream>) {
let settings = home().join(".gemini").join("settings.json");
if !home().join(".gemini").exists() {
return;
}
let value = read_json(&settings).unwrap_or(serde_json::Value::Null);
let usage = value
.pointer("/privacy/usageStatisticsEnabled")
.or_else(|| value.get("usageStatisticsEnabled"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
out.push(Stream {
harness: "gemini",
stream: "usage statistics",
on: usage,
destination: "Google".into(),
off_with: "\"privacy\": { \"usageStatisticsEnabled\": false } in ~/.gemini/settings.json",
foreign: false,
});
let telemetry = value.get("telemetry");
let on = telemetry
.and_then(|t| t.get("enabled"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let endpoint = telemetry
.and_then(|t| t.get("otlpEndpoint"))
.and_then(serde_json::Value::as_str)
.unwrap_or("")
.to_string();
out.push(Stream {
harness: "gemini",
stream: "OpenTelemetry export",
on,
foreign: on && !endpoint.is_empty() && !ours(&endpoint),
destination: if endpoint.is_empty() {
"none configured".into()
} else {
endpoint
},
off_with: "\"telemetry\": { \"enabled\": false } in ~/.gemini/settings.json",
});
}
pub fn streams() -> Vec<Stream> {
let mut out = Vec::new();
claude(&mut out);
codex(&mut out);
gemini(&mut out);
out
}
pub fn warn_once_on_launch(harness: &str, config_home: &Path) {
let mine: Vec<Stream> = streams()
.into_iter()
.filter(|s| s.harness == harness)
.collect();
let on: Vec<&Stream> = mine
.iter()
.filter(|s| s.on && (s.foreign || !s.stream.contains("OpenTelemetry")))
.collect();
let fingerprint = on
.iter()
.map(|s| format!("{}={}", s.stream, s.destination))
.collect::<Vec<_>>()
.join(";");
let file = config_home.join("telemetry-warned").join(harness);
if std::fs::read_to_string(&file).ok().as_deref() == Some(fingerprint.as_str()) {
return;
}
if !on.is_empty() {
for s in &on {
eprintln!(
"note: {harness} sends {} to {}; turn it off with {}",
s.stream, s.destination, s.off_with
);
}
}
let _ = std::fs::create_dir_all(file.parent().unwrap_or(config_home));
let _ = std::fs::write(&file, fingerprint);
}