aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 22:15:01 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 22:17:37 +0300
commit659badf106786f5611e750de0e4936e47c69a3d4 (patch)
tree3cad8ed17a4e4d91d5e178e953a2349e6afa4b42 /src
parentf7fa9486855c824bcd7e783e2d3eb4d12ad85205 (diff)
fix: avoid agent detection from runtime arguments
refs #238
Diffstat (limited to 'src')
-rw-r--r--src/detect.rs254
-rw-r--r--src/pane/osc.rs12
-rw-r--r--src/platform/linux.rs8
-rw-r--r--src/platform/macos.rs9
-rw-r--r--src/platform/mod.rs1
5 files changed, 210 insertions, 74 deletions
diff --git a/src/detect.rs b/src/detect.rs
index 5767a0b6..dd7d21c8 100644
--- a/src/detect.rs
+++ b/src/detect.rs
@@ -824,7 +824,7 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri
if is_generic_runtime_or_shell(&lower_effective) {
if let Some(wrapped_agent) =
- wrapped_agent_name_from_cmdline(process.cmdline.as_deref().unwrap_or_default())
+ wrapped_agent_name_from_runtime_argv(&lower_effective, process.argv.as_deref())
{
return wrapped_agent;
}
@@ -834,8 +834,8 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri
return effective.to_string();
}
- if let Some(wrapped_agent) =
- cmdline_argv0_agent_name(process.cmdline.as_deref().unwrap_or_default())
+ if let Some(wrapped_agent) = argv0_agent_name(process.argv.as_deref())
+ .or_else(|| cmdline_argv0_agent_name(process.cmdline.as_deref().unwrap_or_default()))
{
return wrapped_agent;
}
@@ -843,16 +843,89 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri
effective.to_string()
}
-fn wrapped_agent_name_from_cmdline(cmdline: &str) -> Option<String> {
- for token in cmdline.split_whitespace() {
- if let Some(agent_name) = agent_name_from_path_token(token) {
- return Some(agent_name);
+fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) -> Option<String> {
+ let argv = argv?;
+ let runtime = path_basename(runtime).to_lowercase();
+
+ match runtime.as_str() {
+ "node" | "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]),
+ "python" | "python3" => script_arg_agent_name(argv, &["-c"], &["-m"]),
+ "sh" | "bash" | "zsh" | "fish" => script_arg_agent_name(argv, &["-c"], &[]),
+ "tmux" => None,
+ _ => None,
+ }
+}
+
+fn script_arg_agent_name(
+ argv: &[String],
+ eval_flags: &[&str],
+ module_flags: &[&str],
+) -> Option<String> {
+ let mut args = argv.iter().skip(1);
+ while let Some(arg) = args.next() {
+ if arg == "--" {
+ return args
+ .next()
+ .and_then(|token| agent_name_from_path_token(token));
}
+
+ if flag_matches(arg, eval_flags) || flag_matches(arg, module_flags) {
+ return None;
+ }
+
+ if arg.starts_with('-') {
+ if option_takes_value(arg) {
+ let _ = args.next();
+ }
+ continue;
+ }
+
+ return agent_name_from_path_token(arg);
}
None
}
+fn flag_matches(arg: &str, flags: &[&str]) -> bool {
+ flags
+ .iter()
+ .any(|flag| arg == *flag || short_flag_payload(arg, flag) || long_flag_value(arg, flag))
+}
+
+fn short_flag_payload(arg: &str, flag: &str) -> bool {
+ flag.starts_with('-')
+ && !flag.starts_with("--")
+ && arg.starts_with(flag)
+ && arg.len() > flag.len()
+}
+
+fn long_flag_value(arg: &str, flag: &str) -> bool {
+ flag.starts_with("--")
+ && arg
+ .strip_prefix(flag)
+ .is_some_and(|rest| rest.starts_with('='))
+}
+
+fn option_takes_value(arg: &str) -> bool {
+ matches!(
+ arg,
+ "-r" | "--require"
+ | "--loader"
+ | "--import"
+ | "--experimental-loader"
+ | "--inspect-port"
+ | "-W"
+ | "-X"
+ | "-S"
+ | "-L"
+ | "-o"
+ )
+}
+
+fn argv0_agent_name(argv: Option<&[String]>) -> Option<String> {
+ agent_name_from_path_token(argv?.first()?)
+}
+
fn cmdline_argv0_agent_name(cmdline: &str) -> Option<String> {
agent_name_from_path_token(cmdline.split_whitespace().next()?)
}
@@ -863,14 +936,18 @@ fn agent_name_from_path_token(token: &str) -> Option<String> {
return None;
}
- let basename = std::path::Path::new(trimmed)
- .file_name()
- .and_then(|name| name.to_str())
- .unwrap_or(trimmed);
+ let basename = path_basename(trimmed);
let agent = parse_agent_label(basename)?;
Some(agent_label(agent).to_string())
}
+fn path_basename(path: &str) -> &str {
+ std::path::Path::new(path)
+ .file_name()
+ .and_then(|name| name.to_str())
+ .unwrap_or(path)
+}
+
fn process_priority(process: &crate::platform::ForegroundProcess, normalized_name: &str) -> u8 {
let lower_name = normalized_name.to_lowercase();
if lower_name != process.name.to_lowercase() {
@@ -897,6 +974,20 @@ fn is_generic_runtime_or_shell(name: &str) -> bool {
mod tests {
use super::*;
+ fn foreground_process(
+ pid: u32,
+ name: &str,
+ argv: &[&str],
+ ) -> crate::platform::ForegroundProcess {
+ crate::platform::ForegroundProcess {
+ pid,
+ name: name.to_string(),
+ argv0: None,
+ argv: Some(argv.iter().map(|arg| (*arg).to_string()).collect()),
+ cmdline: Some(argv.join(" ")),
+ }
+ }
+
// ---- Agent identification ----
#[test]
@@ -972,18 +1063,8 @@ mod tests {
let job = crate::platform::ForegroundJob {
process_group_id: 123,
processes: vec![
- crate::platform::ForegroundProcess {
- pid: 1,
- name: "node".to_string(),
- argv0: None,
- cmdline: Some("node /path/to/bin/codex".to_string()),
- },
- crate::platform::ForegroundProcess {
- pid: 2,
- name: "bash".to_string(),
- argv0: None,
- cmdline: Some("bash".to_string()),
- },
+ foreground_process(1, "node", &["node", "/path/to/bin/codex"]),
+ foreground_process(2, "bash", &["bash"]),
],
};
@@ -998,18 +1079,8 @@ mod tests {
let job = crate::platform::ForegroundJob {
process_group_id: 42,
processes: vec![
- crate::platform::ForegroundProcess {
- pid: 42,
- name: "claude".to_string(),
- argv0: None,
- cmdline: Some("claude".to_string()),
- },
- crate::platform::ForegroundProcess {
- pid: 43,
- name: "node".to_string(),
- argv0: None,
- cmdline: Some("node /tmp/mcp/bin/codex".to_string()),
- },
+ foreground_process(42, "claude", &["claude"]),
+ foreground_process(43, "node", &["node", "/tmp/mcp/bin/codex"]),
],
};
@@ -1024,18 +1095,8 @@ mod tests {
let job = crate::platform::ForegroundJob {
process_group_id: 42,
processes: vec![
- crate::platform::ForegroundProcess {
- pid: 42,
- name: "bash".to_string(),
- argv0: None,
- cmdline: Some("bash".to_string()),
- },
- crate::platform::ForegroundProcess {
- pid: 43,
- name: "node".to_string(),
- argv0: None,
- cmdline: Some("node /tmp/mcp/bin/codex".to_string()),
- },
+ foreground_process(42, "bash", &["bash"]),
+ foreground_process(43, "node", &["node", "/tmp/mcp/bin/codex"]),
],
};
@@ -1049,12 +1110,11 @@ mod tests {
fn identify_agent_in_job_detects_nix_wrapped_codex_from_cmdline_argv0() {
let job = crate::platform::ForegroundJob {
process_group_id: 123,
- processes: vec![crate::platform::ForegroundProcess {
- pid: 1,
- name: ".codex-wrapped".to_string(),
- argv0: None,
- cmdline: Some("/etc/profiles/per-user/user/bin/codex --model gpt-5".to_string()),
- }],
+ processes: vec![foreground_process(
+ 1,
+ ".codex-wrapped",
+ &["/etc/profiles/per-user/user/bin/codex", "--model", "gpt-5"],
+ )],
};
assert_eq!(
@@ -1067,12 +1127,11 @@ mod tests {
fn identify_agent_in_job_canonicalizes_nix_wrapped_aliases_from_cmdline_argv0() {
let job = crate::platform::ForegroundJob {
process_group_id: 123,
- processes: vec![crate::platform::ForegroundProcess {
- pid: 1,
- name: ".claude-code-wrapped".to_string(),
- argv0: None,
- cmdline: Some("/nix/store/example/bin/claude-code".to_string()),
- }],
+ processes: vec![foreground_process(
+ 1,
+ ".claude-code-wrapped",
+ &["/nix/store/example/bin/claude-code"],
+ )],
};
assert_eq!(
@@ -1085,12 +1144,11 @@ mod tests {
fn identify_agent_in_job_detects_shell_wrapped_pi() {
let job = crate::platform::ForegroundJob {
process_group_id: 123,
- processes: vec![crate::platform::ForegroundProcess {
- pid: 1,
- name: "sh".to_string(),
- argv0: None,
- cmdline: Some("/bin/sh /tmp/test-bin/pi".to_string()),
- }],
+ processes: vec![foreground_process(
+ 1,
+ "sh",
+ &["/bin/sh", "/tmp/test-bin/pi"],
+ )],
};
assert_eq!(
@@ -1100,8 +1158,70 @@ mod tests {
}
#[test]
- fn wrapped_agent_name_from_cmdline_ignores_plain_shell_flags() {
- assert_eq!(wrapped_agent_name_from_cmdline("bash -lc"), None);
+ fn wrapped_agent_name_from_runtime_argv_ignores_plain_shell_flags() {
+ assert_eq!(
+ wrapped_agent_name_from_runtime_argv("bash", Some(&["bash".into(), "-lc".into()])),
+ None
+ );
+ }
+
+ #[test]
+ fn identify_agent_in_job_ignores_python_c_argument_named_codex() {
+ let job = crate::platform::ForegroundJob {
+ process_group_id: 123,
+ processes: vec![foreground_process(
+ 1,
+ "python3",
+ &["python3", "-c", "import time; time.sleep(60)", "/tmp/codex"],
+ )],
+ };
+
+ assert_eq!(identify_agent_in_job(&job), None);
+ }
+
+ #[test]
+ fn identify_agent_in_job_ignores_node_eval_argument_named_codex() {
+ let job = crate::platform::ForegroundJob {
+ process_group_id: 123,
+ processes: vec![foreground_process(
+ 1,
+ "node",
+ &["node", "-e", "setTimeout(() => {}, 60000)", "/tmp/codex"],
+ )],
+ };
+
+ assert_eq!(identify_agent_in_job(&job), None);
+ }
+
+ #[test]
+ fn identify_agent_in_job_ignores_shell_c_argument_named_codex() {
+ let job = crate::platform::ForegroundJob {
+ process_group_id: 123,
+ processes: vec![foreground_process(
+ 1,
+ "bash",
+ &["bash", "-c", "sleep 60", "/tmp/codex"],
+ )],
+ };
+
+ assert_eq!(identify_agent_in_job(&job), None);
+ }
+
+ #[test]
+ fn identify_agent_in_job_detects_python_script_named_codex() {
+ let job = crate::platform::ForegroundJob {
+ process_group_id: 123,
+ processes: vec![foreground_process(
+ 1,
+ "python3",
+ &["python3", "/tmp/codex", "--model", "gpt-5"],
+ )],
+ };
+
+ assert_eq!(
+ identify_agent_in_job(&job),
+ Some((Agent::Codex, "codex".to_string()))
+ );
}
#[test]
diff --git a/src/pane/osc.rs b/src/pane/osc.rs
index b5278125..0f04b045 100644
--- a/src/pane/osc.rs
+++ b/src/pane/osc.rs
@@ -376,6 +376,7 @@ mod tests {
pid: shell_pid,
name: "zsh".to_string(),
argv0: Some("zsh".to_string()),
+ argv: Some(vec!["zsh".to_string()]),
cmdline: Some("zsh".to_string()),
}],
}
@@ -550,6 +551,10 @@ mod tests {
pid: 42,
name: "droid".to_string(),
argv0: None,
+ argv: Some(vec![
+ "/opt/factory/droid".to_string(),
+ "--resume".to_string(),
+ ]),
cmdline: Some("/opt/factory/droid --resume".to_string()),
}],
};
@@ -561,6 +566,11 @@ mod tests {
pid: 42,
name: "bun".to_string(),
argv0: Some("bun".to_string()),
+ argv: Some(vec![
+ "bun".to_string(),
+ "/home/can/.local/bin/droid".to_string(),
+ "--resume".to_string(),
+ ]),
cmdline: Some("/home/can/.local/bin/droid --resume".to_string()),
}],
};
@@ -584,6 +594,7 @@ mod tests {
pid: 42,
name: "droid".to_string(),
argv0: Some("droid".to_string()),
+ argv: Some(vec!["droid".to_string()]),
cmdline: Some("droid".to_string()),
}],
};
@@ -626,6 +637,7 @@ mod tests {
pid: 42,
name: "droid".to_string(),
argv0: Some("droid".to_string()),
+ argv: Some(vec!["droid".to_string()]),
cmdline: Some("droid".to_string()),
}],
}),
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index 269e3ad8..73a4f0a5 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -34,11 +34,13 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
continue;
}
+ let argv = process_argv(pid);
processes.push(ForegroundProcess {
pid,
name,
argv0: None,
- cmdline: process_cmdline(pid),
+ cmdline: argv.as_ref().map(|parts| parts.join(" ")),
+ argv,
});
}
@@ -73,7 +75,7 @@ fn process_pgrp_and_comm(pid: u32) -> Option<(i32, String)> {
Some((pgrp, comm))
}
-fn process_cmdline(pid: u32) -> Option<String> {
+fn process_argv(pid: u32) -> Option<Vec<String>> {
let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
if bytes.is_empty() {
return None;
@@ -83,7 +85,7 @@ fn process_cmdline(pid: u32) -> Option<String> {
.filter(|part| !part.is_empty())
.map(|part| String::from_utf8_lossy(part).into_owned())
.collect();
- (!parts.is_empty()).then(|| parts.join(" "))
+ (!parts.is_empty()).then_some(parts)
}
/// Get the current working directory of a process.
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index 444f458d..add51aa4 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -32,11 +32,13 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
let Some(name) = comm_from_bsdinfo(&info) else {
continue;
};
+ let argv = process_argv(pid);
processes.push(ForegroundProcess {
pid,
name,
argv0: process_argv0_name(pid),
- cmdline: process_cmdline(pid),
+ cmdline: argv.as_ref().map(|parts| parts.join(" ")),
+ argv,
});
}
@@ -462,10 +464,9 @@ fn comm_from_bsdinfo(info: &libc::proc_bsdinfo) -> Option<String> {
String::from_utf8(bytes).ok()
}
-fn process_cmdline(pid: u32) -> Option<String> {
+fn process_argv(pid: u32) -> Option<Vec<String>> {
let buf = kern_procargs2(pid)?;
- let argv = procargs2_argv(&buf)?;
- Some(argv.join(" "))
+ procargs2_argv(&buf)
}
fn procargs2_argv(buf: &[u8]) -> Option<Vec<String>> {
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 8cf2b08d..a836ed81 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -8,6 +8,7 @@ pub struct ForegroundProcess {
pub pid: u32,
pub name: String,
pub argv0: Option<String>,
+ pub argv: Option<Vec<String>>,
pub cmdline: Option<String>,
}