{"record":{"id":"3404de8e296c88e0","repo":"zeroclaw-labs/zeroclaw","slug":"skill-path-escapes-skills-directory-name","errorCode":null,"errorMessage":"Skill path escapes skills directory: {name}","messagePattern":"Skill path escapes skills directory: (.+?)","errorType":"console","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/skills/mod.rs","lineNumber":444,"sourceCode":"                        service\n                            .remove_skill(&target, zeroclaw_runtime::skills::RemoveMode::Archive)\n                            .map_err(anyhow::Error::msg)?;\n                        println!(\n                            \"{}\",\n                            get_required_cli_string_with_args(\n                                \"cli-skills-removed-archived\",\n                                &[(\"status\", &status), (\"name\", &name), (\"bundle\", alias)],\n                            )\n                        );\n                    } else {\n                        // Global dir: plain delete with a containment guard.\n                        let global_root = skills_dir(&config.data_dir);\n                        let canonical_root =\n                            global_root.canonicalize().unwrap_or(global_root.clone());\n                        if let Ok(c) = dir.canonicalize()\n                            && !c.starts_with(&canonical_root)\n                        {\n                            anyhow::bail!(\"Skill path escapes skills directory: {name}\");\n                        }\n                        std::fs::remove_dir_all(dir)?;\n                        println!(\n                            \"{}\",\n                            get_required_cli_string_with_args(\n                                \"cli-skills-removed-global\",\n                                &[(\"status\", &status), (\"name\", &name)],\n                            )\n                        );\n                    }\n                }\n                many => {\n                    let locs = many\n                        .iter()\n                        .map(|(l, _)| l.clone())\n                        .collect::<Vec<_>>()\n                        .join(\", \");\n                    anyhow::bail!(","sourceCodeStart":426,"sourceCodeEnd":462,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/src/skills/mod.rs#L426-L462","documentation":"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.","triggerScenarios":"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.","commonSituations":"Managing the skills dir with stow or symlinks; data_dir moved after install leaving a stale root; cross-device mounts under the skills path.","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"],"exampleFix":"# before: <data-dir>/skills/foo is a symlink → elsewhere\nzeroclaw skills remove foo            # refuses: escapes skills directory\n\n# after: make it a real directory (or rm the link), then remove\nrm <data-dir>/skills/foo\ngit clone https://github.com/owner/foo <data-dir>/skills/foo\nzeroclaw skills remove foo","handlingStrategy":"validation","validationCode":"// Mirror the containment guard before delegating the removal\nlet root = skills_dir(&config.data_dir).canonicalize()?;\nlet dir = root.join(&name);\nlet c = dir.canonicalize().map_err(|_| \"skill dir missing\")?;\nif !c.starts_with(&root) {\n    eprintln!(\"{name} resolves outside the skills root (symlink?) — refusing\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["cli","skills","security","symlink","filesystem","remove"],"backgroundTag":"symlink-escape","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}