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

  1. Filter empty/whitespace entries out of the tools list before calling engage/resume.
  2. Validate at the input boundary (config parse, API handler) that tool names are non-empty.
  3. Use the same normalization rules as ZeroClaw (trim + lowercase + [a-z0-9_-]) when pre-validating.
  4. 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

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


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