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
- Replace illegal characters: use fs_read instead of fs.read, web_search instead of web search.
- Use the tool's declared identifier (the name it is invoked by), not its description.
- Pre-validate with the same charset rule: trim, lowercase, then all chars in [a-z0-9_-].
- 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
- Always pass the tool's declared identifier, never its display label.
- Constrain tool-name inputs in forms/config to [a-z0-9_-] with a lint rule.
- Sanitize generated selectors (replace spaces/dots with underscores) before calling estop APIs.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Tool name must not be empty
- AcpChannel.request_choice requires at least one choice
- AcpChannel.request_multi_choice requires at least one choice
- modal custom_id exceeds Discord's 100-char limit; cannot ope
- slash command registration failed for '{name}' ({status}): {
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/1ed07eab383426ba.
Report an issue: GitHub.