zeroclaw-labs/zeroclaw · error · anyhow::Error

skill '{$skill}' in {$url} resolves outside the cloned catal

Error message

skill '{$skill}' in {$url} resolves outside the cloned catalog; refusing to install

What it means

Thrown by the catalog skill installer after canonicalizing the selected skills/<skill> path. It verifies the canonical path still starts with the catalog's skills root; if a skill name containing `..` or separator tricks resolves outside the clone, the install is refused. This is a classic path-traversal guard applied to the skill-name argument.

Source

Thrown at crates/zeroclaw-runtime/src/skills/mod.rs:2479

                        skill_dir.display()
                    )
                });
            }
        };
        if entry_meta.file_type().is_symlink() {
            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(
                "cli-skills-install-catalog-skill-symlink",
                &[("skill", skill_name), ("url", url)]
            ));
        }
        let selected = skill_dir.canonicalize().with_context(|| {
            format!(
                "failed to canonicalize selected skill {}",
                skill_dir.display()
            )
        })?;
        if !selected.starts_with(&skills_root) {
            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(
                "cli-skills-install-catalog-skill-escapes",
                &[("skill", skill_name), ("url", url)]
            ));
        }
        if !selected.is_dir() {
            let available = list_contained_catalog_skill_names(&skills_root);
            if available.is_empty() {
                anyhow::bail!(crate::i18n::get_required_cli_string_with_args(
                    "cli-skills-install-skill-not-in-catalog-empty",
                    &[("skill", skill_name), ("url", url)]
                ));
            }
            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(
                "cli-skills-install-skill-not-in-catalog",
                &[
                    ("skill", skill_name),
                    ("url", url),
                    ("available", &available.join(", ")),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Pass a plain skill name that matches one directory directly under the catalog repo's skills/ (no slashes, no dots).
  2. Run the install with a valid name and read the 'Available skills' list in the resulting error to pick the correct name.
  3. If you maintain the catalog, ensure every advertised skill is a directory directly under skills/.

Example fix

# before
zeroclaw skills install "../shared/my-skill" --catalog https://github.com/org/catalog

# after
zeroclaw skills install my-skill --catalog https://github.com/org/catalog
Defensive patterns

Strategy: validation

Validate before calling

fn is_bare_skill_name(name: &str) -> bool {
    !name.is_empty()
        && !name.contains('/')
        && !name.contains('\\')
        && name != "."
        && name != ".."
        && !name.contains("..")
}

Try / catch

match install_catalog_skill(url, skill) {
    Err(e) if e.to_string().contains("resolves outside the cloned catalog") => {
        eprintln!("rejected skill name {skill:?}: use a plain directory name under skills/");
    }
    rest => rest?,
}

Prevention

When it happens

Trigger: Passing a skill name with traversal or separators to the catalog install API, e.g. `my-skill/../../elsewhere`, `../../etc/passwd`, or any name whose canonicalized dir escapes the cloned catalog's skills/ root. The check is `!selected.starts_with(&skills_root)` after `skill_dir.canonicalize()`.

Common situations: Scripts that build skill names from user input or file paths; typos that paste a path instead of a bare skill name; adversarial input fuzzing the CLI; copy-pasted names with trailing `/..`.

Related errors


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