diff options
| -rw-r--r-- | docs/next/website/src/content/docs/cli-reference.mdx | 11 | ||||
| -rw-r--r-- | src/pane.rs | 110 |
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] |
