use std::ffi::OsString;
use std::path::PathBuf;
use crate::LaunchEnv;
pub const CALLER_ENV_DENYLIST: &[&str] = &[
"CLAUDECODE",
"TMUX",
"TMUX_PANE",
"RTM_SOCKET_PATH",
"RTM_DB_PATH",
"HELIOY_SESSION_ID",
"HELIOY_RUNTIME",
"RTM_SESSION_ID",
"RTM_RUNTIME_KIND",
];
pub const CALLER_ENV_DENYLIST_PREFIXES: &[&str] = &["CLAUDE_CODE_", "CLAUDE_PLUGIN_"];
pub fn capture_caller_env() -> Vec<LaunchEnv> {
capture_env_from_os(std::env::vars_os())
}
pub fn capture_env_from_os<I>(iter: I) -> Vec<LaunchEnv>
where
I: IntoIterator<Item = (OsString, OsString)>,
{
capture_env_from(iter.into_iter().map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.to_string_lossy().into_owned(),
)
}))
}
pub fn capture_env_from<I, K, V>(iter: I) -> Vec<LaunchEnv>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
iter.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.filter(|(k, _)| !is_denied(k))
.map(|(k, v)| LaunchEnv::new(k, v))
.collect()
}
fn is_denied(key: &str) -> bool {
if CALLER_ENV_DENYLIST.contains(&key) {
return true;
}
CALLER_ENV_DENYLIST_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
}
pub fn capture_caller_cwd() -> std::io::Result<PathBuf> {
std::env::current_dir()
}
pub fn launcher_probe_cwd() -> PathBuf {
PathBuf::from("/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn denylist_drops_parent_markers() {
let env = capture_env_from([
("PATH", "/usr/bin"),
("CLAUDECODE", "1"),
("CLAUDE_CODE_SESSION_ID", "abc"),
("CLAUDE_PLUGIN_DATA", "/tmp"),
("TMUX", "/private/tmp/tmux"),
("TMUX_PANE", "%4"),
("RTM_SOCKET_PATH", "/tmp/rtm.sock"),
("RTM_DB_PATH", "/tmp/rtm.db"),
("HELIOY_SESSION_ID", "session"),
("HELIOY_RUNTIME", "claude"),
("RTM_SESSION_ID", "session"),
("RTM_RUNTIME_KIND", "claude"),
("HELIOY_PAT", "ghp_secret"),
("ANTHROPIC_API_KEY", "sk-secret"),
]);
let keys: Vec<&str> = env.iter().map(|e| e.key.as_str()).collect();
assert_eq!(keys, vec!["PATH", "HELIOY_PAT", "ANTHROPIC_API_KEY"]);
}
#[test]
fn denylist_keeps_user_state() {
let env = capture_env_from([
("PATH", "/usr/bin"),
("HOME", "/Users/alphab"),
("LANG", "en_US.UTF-8"),
("MISE_SHELL", "zsh"),
]);
assert_eq!(env.len(), 4);
}
#[test]
fn capture_env_from_os_tolerates_non_utf8() {
use std::os::unix::ffi::OsStringExt;
let raw_value = OsString::from_vec(vec![b'A', 0xFF, b'B']);
let env = capture_env_from_os([(OsString::from("RTM_TEST_BAD_BYTES"), raw_value)]);
assert_eq!(env.len(), 1);
assert_eq!(env[0].key, "RTM_TEST_BAD_BYTES");
assert!(env[0].value.contains('\u{FFFD}'), "{:?}", env[0].value);
}
#[test]
fn capture_env_from_os_applies_denylist() {
let env = capture_env_from_os([
(OsString::from("PATH"), OsString::from("/usr/bin")),
(OsString::from("CLAUDECODE"), OsString::from("1")),
(
OsString::from("CLAUDE_CODE_SESSION_ID"),
OsString::from("abc"),
),
]);
let keys: Vec<&str> = env.iter().map(|e| e.key.as_str()).collect();
assert_eq!(keys, vec!["PATH"]);
}
}