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
- Pass a plain skill name that matches one directory directly under the catalog repo's skills/ (no slashes, no dots).
- Run the install with a valid name and read the 'Available skills' list in the resulting error to pick the correct name.
- 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
- Treat skill names as identifiers, not paths: validate against ^[A-Za-z0-9._-]+$ at the CLI boundary.
- Never build skill names by concatenating user-supplied path fragments.
- Fail loudly on names containing separators before any clone or install happens.
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
- skill '{$skill}' in {$url} is a symlink; catalog skills must
- skill catalog {$url} has a skills/ directory that resolves o
- Invalid skill name: {name}
- Skill security audit failed: {}
- Refusing to copy symlinked skill source path: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/6a37f83ed7adec68.
Report an issue: GitHub.