zeroclaw-labs/zeroclaw · error · anyhow::Error
Refusing to copy symlink within skill source: {}
Error message
Refusing to copy symlink within skill source: {} What it means
During the recursive secure copy, an entry inside the source tree is a symlink; the copy aborts and install_local_skill_source removes the partially created destination. The post-install audit would reject the symlink anyway — the secure copy refuses earlier so nothing untrustworthy lands on disk.
Source
Thrown at crates/zeroclaw-runtime/src/skills/mod.rs:2057
std::fs::create_dir_all(dest).with_context(|| {
format!(
"failed to create destination {}",
dest.display().to_string()
)
})?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
let metadata = std::fs::symlink_metadata(&src_path).with_context(|| {
format!(
"failed to read metadata for {}",
src_path.display().to_string()
)
})?;
if metadata.file_type().is_symlink() {
anyhow::bail!(
"Refusing to copy symlink within skill source: {}",
src_path.display()
);
}
if metadata.is_dir() {
copy_dir_recursive_secure(&src_path, &dest_path)?;
} else if metadata.is_file() {
std::fs::copy(&src_path, &dest_path).with_context(|| {
format!(
"failed to copy skill file from {} to {}",
src_path.display().to_string(),
dest_path.display()
)
})?;
}
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Find the links: find <skill-dir> -type l
- Replace each symlink with a real copy (cp -L) and retry the install
- Restructure the skill to be fully self-contained instead of linking out
Example fix
# before: skill contains a symlink find my-skill -type l # my-skill/assets -> ../../shared/assets # after: materialize real copies rm my-skill/assets && cp -L ../../shared/assets my-skill/assets zeroclaw skills install ./my-skill
Defensive patterns
Strategy: validation
Validate before calling
fn contains_symlink(dir: &std::path::Path) -> std::io::Result<bool> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let meta = std::fs::symlink_metadata(entry.path())?;
if meta.file_type().is_symlink() {
return Ok(true);
}
if meta.is_dir() && contains_symlink(&entry.path())? {
return Ok(true);
}
}
Ok(false)
}
if contains_symlink(std::path::Path::new(source))? {
anyhow::bail!("skill source contains symlinks; materialize them first");
} Prevention
- Run 'find <skill-dir> -type l' before installing
- Materialize shared files with cp -L instead of linking
- Keep skill folders self-contained — no links out to repo-shared assets
When it happens
Trigger: A skill source tree containing any symlinked file or subdirectory, e.g. 'ln -s ../../shared/assets assets' inside the skill folder.
Common situations: Monorepo skills sharing files via symlinks; setup scripts using ln -s on macOS/Linux; npm-style linked dependencies vendored into a skill.
Related errors
- Refusing to copy symlinked skill source path: {}
- skill '{$skill}' in {$url} is a symlink; catalog skills must
- Skill security audit failed: {}
- skill catalog {$url} has a symlinked skills/ directory; refu
- skill '{$skill}' in {$url} resolves outside the cloned catal
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/4cd0acc6bf6c6136.
Report an issue: GitHub.