use std::io::IsTerminal;
const MAX_LABEL: usize = 24;
pub(crate) fn detect() -> String {
detect_from(|k| std::env::var(k).ok(), std::io::stdout().is_terminal())
}
pub(crate) fn detect_from(env: impl Fn(&str) -> Option<String>, tty: bool) -> String {
let get = |k: &str| env(k).filter(|v| !v.is_empty() && v != "0");
if get("CLAUDECODE").is_some() {
return match get("CLAUDE_CODE_ENTRYPOINT").as_deref() {
None | Some("cli") => "claude-code".to_string(),
Some(entry) => format!("claude-code:{}", label(entry)),
};
}
if let Some(v) = get("AI_AGENT") {
return label(v.split('_').next().unwrap_or_default());
}
if get("CURSOR_TRACE_ID").is_some() || get("CURSOR_AGENT").is_some() {
return "cursor".to_string();
}
if get("CI").is_some() || get("GITHUB_ACTIONS").is_some() {
return "ci".to_string();
}
if tty {
"human".to_string()
} else {
"piped".to_string()
}
}
fn label(raw: &str) -> String {
let cleaned: String = raw
.trim()
.to_ascii_lowercase()
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '.')
.take(MAX_LABEL)
.collect();
if cleaned.is_empty() {
"unknown".to_string()
} else {
cleaned
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |k| {
pairs
.iter()
.find(|(name, _)| *name == k)
.map(|(_, v)| (*v).to_string())
}
}
#[test]
fn identifies_claude_code_and_its_entrypoint() {
assert_eq!(
detect_from(env(&[("CLAUDECODE", "1")]), false),
"claude-code"
);
assert_eq!(
detect_from(
env(&[("CLAUDECODE", "1"), ("CLAUDE_CODE_ENTRYPOINT", "cli")]),
false
),
"claude-code"
);
assert_eq!(
detect_from(
env(&[("CLAUDECODE", "1"), ("CLAUDE_CODE_ENTRYPOINT", "mcp")]),
false
),
"claude-code:mcp"
);
}
#[test]
fn keeps_the_tool_from_ai_agent_but_not_its_version() {
assert_eq!(
detect_from(env(&[("AI_AGENT", "claude-code_2-1-236_agent")]), false),
"claude-code"
);
}
#[test]
fn falls_back_to_the_terminal_check() {
assert_eq!(detect_from(env(&[]), true), "human");
assert_eq!(detect_from(env(&[]), false), "piped");
assert_eq!(
detect_from(env(&[("CLAUDECODE", "1")]), true),
"claude-code"
);
}
#[test]
fn ignores_unset_and_falsey_values() {
assert_eq!(detect_from(env(&[("CLAUDECODE", "")]), true), "human");
assert_eq!(detect_from(env(&[("CLAUDECODE", "0")]), true), "human");
assert_eq!(detect_from(env(&[("CI", "0")]), true), "human");
}
#[test]
fn sanitizes_a_hostile_label() {
let long = "x".repeat(100);
assert_eq!(label(&long).len(), MAX_LABEL);
assert_eq!(label("Foo Bar/../;drop"), "foobar..drop");
assert_eq!(label(" "), "unknown");
}
}