{"record":{"id":"2bc7c0fb14bd21e7","repo":"zeroclaw-labs/zeroclaw","slug":"skill-catalog-url-has-a-symlinked-skills-direc","errorCode":null,"errorMessage":"skill catalog {$url} has a symlinked skills/ directory; refusing to inspect it","messagePattern":"skill catalog (.+?) has a symlinked skills/ directory; refusing to inspect it","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/skills/mod.rs","lineNumber":2413,"sourceCode":"        let skills_meta = match std::fs::symlink_metadata(&skills_dir) {\n            Ok(metadata) => metadata,\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {\n                anyhow::bail!(crate::i18n::get_required_cli_string_with_args(\n                    \"cli-skills-install-skill-not-in-catalog-empty\",\n                    &[(\"skill\", skill_name), (\"url\", url)]\n                ));\n            }\n            Err(err) => {\n                return Err(err).with_context(|| {\n                    format!(\n                        \"failed to read metadata for catalog skills root {}\",\n                        skills_dir.display()\n                    )\n                });\n            }\n        };\n        if skills_meta.file_type().is_symlink() {\n            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(\n                \"cli-skills-install-catalog-root-symlink\",\n                &[(\"url\", url)]\n            ));\n        }\n        let skills_root = skills_dir.canonicalize().with_context(|| {\n            format!(\n                \"failed to canonicalize catalog skills root {}\",\n                skills_dir.display()\n            )\n        })?;\n        if !skills_root.starts_with(&clone_root) {\n            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(\n                \"cli-skills-install-catalog-root-escapes\",\n                &[(\"url\", url)]\n            ));\n        }\n        if !skills_root.is_dir() {\n            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(","sourceCodeStart":2395,"sourceCodeEnd":2431,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/skills/mod.rs#L2395-L2431","documentation":"The catalog's skills/ entry is a symlink, and the installer refuses to inspect it: a catalog-controlled symlink could redirect skill lookup to an arbitrary directory on the host. This trust-boundary check runs before any skill name is resolved or available names are enumerated.","triggerScenarios":"A catalog repo that commits 'skills' as a symlink (Git supports committing symlinks) — either a deliberately tricky/malicious catalog or an exotic hand-built layout.","commonSituations":"Security testing catalogs; repos prepared on systems where symlinks were accidentally committed; adversarial-catalog research.","solutions":["Do not use that repository as a catalog; if unexpected, treat it as suspicious","Fork the catalog and replace the symlinked skills entry with a real directory, then install from the fork"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match install_git_catalog_skill_source(url, skill, &skills_path, false, &ws) {\n    Err(e) if e.to_string().contains(\"symlinked skills/ directory\") => {\n        // catalog is untrusted/broken: stop using this URL; do not attempt to\n        // bypass — the check protects the host filesystem\n    }\n    r => r,\n}","preventionTips":["Treat a symlinked skills/ catalog as hostile; report it upstream","Verify catalogs from third parties before distributing them to users","Never 'fix' this by pre-cloning and pointing the installer at the resolved target"],"tags":["skills","catalog","security","symlink"],"backgroundTag":"symlink-not-allowed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}