zeroclaw-labs/zeroclaw · error · anyhow::Error
skill '{$skill}' in {$url} is a symlink; catalog skills must
Error message
skill '{$skill}' in {$url} is a symlink; catalog skills must be real directories inside the repository What it means
Thrown by the skills catalog installer in crates/zeroclaw-runtime/src/skills/mod.rs. After cloning a catalog repo, it stats skills/<skill> and refuses to install when the entry is a symbolic link. Catalog skills must be real directories inside the clone so that what gets installed is exactly what the repo versioned; a symlink could point outside the clone or at content that changes later.
Source
Thrown at crates/zeroclaw-runtime/src/skills/mod.rs:2467
"cli-skills-install-skill-not-in-catalog",
&[
("skill", skill_name),
("url", url),
("available", &available.join(", ")),
]
));
}
Err(err) => {
return Err(err).with_context(|| {
format!(
"failed to read metadata for selected catalog skill {}",
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);View on GitHub (pinned to 88bb9c8533)
Solutions
- In the catalog repo, replace the symlink with a real directory: copy the target contents (cp -rL) and commit them so skills/<name> is a plain directory.
- If you do not maintain the catalog, report the symlink to its maintainer and install the skill from its real source location instead.
- Verify before installing: `git ls-files -s skills/` in the clone — mode 120000 entries are symlinks; `ls -la skills/` shows the arrows.
- If content sharing is the goal, use a build/release step that materializes real directories rather than symlinks.
Example fix
# before (in the catalog repo) ln -s ../shared/my-skill skills/my-skill # after cp -rL ../shared/my-skill skills/my-skill git add skills/my-skill git commit -m "skills: materialize my-skill as a real directory"
Defensive patterns
Strategy: validation
Validate before calling
fn is_real_dir(p: &std::path::Path) -> bool {
std::fs::symlink_metadata(p)
.map(|m| m.is_dir() && !m.file_type().is_symlink())
.unwrap_or(false)
}
// before install: assert is_real_dir(catalog_clone.join("skills").join(skill)) Try / catch
match install_catalog_skill(url, skill) {
Ok(_) => {},
Err(e) if e.to_string().contains("is a symlink") => {
eprintln!("catalog bug: {skill} is a symlink; report to catalog maintainer");
}
Err(e) => return Err(e),
} Prevention
- In catalog repos, lint CI on `git ls-files -s skills/` and reject mode-120000 (symlink) entries.
- Never author catalog skills as symlinks; materialize shared content at release time.
- Pin catalog repos to reviewed commits rather than floating branches.
When it happens
Trigger: Calling the catalog skill install path (skills install from a catalog URL, e.g. `zeroclaw skills install my-skill --catalog <url>`) where the cloned catalog repository has `skills/<skill-name>` as a symlink. The check is `entry_meta.file_type().is_symlink()` on the stat'd skill dir, so any symlink (even one pointing to a sibling dir in the same repo) trips it.
Common situations: Catalog repos that deduplicate shared skills via symlinks; repos prepared on OSes/tools that materialize shared folders as symlinks; a malicious or compromised catalog attempting a symlink-based install attack; nested catalogs where one skill links to another.
Related errors
- skill '{$skill}' in {$url} resolves outside the cloned catal
- Refusing to copy symlinked skill source path: {}
- Refusing to copy symlink within skill source: {}
- skill catalog {$url} has a symlinked skills/ directory; refu
- skill catalog {$url} has a skills/ directory that resolves o
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3ce9ac2888c89999.
Report an issue: GitHub.