From c55c7e47103f0051c8bcb8bfd27edeb666453c24 Mon Sep 17 00:00:00 2001 From: twtrubiks Date: Mon, 19 Oct 2026 11:04:36 +0800 Subject: [PATCH] fix(runtime): skip non-UTF-8 environment variables instead of panicking (#3356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std::env::vars() panics on any non-UTF-8 value. zsh exports a metafied $_ when the command path contains non-ASCII characters (e.g. 下載), so claw panicked on startup. Use vars_os() and skip entries that are not valid UTF-8. --- rust/crates/runtime/src/remote.rs | 14 ++++++++-- rust/crates/runtime/src/sandbox.rs | 6 +++- .../tests/cli_flags_and_config_defaults.rs | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/rust/crates/runtime/src/remote.rs b/rust/crates/runtime/src/remote.rs index 24ee780c..69a93e76 100644 --- a/rust/crates/runtime/src/remote.rs +++ b/rust/crates/runtime/src/remote.rs @@ -66,7 +66,7 @@ pub struct UpstreamProxyState { impl RemoteSessionContext { #[must_use] pub fn from_env() -> Self { - Self::from_env_map(&env::vars().collect()) + Self::from_env_map(&utf8_env_map()) } #[must_use] @@ -89,7 +89,7 @@ impl RemoteSessionContext { impl UpstreamProxyBootstrap { #[must_use] pub fn from_env() -> Self { - Self::from_env_map(&env::vars().collect()) + Self::from_env_map(&utf8_env_map()) } #[must_use] @@ -234,6 +234,16 @@ pub fn inherited_upstream_proxy_env( .collect() } +/// Snapshot of the process environment, skipping non-UTF-8 entries. +/// +/// `env::vars()` panics on non-UTF-8 values (e.g. zsh exports a metafied `$_` +/// when the command path contains non-ASCII characters). +fn utf8_env_map() -> BTreeMap { + env::vars_os() + .filter_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?))) + .collect() +} + fn default_ca_bundle_path() -> PathBuf { env::var_os("HOME") .map_or_else(|| PathBuf::from("."), PathBuf::from) diff --git a/rust/crates/runtime/src/sandbox.rs b/rust/crates/runtime/src/sandbox.rs index 2536a28c..967787bf 100644 --- a/rust/crates/runtime/src/sandbox.rs +++ b/rust/crates/runtime/src/sandbox.rs @@ -109,7 +109,11 @@ impl SandboxConfig { pub fn detect_container_environment() -> ContainerEnvironment { let proc_1_cgroup = fs::read_to_string("/proc/1/cgroup").ok(); detect_container_environment_from(SandboxDetectionInputs { - env_pairs: env::vars().collect(), + // `env::vars()` panics on non-UTF-8 values (e.g. zsh exports a + // metafied `$_` for non-ASCII paths); skip those entries instead. + env_pairs: env::vars_os() + .filter_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?))) + .collect(), dockerenv_exists: Path::new("/.dockerenv").exists(), containerenv_exists: Path::new("/run/.containerenv").exists(), proc_1_cgroup: proc_1_cgroup.as_deref(), diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs index 95fd133d..c063ebf6 100644 --- a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -257,6 +257,34 @@ fn local_smoke_commands_do_not_require_live_credentials() { fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); } +#[cfg(unix)] +#[test] +fn doctor_tolerates_non_utf8_environment_values() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + // given: zsh exports a metafied (non-UTF-8) `$_` when the command path + // contains non-ASCII characters such as `下載` + let temp_dir = unique_temp_dir("non-utf8-env"); + let config_home = temp_dir.join(".claw"); + fs::create_dir_all(&config_home).expect("config home should exist"); + let metafied_path = OsStr::from_bytes(b"/home/user/\xe4\xb8\x83\xab\xe8\xbc\x83\xa9/claw"); + + // when + let output = offline_command_in(&temp_dir, &config_home) + .env("_", metafied_path) + .arg("doctor") + .output() + .expect("claw should launch"); + + // then + assert_success(&output); + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + assert!(stdout.contains("Doctor")); + + fs::remove_dir_all(temp_dir).expect("cleanup temp dir"); +} + #[test] fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() { let temp_dir = unique_temp_dir("subcommand-help");