zeroclaw-labs/zeroclaw · warning

Tool name '{raw}' contains invalid characters

Error message

Tool name '{raw}' contains invalid characters

What it means

normalize_tool_name found characters outside the allowed set. After trim + lowercase, a tool name may contain only ASCII alphanumerics, underscore, and hyphen; anything else (dots, spaces, slashes, unicode) is rejected so tool matching stays unambiguous.

Source

Thrown at crates/zeroclaw-runtime/src/security/estop.rs:270

    let expanded = shellexpand::tilde(state_file).into_owned();
    let path = PathBuf::from(expanded);
    if path.is_absolute() {
        path
    } else {
        config_dir.join(path)
    }
}

fn normalize_tool_name(raw: &str) -> Result<String> {
    let value = raw.trim().to_ascii_lowercase();
    if value.is_empty() {
        anyhow::bail!("Tool name must not be empty");
    }
    if !value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
    {
        anyhow::bail!("Tool name '{raw}' contains invalid characters");
    }
    Ok(value)
}

fn dedup_sort(values: &[String]) -> Vec<String> {
    let mut deduped = values
        .iter()
        .map(|value| value.trim())
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
        .collect::<Vec<_>>();
    deduped.sort_unstable();
    deduped.dedup();
    deduped
}

fn now_rfc3339() -> String {
    let secs = SystemTime::now()

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Replace illegal characters: use fs_read instead of fs.read, web_search instead of web search.
  2. Use the tool's declared identifier (the name it is invoked by), not its description.
  3. Pre-validate with the same charset rule: trim, lowercase, then all chars in [a-z0-9_-].
  4. If a tool genuinely needs other characters, wrap/alias it behind a compliant name.

Example fix

// before
estop.resume(ResumeSelector::Tools(vec!["fs.read file".into()]), None, None)?;

// after
estop.resume(ResumeSelector::Tools(vec!["fs_read_file".into()]), None, None)?;
Defensive patterns

Strategy: validation

Validate before calling

let ok = raw.chars().all(|c| c.is_ascii_alphanumeric() || "_-".contains(c))
    && !raw.trim().is_empty();

Type guard

fn is_valid_tool_name(raw: &str) -> bool {
    let v = raw.trim().to_ascii_lowercase();
    !v.is_empty() && v.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}

Try / catch

Err(e) if e.to_string().contains("contains invalid characters") => {
    // map the raw name to its declared tool identifier (e.g. "fs.read" -> "fs_read") and retry
}

Prevention

When it happens

Trigger: Passing names like "fs.read_file" (dot), "web search" (space), "tools/*" (slash), or a display label with unicode; names sourced from unvalidated config or user input.

Common situations: Using the tool's display/title string instead of its identifier; copy-pasting tool names from docs that pretty-print them; generating selectors from natural-language input.

Understand the failure class

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/1ed07eab383426ba. Report an issue: GitHub.