aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-22 02:20:47 +0300
committerGitHub <noreply@github.com>2026-09-22 02:20:47 +0300
commit8ac9542757292f7a8d42a2d532bc6a8a33c7ffce (patch)
tree76a89e2ef69ae8725bba0bcec5f635c181de0134
parent28360107c1bfab57fb0de99d74bfa48e74423ca8 (diff)
fix: clear inherited terminal and agent session identity (#4461)
refs #4104
-rw-r--r--docs/next/website/src/content/docs/cli-reference.mdx11
-rw-r--r--src/pane.rs110
2 files changed, 105 insertions, 16 deletions
diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx
index cf9b355f..12de68b7 100644
--- a/docs/next/website/src/content/docs/cli-reference.mdx
+++ b/docs/next/website/src/content/docs/cli-reference.mdx
@@ -517,6 +517,17 @@ terminal size, and values smaller than the popup minimum are clamped. A popup
is not a Herdr pane, does not export `HERDR_PANE_ID`, and does not participate
in pane or agent APIs. Native non-terminal plugin panes are outside plugin v1.
+New panes identify as `TERM_PROGRAM=herdr`, with `TERM_PROGRAM_VERSION` set to
+Herdr's version. They keep `TERM=xterm-256color` and `COLORTERM=truecolor`.
+Herdr removes inherited outer-terminal session markers (iTerm2, WezTerm, Kitty,
+Windows Terminal, tmux, screen, and Zellij), iTerm2's `LC_TERMINAL` identity,
+and known Claude Code, Codex, and OMP session markers before applying explicit
+launch environment values. This prevents new panes from claiming the terminal
+or agent session that originally started the server. Ordinary environment
+variables, including API keys and display settings, remain inherited rather than
+being replaced with the attaching client's environment. SSH-agent forwarding has
+its own reconnect handling. Existing pane processes are unchanged by this cleanup.
+
`--env KEY=VALUE` can be repeated on process-launching commands. It applies to the newly launched process only. Herdr-managed variables such as `HERDR_SOCKET_PATH`, `HERDR_BIN_PATH`, `HERDR_ENV`, `HERDR_WORKSPACE_ID`, `HERDR_TAB_ID`, `HERDR_PANE_ID`, `HERDR_PLUGIN_ID`, `HERDR_PLUGIN_ROOT`, `HERDR_PLUGIN_CONFIG_DIR`, `HERDR_PLUGIN_STATE_DIR`, `HERDR_PLUGIN_ENTRYPOINT_ID`, and `HERDR_PLUGIN_CONTEXT_JSON` stay authoritative when they conflict with caller-provided env.
## Read sources
diff --git a/src/pane.rs b/src/pane.rs
index 485b226c..9d75c21c 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -96,7 +96,25 @@ fn apply_pane_terminal_env(cmd: &mut CommandBuilder) {
// when the remote side lacks matching terminfo entries.
cmd.env("TERM", PANE_TERM);
cmd.env("COLORTERM", PANE_COLORTERM);
- cmd.env_remove("WT_SESSION");
+ cmd.env("TERM_PROGRAM", "herdr");
+ cmd.env("TERM_PROGRAM_VERSION", crate::build_info::version());
+ // Host handles refer to the outer terminal, never to this pane.
+ for key in [
+ "ITERM_SESSION_ID",
+ "LC_TERMINAL",
+ "LC_TERMINAL_VERSION",
+ "WEZTERM_PANE",
+ "KITTY_WINDOW_ID",
+ "WT_SESSION",
+ "TMUX",
+ "TMUX_PANE",
+ "STY",
+ "ZELLIJ",
+ "ZELLIJ_SESSION_NAME",
+ "ZELLIJ_PANE_ID",
+ ] {
+ cmd.env_remove(key);
+ }
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
@@ -148,10 +166,18 @@ impl PaneLaunchEnv {
fn apply_pane_launch_env(cmd: &mut CommandBuilder, launch_env: &PaneLaunchEnv) {
#[cfg(unix)]
crate::platform::ssh_agent::apply_pane_env(cmd);
- cmd.env_remove("CODEX_THREAD_ID");
- // OMP sets OMPCODE for shells it spawns. A pane launched from inside OMP
- // must not inherit it or its root agent would look like a nested session.
- cmd.env_remove("OMPCODE");
+ // A new pane is not a child agent of the process that started the server.
+ // Explicit launch env below can opt back into an intentional child session.
+ for key in [
+ "CODEX_THREAD_ID",
+ "OMPCODE",
+ "CLAUDECODE",
+ "CLAUDE_CODE_CHILD_SESSION",
+ "CLAUDE_CODE_SESSION_ID",
+ "CLAUDE_CODE_MESSAGING_TOKEN",
+ ] {
+ cmd.env_remove(key);
+ }
for (key, value) in &launch_env.extra {
cmd.env(key, value);
}
@@ -3715,6 +3741,7 @@ impl PaneRuntime {
#[cfg(test)]
mod tests {
use super::*;
+ use std::ffi::OsStr;
#[tokio::test]
async fn clear_pane_preserves_wrapped_input_and_unfinished_vt_sequence() {
@@ -3824,33 +3851,84 @@ mod tests {
}
#[test]
- fn pane_launch_env_removes_outer_codex_thread_id() {
+ fn pane_launch_env_removes_outer_agent_identity() {
+ let keys = [
+ "CODEX_THREAD_ID",
+ "OMPCODE",
+ "CLAUDECODE",
+ "CLAUDE_CODE_CHILD_SESSION",
+ "CLAUDE_CODE_SESSION_ID",
+ "CLAUDE_CODE_MESSAGING_TOKEN",
+ ];
let mut cmd = CommandBuilder::new("shell");
- cmd.env("CODEX_THREAD_ID", "outer-session");
+ for key in keys {
+ cmd.env(key, "outer-session");
+ }
+ cmd.env("ANTHROPIC_API_KEY", "fake-api-key");
+ cmd.env("DISPLAY", ":42");
apply_pane_launch_env(&mut cmd, &PaneLaunchEnv::default());
- assert!(cmd.get_env("CODEX_THREAD_ID").is_none());
+ for key in keys {
+ assert!(cmd.get_env(key).is_none(), "{key} must not leak into panes");
+ }
+ assert_eq!(
+ cmd.get_env("ANTHROPIC_API_KEY"),
+ Some(OsStr::new("fake-api-key"))
+ );
+ assert_eq!(cmd.get_env("DISPLAY"), Some(OsStr::new(":42")));
}
#[test]
- fn pane_launch_env_removes_outer_ompcode_marker() {
+ fn pane_terminal_identity_removes_outer_terminal_identity() {
+ let keys = [
+ "ITERM_SESSION_ID",
+ "LC_TERMINAL",
+ "LC_TERMINAL_VERSION",
+ "WEZTERM_PANE",
+ "KITTY_WINDOW_ID",
+ "WT_SESSION",
+ "TMUX",
+ "TMUX_PANE",
+ "STY",
+ "ZELLIJ",
+ "ZELLIJ_SESSION_NAME",
+ "ZELLIJ_PANE_ID",
+ ];
let mut cmd = CommandBuilder::new("shell");
- cmd.env("OMPCODE", "1");
+ for key in keys {
+ cmd.env(key, "outer-session");
+ }
+ cmd.env("TERM_PROGRAM", "iTerm.app");
+ cmd.env("TERM_PROGRAM_VERSION", "outer-version");
- apply_pane_launch_env(&mut cmd, &PaneLaunchEnv::default());
+ apply_pane_terminal_env(&mut cmd);
- assert!(cmd.get_env("OMPCODE").is_none());
+ for key in keys {
+ assert!(cmd.get_env(key).is_none(), "{key} must not leak into panes");
+ }
+ assert_eq!(cmd.get_env("TERM_PROGRAM"), Some(OsStr::new("herdr")));
+ assert_eq!(
+ cmd.get_env("TERM_PROGRAM_VERSION"),
+ Some(OsStr::new(&crate::build_info::version()))
+ );
}
#[test]
- fn pane_terminal_identity_removes_outer_windows_terminal_session() {
+ fn pane_launch_env_allows_explicit_session_identity() {
+ let extra = vec![
+ ("CLAUDE_CODE_CHILD_SESSION".into(), "1".into()),
+ ("CLAUDE_CODE_SESSION_ID".into(), "intentional-child".into()),
+ ("CLAUDE_CODE_MESSAGING_TOKEN".into(), "fake-token".into()),
+ ("ITERM_SESSION_ID".into(), "intentional-host".into()),
+ ];
let mut cmd = CommandBuilder::new("shell");
- cmd.env("WT_SESSION", "outer-session");
-
apply_pane_terminal_env(&mut cmd);
+ apply_pane_launch_env(&mut cmd, &PaneLaunchEnv::from_extra(extra.clone()));
- assert!(cmd.get_env("WT_SESSION").is_none());
+ for (key, value) in extra {
+ assert_eq!(cmd.get_env(key), Some(OsStr::new(&value)));
+ }
}
#[tokio::test]