fix(runtime): skip non-UTF-8 environment variables instead of panicking (#3356)

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.
This commit is contained in:
twtrubiks authored and GitHub committed 2026-10-19 12:04:36 +09:00
1 parent 917a9e28ee
commit c55c7e4710
3 files changed
+45 -3

No files matched your search

+12 -2
View File
@@ -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<String, String> {
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)
+5 -1
View File
@@ -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(),
@@ -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");