use std::collections::HashMap;
use std::sync::OnceLock;
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const HOST_KEYS: &[&str] = &[
"DO_NOT_TRACK",
"LEAN_CTX_TELEMETRY",
"LEAN_CTX_TELEMETRY_IN_CI",
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"BUILDKITE",
"CIRCLECI",
"TRAVIS",
"JENKINS_URL",
"TF_BUILD",
"TEAMCITY_VERSION",
"BITBUCKET_BUILD_NUMBER",
"CODEBUILD_BUILD_ID",
"DRONE",
"APPVEYOR",
"SEMAPHORE",
"HEROKU_TEST_RUN_ID",
"CONTINUOUS_INTEGRATION",
"CODESPACES",
"GITPOD_WORKSPACE_ID",
"REPL_ID",
"CLAUDE_CODE_REMOTE",
"KUBERNETES_SERVICE_HOST",
"container",
"CLAUDECODE",
"CODEX_HOME",
"CURSOR_TRACE_ID",
"GEMINI_CLI",
];
#[cfg(all(target_os = "linux", not(test)))]
const MAX_ANCESTORS: usize = 4;
#[must_use]
pub fn var(key: &str) -> Option<String> {
match std::env::var(key) {
Ok(value) => Some(value),
Err(_) => ancestors().get(key).cloned(),
}
}
#[must_use]
pub fn stdin_is_terminal() -> bool {
#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_CHAR, GetFileType};
use windows_sys::Win32::System::Console::GetConsoleMode;
let handle = std::io::stdin().as_raw_handle();
if handle.is_null() || handle as isize == -1 {
return false;
}
let console = unsafe {
let mut mode = 0_u32;
GetFileType(handle) == FILE_TYPE_CHAR && GetConsoleMode(handle, &raw mut mode) != 0
};
console || (inside_posix_terminal() && std::io::IsTerminal::is_terminal(&std::io::stdin()))
}
#[cfg(not(windows))]
{
use std::io::IsTerminal as _;
std::io::stdin().is_terminal()
}
}
#[cfg(windows)]
fn inside_posix_terminal() -> bool {
std::env::var_os("MSYSTEM").is_some() || std::env::var_os("TERM").is_some()
}
fn ancestors() -> &'static HashMap<&'static str, String> {
static ANCESTORS: OnceLock<HashMap<&'static str, String>> = OnceLock::new();
ANCESTORS.get_or_init(read_ancestors)
}
#[cfg(any(test, not(target_os = "linux")))]
fn read_ancestors() -> HashMap<&'static str, String> {
HashMap::new()
}
#[cfg(all(target_os = "linux", not(test)))]
fn read_ancestors() -> HashMap<&'static str, String> {
let mut found = HashMap::new();
let mut pid = std::os::unix::process::parent_id();
for _ in 0..MAX_ANCESTORS {
if pid <= 1 {
break;
}
if let Ok(environ) = std::fs::read(format!("/proc/{pid}/environ")) {
collect_host_keys(&environ, &mut found);
}
match std::fs::read_to_string(format!("/proc/{pid}/stat"))
.ok()
.as_deref()
.and_then(parent_pid)
{
Some(parent) => pid = parent,
None => break,
}
}
found
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn collect_host_keys(environ: &[u8], found: &mut HashMap<&'static str, String>) {
for entry in environ.split(|byte| *byte == 0) {
let Some(separator) = entry.iter().position(|byte| *byte == b'=') else {
continue;
};
let (name, value) = (&entry[..separator], &entry[separator + 1..]);
if let Some(key) = HOST_KEYS.iter().find(|key| key.as_bytes() == name) {
found
.entry(key)
.or_insert_with(|| String::from_utf8_lossy(value).into_owned());
}
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parent_pid(stat: &str) -> Option<u32> {
stat.rsplit_once(')')?
.1
.split_whitespace()
.nth(1)?
.parse()
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_host_keys_are_kept_and_the_nearest_ancestor_wins() {
let mut found = HashMap::new();
collect_host_keys(
b"GITHUB_TOKEN=secret\0CI=true\0DO_NOT_TRACK=1\0container=podman\0NOEQUALS\0",
&mut found,
);
collect_host_keys(b"CI=false\0GITLAB_CI=true\0", &mut found);
assert_eq!(found.get("CI").map(String::as_str), Some("true"));
assert_eq!(found.get("DO_NOT_TRACK").map(String::as_str), Some("1"));
assert_eq!(found.get("container").map(String::as_str), Some("podman"));
assert_eq!(found.get("GITLAB_CI").map(String::as_str), Some("true"));
assert_eq!(found.len(), 4, "{found:?}");
}
#[test]
fn parent_pid_survives_spaces_and_parentheses_in_the_command_name() {
assert_eq!(parent_pid("4242 (codex) S 17 4242 4242 0"), Some(17));
assert_eq!(parent_pid("4242 (my (odd) cmd) R 99 1 1"), Some(99));
assert_eq!(parent_pid("garbage"), None);
}
#[test]
fn every_ci_marker_is_a_host_key() {
for marker in crate::core::telemetry_consent::CI_MARKERS {
assert!(HOST_KEYS.contains(marker), "{marker}");
}
}
#[test]
fn every_client_marker_is_a_host_key() {
for marker in ["CLAUDECODE", "CODEX_HOME", "CURSOR_TRACE_ID", "GEMINI_CLI"] {
assert!(HOST_KEYS.contains(&marker), "{marker}");
}
}
}