zeroclaw-labs/zeroclaw · error · anyhow::Error
Refusing to copy symlinked skill source path: {}
Error message
Refusing to copy symlinked skill source path: {} What it means
copy_dir_recursive_secure stats the source with symlink_metadata (which does not follow links) and refuses to copy when the top-level source path itself is a symlink. The installer only copies a real directory the user actually named, never whatever a link resolves to.
Source
Thrown at crates/zeroclaw-runtime/src/skills/mod.rs:2027
}
anyhow::bail!("Skill security audit failed: {}", report.summary());
}
fn remove_git_metadata(skill_path: &Path) -> Result<()> {
let git_dir = skill_path.join(".git");
if git_dir.exists() {
std::fs::remove_dir_all(&git_dir)
.with_context(|| format!("failed to remove {}", git_dir.display().to_string()))?;
}
Ok(())
}
fn copy_dir_recursive_secure(src: &Path, dest: &Path) -> Result<()> {
let src_meta = std::fs::symlink_metadata(src)
.with_context(|| format!("failed to read metadata for {}", src.display().to_string()))?;
if src_meta.file_type().is_symlink() {
anyhow::bail!(
"Refusing to copy symlinked skill source path: {}",
src.display()
);
}
if !src_meta.is_dir() {
anyhow::bail!(
"Skill source must be a directory: {}",
src.display().to_string()
);
}
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)? {View on GitHub (pinned to 88bb9c8533)
Solutions
- Resolve the link and pass the real directory: run 'realpath <path>' and install that path
- In code, std::fs::canonicalize the path before calling install_local_skill_source
- Restructure so the skill directory is a real directory in (or copied into) the workspace
Example fix
# before zeroclaw skills install ~/links/my-skill # ~/links/my-skill is a symlink # after zeroclaw skills install "$(realpath ~/links/my-skill)"
Defensive patterns
Strategy: validation
Validate before calling
// Resolve links before installing:
let real = std::fs::canonicalize(&source)
.with_context(|| format!("resolve skill source {source}"))?;
install_local_skill_source(real.to_str().unwrap(), &skills_path, allow_scripts)?; Type guard
fn is_real_directory(p: &std::path::Path) -> bool {
std::fs::symlink_metadata(p)
.map(|m| !m.file_type().is_symlink() && m.is_dir())
.unwrap_or(false)
} Prevention
- Run realpath on user-supplied skill paths before install
- Prefer real directories over symlink farms for skill management
- Remember symlink_metadata semantics: the check is on the link itself, not its target
When it happens
Trigger: skills install <path> where path is a symlink to the actual skill directory — common with dotfiles-style management, stow, or a monorepo checkout where the skills folder is linked into the workspace.
Common situations: Skills managed via symlink farms; workspaces assembled from linked checkouts; shortcuts created for convenience pointing at the real skill repo.
Related errors
- Refusing to copy symlink within skill source: {}
- 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/30fe7f08cc95dcba.
Report an issue: GitHub.