zeroclaw-labs/zeroclaw · error · anyhow::Error
Skill path escapes skills directory: {name}
Error message
Skill path escapes skills directory: {name} What it means
When removing a skill that lives in the global skills dir, the code canonicalizes both the skill directory and the global root and requires the skill path to remain under the root before remove_dir_all runs. If the skill path resolves outside the canonical root — typically via a symlink — the command refuses to delete, a containment guard against destroying arbitrary directories. A missing/unresolvable global root also triggers this, because the root falls back to its uncanonicalized path while the child canonicalizes successfully, so the starts_with check fails.
Source
Thrown at src/skills/mod.rs:444
service
.remove_skill(&target, zeroclaw_runtime::skills::RemoveMode::Archive)
.map_err(anyhow::Error::msg)?;
println!(
"{}",
get_required_cli_string_with_args(
"cli-skills-removed-archived",
&[("status", &status), ("name", &name), ("bundle", alias)],
)
);
} else {
// Global dir: plain delete with a containment guard.
let global_root = skills_dir(&config.data_dir);
let canonical_root =
global_root.canonicalize().unwrap_or(global_root.clone());
if let Ok(c) = dir.canonicalize()
&& !c.starts_with(&canonical_root)
{
anyhow::bail!("Skill path escapes skills directory: {name}");
}
std::fs::remove_dir_all(dir)?;
println!(
"{}",
get_required_cli_string_with_args(
"cli-skills-removed-global",
&[("status", &status), ("name", &name)],
)
);
}
}
many => {
let locs = many
.iter()
.map(|(l, _)| l.clone())
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(View on GitHub (pinned to 88bb9c8533)
Solutions
- Inspect the entry: `ls -la <data-dir>/skills/<name>` and look for a '->' symlink target
- Replace the symlink with the real directory (or delete it outright) and re-run the remove
- Verify the global skills root exists as a real directory (skills_dir of your configured data_dir)
- If data_dir was relocated, fix it in config so root and children canonicalize consistently
Example fix
# before: <data-dir>/skills/foo is a symlink → elsewhere zeroclaw skills remove foo # refuses: escapes skills directory # after: make it a real directory (or rm the link), then remove rm <data-dir>/skills/foo git clone https://github.com/owner/foo <data-dir>/skills/foo zeroclaw skills remove foo
Defensive patterns
Strategy: validation
Validate before calling
// Mirror the containment guard before delegating the removal
let root = skills_dir(&config.data_dir).canonicalize()?;
let dir = root.join(&name);
let c = dir.canonicalize().map_err(|_| "skill dir missing")?;
if !c.starts_with(&root) {
eprintln!("{name} resolves outside the skills root (symlink?) — refusing");
} Prevention
- Never symlink individual skill directories into the global skills dir; clone or copy instead
- Keep the configured data_dir stable so the skills root always exists and canonicalizes
- Audit `ls -la` of the skills dir for '->' entries before scripting bulk removes
When it happens
Trigger: The skill directory under skills/ is a symlink to elsewhere (dotfiles managers, manual ln -s); the global skills root does not exist or is itself a dangling symlink; filesystems where canonicalization resolves differently for parent and child.
Common situations: Managing the skills dir with stow or symlinks; data_dir moved after install leaving a stale root; cross-device mounts under the skills path.
Related errors
- attachment path {} canonicalizes to {} which escapes workspa
- Key file path is a symlink — refusing to write
- Refusing to copy symlinked skill source path: {}
- Refusing to copy symlink within skill source: {}
- skill catalog {$url} has a symlinked skills/ directory; refu
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3404de8e296c88e0.
Report an issue: GitHub.