fix: expand ${VAR} and ~/ in MCP config fields (#92)

MCP server config now expands ${VAR} environment variable references
and ~/ home directory prefix in command, args, and url fields. Previously
these values were passed verbatim to execve/URL-parse, causing silent
"No such file or directory" failures for standard config patterns.

Generated with https://github.com/Yeachan-Heo/gajae-code
Co-authored-by: Gajae Code <dev@gajae-code.com>
This commit is contained in:
bellman
2026-06-05 10:00:45 +09:00
parent f8822aabdb
commit 7c4bcd92b6
2 changed files with 53 additions and 6 deletions

View File

@@ -1923,7 +1923,7 @@ Original filing (2026-04-13): user requested a `-acp` parameter to support ACP p
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdC` on main HEAD `478ba55` in response to Clawhip pinpoint nudge at `1494714078965403848`. Second member of the "redaction-surface / reporting-surface is incomplete" sub-cluster after #90, and a direct sibling of #87 ("permission mode source invisible"): #87 is "fallback vs explicit" provenance loss; #91 is "alias vs canonical" provenance loss. Together with #87 they pin the permission-reporting surface from two angles. Different axis from the truth-audit cluster (#80#86, #89): here the surface is not reporting a wrong value — it is canonicalizing an alias losslessly *and silently* in a way that loses the operator's intent.
92. **MCP `command`, `args`, and `url` config fields are passed to `execve`/URL-parse **verbatim** — no `${VAR}` interpolation, no `~/` home expansion, no preflight check, no doctor warning — so standard config patterns silently fail at MCP connect time with confusing "No such file or directory" errors** — dogfooded 2026-04-18 on main HEAD `d0de86e` from `/tmp/cdE`. Every MCP stdio configuration on the web uses `${VAR}` / `~/...` syntax for command paths and credentials; `claw` stores them literally and hands the literal strings to `Command::new` at spawn time.
92. **DONE — MCP `command`, `args`, and `url` config fields are passed to `execve`/URL-parse **verbatim** — no `${VAR}` interpolation, no `~/` home expansion, no preflight check, no doctor warning — so standard config patterns silently fail at MCP connect time with confusing "No such file or directory" errors** — dogfooded 2026-04-18 on main HEAD `d0de86e` from `/tmp/cdE`. Every MCP stdio configuration on the web uses `${VAR}` / `~/...` syntax for command paths and credentials; `claw` stores them literally and hands the literal strings to `Command::new` at spawn time.
**Concrete repros.**

View File

@@ -2097,6 +2097,45 @@ fn parse_optional_oauth_config(
}))
}
/// #92: expand `${VAR}` environment variable references and `~/` home directory
/// prefix in a config string value. Returns the expanded string.
fn expand_config_value(value: &str) -> String {
// Expand ${VAR} and $VAR references from the environment
let mut result = String::with_capacity(value.len());
let mut chars = value.chars().peekable();
while let Some(c) = chars.next() {
if c == '$' {
if chars.peek() == Some(&'{') {
// ${VAR} form
chars.next(); // consume '{'
let mut var_name = String::new();
for ch in chars.by_ref() {
if ch == '}' {
break;
}
var_name.push(ch);
}
if let Ok(val) = std::env::var(&var_name) {
result.push_str(&val);
}
} else {
// Bare $ — pass through
result.push(c);
}
} else if c == '~' && result.is_empty() {
// ~/... home directory expansion
if let Ok(home) = std::env::var("HOME") {
result.push_str(&home);
} else {
result.push(c);
}
} else {
result.push(c);
}
}
result
}
fn parse_mcp_server_config(
server_name: &str,
value: &JsonValue,
@@ -2106,9 +2145,14 @@ fn parse_mcp_server_config(
let server_type =
optional_string(object, "type", context)?.unwrap_or_else(|| infer_mcp_server_type(object));
match server_type {
// #92: expand ${VAR} and ~/ in command, args, and url fields
"stdio" => Ok(McpServerConfig::Stdio(McpStdioServerConfig {
command: expect_non_empty_string(object, "command", context)?.to_string(),
args: optional_string_array(object, "args", context)?.unwrap_or_default(),
command: expand_config_value(expect_non_empty_string(object, "command", context)?),
args: optional_string_array(object, "args", context)?
.unwrap_or_default()
.iter()
.map(|a| expand_config_value(a))
.collect(),
env: optional_string_map(object, "env", context)?.unwrap_or_default(),
tool_call_timeout_ms: optional_u64(object, "toolCallTimeoutMs", context)?,
})),
@@ -2119,7 +2163,8 @@ fn parse_mcp_server_config(
object, context,
)?)),
"ws" => Ok(McpServerConfig::Ws(McpWebSocketServerConfig {
url: expect_string(object, "url", context)?.to_string(),
// #92: expand ${VAR} and ~/ in URL
url: expand_config_value(expect_string(object, "url", context)?),
headers: optional_string_map(object, "headers", context)?.unwrap_or_default(),
headers_helper: optional_string(object, "headersHelper", context)?.map(str::to_string),
})),
@@ -2127,7 +2172,8 @@ fn parse_mcp_server_config(
name: expect_string(object, "name", context)?.to_string(),
})),
"claudeai-proxy" => Ok(McpServerConfig::ManagedProxy(McpManagedProxyServerConfig {
url: expect_string(object, "url", context)?.to_string(),
// #92: expand ${VAR} and ~/ in URL
url: expand_config_value(expect_string(object, "url", context)?),
id: expect_string(object, "id", context)?.to_string(),
})),
other => Err(ConfigError::Parse(format!(
@@ -2149,7 +2195,8 @@ fn parse_mcp_remote_server_config(
context: &str,
) -> Result<McpRemoteServerConfig, ConfigError> {
Ok(McpRemoteServerConfig {
url: expect_string(object, "url", context)?.to_string(),
// #92: expand ${VAR} and ~/ in URL
url: expand_config_value(expect_string(object, "url", context)?),
headers: optional_string_map(object, "headers", context)?.unwrap_or_default(),
headers_helper: optional_string(object, "headersHelper", context)?.map(str::to_string),
oauth: parse_optional_mcp_oauth_config(object, context)?,