From d115b28a1f2b0f491744b0bbd97582b6002469da Mon Sep 17 00:00:00 2001 From: SimianAstronaut7 <79373020+SimianAstronaut7@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:22:57 -0400 Subject: [PATCH] fix(daemon): expand tilde to home directory in file paths (#3424) Rust treats `~` as a literal path character, not a home directory shorthand. Several config resolution paths used `PathBuf::from()` on user-provided strings without expanding `~` first, causing a literal `~` folder to be created in the working directory. Apply `shellexpand::tilde()` to all user-facing path inputs: - ZEROCLAW_CONFIG_DIR env var (config/schema.rs, onboard/wizard.rs) - ZEROCLAW_WORKSPACE env var (config/schema.rs, onboard/wizard.rs, channels/matrix.rs) - active_workspace.toml marker file config_dir (config/schema.rs) The WhatsApp Web session_path was already correctly expanded via shellexpand::tilde() in whatsapp_web.rs. Closes #3417 Co-authored-by: Claude Opus 4.6 Co-authored-by: Argenis --- src/channels/matrix.rs | 7 +++++-- src/config/schema.rs | 11 +++++++---- src/onboard/wizard.rs | 5 +++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/channels/matrix.rs b/src/channels/matrix.rs index 38a299716..8e5ad0e4a 100644 --- a/src/channels/matrix.rs +++ b/src/channels/matrix.rs @@ -765,8 +765,11 @@ impl Channel for MatrixChannel { // Download media to workspace if present let body = if let Some((url, filename)) = media_download { let workspace = std::path::PathBuf::from( - std::env::var("ZEROCLAW_WORKSPACE") - .unwrap_or_else(|_| "/tmp/zeroclaw-uploads".to_string()), + shellexpand::tilde( + &std::env::var("ZEROCLAW_WORKSPACE") + .unwrap_or_else(|_| "/tmp/zeroclaw-uploads".to_string()), + ) + .as_ref(), ); let _ = tokio::fs::create_dir_all(&workspace).await; let dest = workspace.join(&filename); diff --git a/src/config/schema.rs b/src/config/schema.rs index 5a738b2df..7eb477337 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -4090,7 +4090,8 @@ async fn load_persisted_workspace_dirs( return Ok(None); } - let parsed_dir = PathBuf::from(raw_config_dir); + let expanded_dir = shellexpand::tilde(raw_config_dir); + let parsed_dir = PathBuf::from(expanded_dir.as_ref()); let config_dir = if parsed_dir.is_absolute() { parsed_dir } else { @@ -4233,7 +4234,7 @@ async fn resolve_runtime_config_dirs( if let Ok(custom_config_dir) = std::env::var("ZEROCLAW_CONFIG_DIR") { let custom_config_dir = custom_config_dir.trim(); if !custom_config_dir.is_empty() { - let zeroclaw_dir = PathBuf::from(custom_config_dir); + let zeroclaw_dir = PathBuf::from(shellexpand::tilde(custom_config_dir).as_ref()); return Ok(( zeroclaw_dir.clone(), zeroclaw_dir.join("workspace"), @@ -4244,8 +4245,9 @@ async fn resolve_runtime_config_dirs( if let Ok(custom_workspace) = std::env::var("ZEROCLAW_WORKSPACE") { if !custom_workspace.is_empty() { + let expanded = shellexpand::tilde(&custom_workspace); let (zeroclaw_dir, workspace_dir) = - resolve_config_dir_for_workspace(&PathBuf::from(custom_workspace)); + resolve_config_dir_for_workspace(&PathBuf::from(expanded.as_ref())); return Ok(( zeroclaw_dir, workspace_dir, @@ -5094,8 +5096,9 @@ impl Config { // Workspace directory: ZEROCLAW_WORKSPACE if let Ok(workspace) = std::env::var("ZEROCLAW_WORKSPACE") { if !workspace.is_empty() { + let expanded = shellexpand::tilde(&workspace); let (_, workspace_dir) = - resolve_config_dir_for_workspace(&PathBuf::from(workspace)); + resolve_config_dir_for_workspace(&PathBuf::from(expanded.as_ref())); self.workspace_dir = workspace_dir; } } diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index e90913f24..85ca050a2 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -425,7 +425,7 @@ fn resolve_quick_setup_dirs_with_home(home: &Path) -> (PathBuf, PathBuf) { if let Ok(custom_config_dir) = std::env::var("ZEROCLAW_CONFIG_DIR") { let trimmed = custom_config_dir.trim(); if !trimmed.is_empty() { - let config_dir = PathBuf::from(trimmed); + let config_dir = PathBuf::from(shellexpand::tilde(trimmed).as_ref()); return (config_dir.clone(), config_dir.join("workspace")); } } @@ -433,8 +433,9 @@ fn resolve_quick_setup_dirs_with_home(home: &Path) -> (PathBuf, PathBuf) { if let Ok(custom_workspace) = std::env::var("ZEROCLAW_WORKSPACE") { let trimmed = custom_workspace.trim(); if !trimmed.is_empty() { + let expanded = shellexpand::tilde(trimmed); return crate::config::schema::resolve_config_dir_for_workspace(&PathBuf::from( - trimmed, + expanded.as_ref(), )); } }