1pub const PANE_ENV_KEEP: &[&str] = &[
49 "AGENTMUX_LOCAL_URL",
57 "AGENTMUX_AUTH_KEY",
58 "AGENTMUX_BLOCKID",
60 "AGENTMUX_TABID",
61 "AGENTMUX_VERSION",
64 "AGENTMUX_BUILD",
65 "AGENTMUX_INSTANCE_CHANNEL",
66 "AGENTMUX_LOG_DIR",
67 "AGENTMUX_AGENT_ID",
78 "AGENTMUX_AGENT_SLUG",
79 "AGENTMUX_AGENT_DISPLAY",
80 "AGENTMUX_INSTANCE_SLUG",
81];
82
83pub const NESTING_SENTINEL_KEY: &str = "AGENTMUX";
87
88pub fn keys_to_strip() -> Vec<String> {
94 std::env::vars_os()
95 .filter_map(|(k, _)| k.into_string().ok())
96 .filter(|k| should_strip(k))
97 .collect()
98}
99
100pub fn should_strip(key: &str) -> bool {
102 if !key.starts_with("AGENTMUX") {
103 return false;
104 }
105 if key == NESTING_SENTINEL_KEY {
107 return false;
108 }
109 !PANE_ENV_KEEP.contains(&key)
110}
111
112
113pub fn sanitize_pty_command(c: &mut portable_pty::CommandBuilder) {
125 for key in keys_to_strip() {
126 c.env_remove(&key);
127 }
128 c.env(NESTING_SENTINEL_KEY, "1");
129 for (key, value) in build_info() {
130 c.env(key, value);
131 }
132}
133
134pub fn build_info() -> &'static [(&'static str, String)] {
145 static INFO: std::sync::OnceLock<Vec<(&'static str, String)>> = std::sync::OnceLock::new();
146 INFO.get_or_init(|| {
147 let version = env!("CARGO_PKG_VERSION").to_string();
148 let build = std::env::current_exe()
149 .ok()
150 .and_then(|exe| exe.parent().and_then(agentmux_common::runtime_mode::portable_build_label))
151 .unwrap_or_else(|| version.clone());
152 let mut info = vec![("AGENTMUX_VERSION", version), ("AGENTMUX_BUILD", build)];
153 if let Some(channel) = std::env::var("AGENTMUX_CHANNEL").ok().filter(|c| !c.trim().is_empty()) {
154 info.push(("AGENTMUX_INSTANCE_CHANNEL", channel));
155 }
156 info
157 })
158}
159
160pub fn sanitize_process_command(cmd: &mut tokio::process::Command) {
167 for key in keys_to_strip() {
168 cmd.env_remove(&key);
169 }
170 cmd.env(NESTING_SENTINEL_KEY, "1");
171 for (key, value) in build_info() {
172 cmd.env(key, value);
173 }
174}
175
176
177pub fn sanitize_external_command(cmd: &mut tokio::process::Command) {
189 for key in all_agentmux_keys() {
190 cmd.env_remove(&key);
191 }
192 cmd.env(NESTING_SENTINEL_KEY, "1");
193}
194
195pub fn sanitize_external_pty_command(c: &mut portable_pty::CommandBuilder) {
197 for key in all_agentmux_keys() {
198 c.env_remove(&key);
199 }
200 c.env(NESTING_SENTINEL_KEY, "1");
201}
202
203fn all_agentmux_keys() -> Vec<String> {
205 std::env::vars_os()
206 .filter_map(|(k, _)| k.into_string().ok())
207 .filter(|k| k.starts_with("AGENTMUX") && k != NESTING_SENTINEL_KEY)
208 .collect()
209}
210
211
212pub fn sanitize_external_std_command(cmd: &mut std::process::Command) {
218 for key in all_agentmux_keys() {
219 cmd.env_remove(&key);
220 }
221 cmd.env(NESTING_SENTINEL_KEY, "1");
222}
223
224#[cfg(test)]
225mod spawn_site_coverage {
226 use std::collections::BTreeMap;
256 use std::path::{Path, PathBuf};
257
258 const SPAWN_MARKERS: &[&str] = &["CommandBuilder::new", "process::Command::new", "Command::new("];
260
261 const CLASSES: &[&str] = &["sanitized:", "probe:", "test:", "own-exe:", "not a spawn:", "mixed:"];
264
265 const SPAWN_INVENTORY: &[(&str, &str, usize, &str)] = &[
270 ("src/agents/runner.rs", "bin", 1,
271 "sanitized: agent CLI subprocess, sanitize_process_command"),
272 ("src/backend/attachments/extract.rs", "exe", 1,
273 "sanitized: our own binary re-run as `__extract` to parse one attachment in isolation; it needs no instance identity, so sanitize_external_std_command"),
274 ("src/backend/attachments/extract.rs", "harness", 1,
275 "test: re-runs the test harness as a child that sleeps, to prove the extraction deadline kills it"),
276 ("src/backend/storage/muxbus.rs", "std::env::current_exe(", 1,
277 "test: re-runs the test harness as children that save and read one keychain sign-in at once (ignored; real keychain)"),
278 ("src/backend/blockcontroller/app_server.rs", "fake_server_binary(", 1,
279 "test: runs the fixture server built above; lives and dies with the test"),
280 ("src/backend/remote/ssh.rs", "ssh", 1,
281 "probe: `ssh -V` for the client's version (forced askpass), output parsed then discarded"),
282 ("src/backend/remote/host.rs", "&self.ssh_path", 1,
283 "sanitized: a durable SSH pane's ssh (attach, end, helper install: one ssh_command), the same sanitize_process_command"),
284 ("src/backend/blockcontroller/persistent/tests/eager_resume.rs", "\"taskkill\"", 1,
285 "test: the eager-resume harness's Drop force-kills its stub CLI on Windows so a failed assertion cannot leave it running"),
286 ("src/backend/blockcontroller/persistent/tests/eager_resume.rs", "\"kill\"", 1,
287 "test: the same Drop teardown on Unix"),
288 ("src/backend/blockcontroller/persistent/tests/send_input.rs", "\"echo\"", 1,
289 "not a spawn: appears inside a comment explaining why `echo` cannot be spawned directly"),
290 ("src/backend/blockcontroller/shell/lifecycle.rs", "\"/bin/sh\"", 1,
291 "sanitized: Unix fallback shell, covered by the same single sanitize_pty_command"),
292 ("src/backend/blockcontroller/shell/lifecycle.rs", "\"cmd.exe\"", 1,
293 "sanitized: Windows fallback shell, covered by the same single sanitize_pty_command"),
294 ("src/backend/blockcontroller/shell/lifecycle.rs", "\"shell-under-test\"", 1,
295 "not a spawn: global_cmd_env_tests builds a CommandBuilder only to read back the cmd:env applied to it; never spawned"),
296 ("src/backend/blockcontroller/shell/lifecycle.rs", "&program", 1,
297 "sanitized: pane PTY, one choke-point sanitize_pty_command covers all four builders here"),
298 ("src/backend/blockcontroller/shell/lifecycle.rs", "&shell_path", 1,
299 "sanitized: the configured login shell, covered by the same sanitize_pty_command"),
300 ("src/backend/blockcontroller/shell/lifecycle.rs", "\"wsl.exe\"", 1,
301 "sanitized: a WSL pane's wsl.exe, covered by the same sanitize_pty_command"),
302 ("src/backend/shells.rs", "&c.path", 1,
303 "probe: each installed shell's `--version` for Settings' default-shell list, output parsed then discarded"),
304 ("src/backend/blockcontroller/shell/ssh_pane.rs", "ssh_path", 1,
305 "sanitized: an SSH pane's system ssh, built here and sanitized by lifecycle.rs's sanitize_pty_command"),
306 ("src/backend/blockcontroller/shell/tests.rs", "\"sleep\"", 1,
307 "test: disposable `sleep 30` made its own process-group leader, to prove a group-kill reaches an isolated group"),
308 ("src/backend/claude_layout.rs", "\"git\"", 1,
309 "test: builds a throwaway repository with a linked worktree in a temp dir, to check the CLI's memory-folder rule"),
310 ("src/backend/container_cli.rs", "\"sh\"", 2,
311 "test: Unix only; runs the container CLI install and check scripts against a stub npm in a temp dir, to prove they are idempotent and write no marker on failure"),
312 ("src/backend/lsp/supervisor.rs", "&resolved", 1,
313 "sanitized: language server, sanitize_process_command"),
314 ("src/backend/mcp_probe.rs", "command", 1,
315 "sanitized: MCP server probe, sanitize_process_command"),
316 ("src/backend/process_tracker/windows.rs", "\"cmd\"", 1,
317 "test: a short-lived child placed in a Job Object to check the below-normal priority limit applies to it"),
318 ("src/backend/process_tracker/registry.rs", "\"cmd\"", 5,
319 "test: disposable children for the job-object tracker (assign, adopt, CPU accounting)"),
320 ("src/backend/process_tracker/registry.rs", "\"sh\"", 1,
321 "test: disposable child for the job-object tracker"),
322 ("src/backend/process_tracker/scan.rs", "\"sh\"", 1,
323 "test: disposable children the scan tracker tests find and kill"),
324 ("src/backend/process_tracker/cgroup_linux.rs", "\"sleep\"", 1,
325 "test: the argv the PTY-wrap test wraps; never spawned as built"),
326 ("src/backend/process_tracker/cgroup_linux.rs", "&argv[0]", 1,
327 "test: runs the wrapped argv the cgroup tests track and kill"),
328 ("src/backend/process_tracker/cgroup_linux.rs", "\"sh\"", 1,
329 "test: disposable children the cgroup tests track and kill"),
330 ("src/sagas/close_pane.rs", "\"cmd\"", 2,
331 "test: stand-in background dev servers (Stop ends one; a failed Stop keeps one)"),
332 ("src/sagas/close_pane.rs", "\"sh\"", 2,
333 "test: stand-in background dev servers (Stop ends one; a failed Stop keeps one)"),
334 ("src/server/agent_resources.rs", "\"cmd\"", 2,
335 "test: children for the survivor check and the forced tree kill"),
336 ("src/server/agent_resources.rs", "\"sh\"", 2,
337 "test: children for the survivor check and the forced tree kill"),
338 ("src/backend/remote/helper_install.rs", "\"sh\"", 2,
339 "test: runs the install scripts in a real sh against a temp home"),
340 ("src/backend/fs_ops/host_jobs.rs", "\"mkfifo\"", 1,
341 "test: makes a FIFO for the host transfer tests (Linux only)"),
342 ("src/backend/tower_remote.rs", "\"wsl.exe\"", 1,
343 "sanitized: Tower's helper inside a WSL distribution, sanitize_external_command"),
344 ("src/backend/remote/wsl.rs", "\"wsl.exe\"", 1,
345 "probe: `wsl.exe --list --quiet`, output parsed then discarded"),
346 ("src/backend/remote/address.rs", "\"wsl.exe\"", 1,
347 "sanitized: a WSL pane's address (`cat /proc/net/...`), sanitize_external_command"),
348 ("src/backend/remote/address.rs", "binary", 1,
349 "sanitized: an SSH pane's host (`ssh -G`), sanitize_external_command"),
350 ("src/backend/shell_node.rs", "\"cmd\"", 1,
351 "sanitized: persistent shell node, choke-point sanitize_process_command"),
352 ("src/backend/shell_node.rs", "\"sh\"", 1,
353 "sanitized: Unix branch of the same node, one sanitize_process_command covers both"),
354 ("src/backend/shell_node.rs", "program", 1,
355 "sanitized: an agent Shell run elsewhere (wsl.exe for WSL, ssh for SSH), the same sanitize_process_command"),
356 ("src/backend/tool_store.rs", "\"where\"", 2,
357 "probe: Windows PATH lookup for a tool; output parsed for a path, never executed"),
358 ("src/backend/tool_store.rs", "\"which\"", 2,
359 "probe: Unix PATH lookup for a tool; output parsed for a path, never executed"),
360 ("src/backend/tool_store.rs", "cmd", 1,
361 "sanitized: probe_version runs a third-party binary, strict policy (this PR)"),
362 ("src/crash_monitor.rs", "&exe", 1,
363 "own-exe: re-spawns OUR OWN binary as the crash monitor; it must stay this instance, so inheritance is the requirement here, not the defect"),
364 ("src/backend/cli_install.rs", "\"cmd\"", 1,
365 "sanitized: shell wrapper for npm install, sanitize_external_std_command"),
366 ("src/backend/cli_install.rs", "\"npm\"", 1,
367 "sanitized: npm runs arbitrary postinstall scripts, sanitize_external_std_command"),
368 ("src/backend/cli_prune.rs", "\"ps\"", 1,
369 "sanitized: lists running command lines to see which installed CLI is in use, sanitize_external_std_command"),
370 ("src/server/cli_handlers.rs", "\"where\"", 2,
371 "probe: Windows availability check for npm before install; runs nothing else"),
372 ("src/server/cli_handlers.rs", "\"which\"", 2,
373 "probe: Unix availability check for npm before install; runs nothing else"),
374 ("src/backend/fs_ops/git.rs", "\"git\"", 5,
375 "mixed: run_git (Files pane git markers, agents' work facts) is sanitize_external_command'd; four are this file's own #[cfg(test)] module"),
376 ("src/backend/work_facts/git.rs", "\"git\"", 1,
377 "test: builds a throwaway repository in a temp dir to check the work-facts probe; production git goes through fs_ops::git's run_git"),
378 ("src/backend/fs_ops/platform.rs", "\"explorer.exe\"", 2,
379 "sanitized: Files pane open/reveal (file manager, default app), sanitize_external_std_command in platform::spawn_detached"),
380 ("src/backend/fs_ops/platform.rs", "\"open\"", 2,
381 "sanitized: Files pane open/reveal (file manager, default app), sanitize_external_std_command in platform::spawn_detached"),
382 ("src/backend/fs_ops/platform.rs", "\"xdg-open\"", 2,
383 "sanitized: Files pane open/reveal (file manager, default app), sanitize_external_std_command in platform::spawn_detached"),
384 ("src/backend/fs_watch/pool.rs", "\"wsl.exe\"", 2,
385 "test: an ignored test writes and removes a file inside a WSL distro"),
386 ("src/server/editor_handlers.rs", "\"explorer.exe\"", 1,
387 "sanitized: file manager, strict policy (I7 fix, this PR)"),
388 ("src/server/editor_handlers.rs", "\"open\"", 1,
389 "sanitized: file manager, strict policy (I7 fix, this PR)"),
390 ("src/server/editor_handlers.rs", "\"xdg-open\"", 1,
391 "sanitized: file manager, strict policy (I7 fix, this PR)"),
392 ("src/server/identity_auth_spawn.rs", "&cli_path", 1,
393 "sanitized: provider OAuth CLI, strict policy"),
394 ("src/server/identity_auth_spawn.rs", "&spawn_program", 1,
395 "sanitized: provider OAuth CLI under a PTY (the resolved npm shim), strict policy"),
396 ("src/server/install_handlers.rs", "if cfg!(windows", 1,
397 "sanitized: installer shell, sanitize_external_command"),
398 ("src/server/shell_handlers.rs", "&shell", 1,
399 "sanitized: /api/v1/shell/create runner (resolved POSIX shell), sanitize_process_command"),
400 ("src/server/system_install_handlers.rs", "\"apt-cache\"", 1,
401 "probe: package-manager availability query, no install"),
402 ("src/server/system_install_handlers.rs", "\"brew\"", 1,
403 "probe: package-manager availability query, no install"),
404 ("src/server/system_install_handlers.rs", "\"winget\"", 1,
405 "probe: package-manager availability query, no install"),
406 ("src/server/system_install_handlers.rs", "&step.program", 1,
407 "sanitized: package-manager install step, sanitize_external_command"),
408 ("src/server/system_install_handlers.rs", "program", 1,
409 "not a spawn: appears inside this module's header comment describing the argv rule"),
410 ("src/server/voice.rs", "&cli", 1,
411 "sanitized: voice CLI, sanitize_process_command"),
412 ("src/test_support.rs", "\"rustc\"", 1,
413 "test: builds the fixture App Server binary once per source hash, shared by the app_server tests"),
414 ("src/util.rs", "\"cmd\"", 3,
415 "mixed: one is open_browser's sanitized Windows branch; two are in this file's own #[cfg(test)] module"),
416 ("src/util.rs", "\"open\"", 1,
417 "sanitized: browser launch, strict policy (I7 fix, this PR)"),
418 ("src/util.rs", "\"xdg-open\"", 1,
419 "sanitized: browser launch, strict policy (I7 fix, this PR)"),
420 ];
421
422 fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
423 let Ok(entries) = std::fs::read_dir(dir) else { return };
424 for entry in entries.flatten() {
425 let path = entry.path();
426 if path.is_dir() {
427 rs_files(&path, out);
428 } else if path.extension().is_some_and(|e| e == "rs") {
429 out.push(path);
430 }
431 }
432 }
433
434 fn line_spawns(line: &str) -> bool {
437 for marker in SPAWN_MARKERS {
438 let mut from = 0;
439 while let Some(idx) = line[from..].find(marker) {
440 let at = from + idx;
441 let prev_is_ident = at > 0
442 && line[..at]
443 .chars()
444 .next_back()
445 .is_some_and(|c| c.is_alphanumeric() || c == '_');
446 if !prev_is_ident {
447 return true;
448 }
449 from = at + marker.len();
450 }
451 }
452 false
453 }
454
455 fn program_of(line: &str) -> String {
457 let Some(at) = line.find("new(") else { return "?".to_string() };
458 let rest = &line[at + 4..];
459 let end = rest.find([',', ')']).unwrap_or(rest.len());
460 rest[..end].trim().to_string()
461 }
462
463 fn actual_inventory() -> BTreeMap<(String, String), usize> {
464 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
465 let mut files = Vec::new();
466 rs_files(&root.join("src"), &mut files);
467 assert!(files.len() > 50, "source walk found only {} files — the walk is broken", files.len());
468
469 let mut map: BTreeMap<(String, String), usize> = BTreeMap::new();
470 for file in &files {
471 let rel = file.strip_prefix(root).unwrap_or(file).to_string_lossy().replace('\\', "/");
472 if rel.ends_with("backend/pane_env.rs") {
475 continue;
476 }
477 let Ok(src) = std::fs::read_to_string(file) else { continue };
478 for line in src.lines() {
479 if line_spawns(line) {
480 *map.entry((rel.clone(), program_of(line))).or_default() += 1;
481 }
482 }
483 }
484 map
485 }
486
487 #[test]
488 fn the_spawn_surface_matches_the_inventory_exactly() {
489 let actual = actual_inventory();
490 let expected: BTreeMap<(String, String), usize> = SPAWN_INVENTORY
491 .iter()
492 .map(|(f, p, c, _)| ((f.to_string(), p.to_string()), *c))
493 .collect();
494
495 let mut problems = Vec::new();
496 for (key, count) in &actual {
497 match expected.get(key) {
498 None => problems.push(format!(
499 "NEW spawn site: {} -> Command::new({}) x{count}",
500 key.0, key.1
501 )),
502 Some(exp) if exp != count => problems.push(format!(
503 "COUNT changed: {} -> Command::new({}): inventory says {exp}, source has {count}",
504 key.0, key.1
505 )),
506 _ => {}
507 }
508 }
509 for key in expected.keys() {
510 if !actual.contains_key(key) {
511 problems.push(format!("STALE entry (no longer in source): {} -> {}", key.0, key.1));
512 }
513 }
514
515 assert!(
516 problems.is_empty(),
517 "I7: srv's process-spawn surface changed.\n {}\n\n\
518 If you added a spawn: apply the right `pane_env::sanitize_*` to it, then add \n\
519 it to SPAWN_INVENTORY with a classification. If you removed one, drop its row. \n\
520 The inventory is pinned deliberately — a new spawn must not be able to hide \n\
521 beside an already-sanitized one (#3365 P1). Background: \n\
522 docs/retro/retro-env-inheritance-instance-isolation-breach-2026-09-17.md",
523 problems.join("\n ")
524 );
525 }
526
527 #[test]
528 fn every_inventory_row_is_classified_and_justified() {
529 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
530 for (file, program, _, reason) in SPAWN_INVENTORY {
531 assert!(
532 CLASSES.iter().any(|c| reason.starts_with(c)),
533 "{file} -> {program}: reason must start with one of {CLASSES:?}, got {reason:?}"
534 );
535 assert!(
536 reason.len() > 30,
537 "{file} -> {program}: needs a real reason, got {reason:?}"
538 );
539 if reason.starts_with("sanitized:") {
543 let src = std::fs::read_to_string(root.join(file)).expect("read inventory file");
544 assert!(
545 SANITIZERS_FOR_AUDIT.iter().any(|s| src.contains(s)),
546 "{file} -> {program} is classified `sanitized:` but the file calls no sanitizer"
547 );
548 }
549 }
550 }
551
552 const SANITIZERS_FOR_AUDIT: &[&str] = &[
553 "sanitize_pty_command",
554 "sanitize_process_command",
555 "sanitize_external_command",
556 "sanitize_external_pty_command",
557 "sanitize_external_std_command",
558 ];
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
568 fn the_identity_vars_that_caused_the_breach_are_stripped() {
569 for key in [
570 "AGENTMUX_CHANNEL", "AGENTMUX_DATA_DIR", "AGENTMUX_DATA_HOME", "AGENTMUX_EXTRACTED_RUN", "AGENTMUX_CONFIG_DIR",
575 "AGENTMUX_CONFIG_HOME",
576 "AGENTMUX_CEF_CACHE_DIR",
577 "AGENTMUX_INSTANCE_DIR",
578 "AGENTMUX_INSTANCE_RUNTIME_DIR",
579 "AGENTMUX_AGENTS_DIR",
580 "AGENTMUX_RUNTIME_MODE",
581 "AGENTMUX_CLONE_ID",
582 "AGENTMUX_APP_PATH",
583 "AGENTMUX_SRV_PIPE_PATH",
584 "AGENTMUX_SPLASH_READY_FILE",
585 "AGENTMUX_HOME_OVERRIDE",
586 ] {
587 assert!(should_strip(key), "{key} must not be inheritable by a pane");
588 }
589 }
590
591 #[test]
592 fn the_helper_keep_set_survives() {
593 for key in PANE_ENV_KEEP {
594 assert!(!should_strip(key), "{key} is required by an in-pane helper");
595 }
596 }
597
598 #[test]
602 fn an_unknown_agentmux_var_is_stripped_by_default() {
603 assert!(should_strip("AGENTMUX_SOME_FUTURE_IDENTITY_VAR"));
604 }
605
606 #[test]
607 fn non_agentmux_vars_are_left_alone() {
608 for key in ["PATH", "HOME", "TERM", "SSH_AUTH_SOCK", "LANG", "SystemRoot", "AGENT_ID"] {
609 assert!(!should_strip(key), "{key} is not ours to strip");
610 }
611 }
612
613 #[test]
616 fn the_nesting_sentinel_is_never_stripped() {
617 assert!(!should_strip(NESTING_SENTINEL_KEY));
618 }
619
620 #[test]
624 fn the_external_policy_strips_even_the_helper_keep_set() {
625 std::env::set_var("AGENTMUX_LOCAL_URL", "http://127.0.0.1:1");
626 let stripped = all_agentmux_keys();
627 assert!(stripped.iter().any(|k| k == "AGENTMUX_LOCAL_URL"),
628 "external processes must not receive the instance API endpoint");
629 assert!(!stripped.iter().any(|k| k == NESTING_SENTINEL_KEY),
630 "the sentinel carries no identity and must survive");
631 std::env::remove_var("AGENTMUX_LOCAL_URL");
632 }
633
634 #[test]
636 fn a_pane_is_told_which_build_it_runs_in() {
637 let mut cmd = tokio::process::Command::new("agent");
638 sanitize_process_command(&mut cmd);
639 let envs: std::collections::BTreeMap<String, String> = cmd
640 .as_std()
641 .get_envs()
642 .filter_map(|(k, v)| Some((k.to_str()?.to_string(), v?.to_str()?.to_string())))
643 .collect();
644 let version = env!("CARGO_PKG_VERSION");
645 assert_eq!(envs["AGENTMUX_VERSION"], version, "the plain version: log pointer files are named by it");
646 assert!(envs["AGENTMUX_BUILD"].starts_with(version));
647 assert!(!envs.contains_key("AGENTMUX_CHANNEL"), "the data channel itself still never reaches a pane");
648 }
649
650 #[test]
651 fn keys_to_strip_reads_the_live_environment() {
652 std::env::set_var("AGENTMUX_TEST_ONLY_LEAK", "1");
653 assert!(keys_to_strip().iter().any(|k| k == "AGENTMUX_TEST_ONLY_LEAK"));
654 std::env::remove_var("AGENTMUX_TEST_ONLY_LEAK");
655 }
656}