zeroclaw-labs/zeroclaw · warning
Tool name must not be empty
Error message
Tool name must not be empty
What it means
normalize_tool_name was given a string that is empty after trimming (or contains only whitespace). Tool names passed to estop engage/resume (e.g. ResumeSelector::Tools) must be non-empty so the frozen-tools list stays meaningful; an empty selector could never match anything and usually signals a caller bug.
Source
Thrown at crates/zeroclaw-runtime/src/security/estop.rs:264
Ok(())
}
}
pub fn resolve_state_file_path(config_dir: &Path, state_file: &str) -> PathBuf {
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();View on GitHub (pinned to 88bb9c8533)
Solutions
- Filter empty/whitespace entries out of the tools list before calling engage/resume.
- Validate at the input boundary (config parse, API handler) that tool names are non-empty.
- Use the same normalization rules as ZeroClaw (trim + lowercase + [a-z0-9_-]) when pre-validating.
- Log which raw value was rejected so the source of the blank entry is findable.
Example fix
// before estop.resume(ResumeSelector::Tools(vec!["".into(), "fs_read".into()]), None, None)?; // after — strip empties first let tools = ["fs_read", "web_search"]; assert!(tools.iter().all(|t| !t.trim().is_empty())); estop.resume(ResumeSelector::Tools(tools.to_vec()), None, None)?;
Defensive patterns
Strategy: validation
Validate before calling
let tools: Vec<String> = raw_tools
.into_iter()
.map(|t| t.trim().to_ascii_lowercase())
.filter(|t| !t.is_empty())
.collect();
assert!(!tools.is_empty(), "tool selector list must contain at least one name"); 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() == "Tool name must not be empty" => {
// filter blanks from the source list and retry; log the offending raw input
} Prevention
- Normalize and filter tool lists at the input boundary (config parse, API handler).
- Reject blank entries in forms/UI before submission.
- Unit-test tool-name normalization paths with whitespace-only inputs.
When it happens
Trigger: Calling engage() or resume(ResumeSelector::Tools(...)) with an empty string, a "", or a whitespace-only entry in the tools list; building the list from unvalidated config or user input.
Common situations: Config file with `tools = [""]` placeholders; splitting a comma-separated string that has a trailing comma; forms/APIs that submit before the user types a tool name.
Related errors
- Tool name '{raw}' contains invalid characters
- 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/69554816524e1c4a.
Report an issue: GitHub.