{"record":{"id":"2e8c3e0cd72f364b","repo":"zeroclaw-labs/zeroclaw","slug":"skill-catalog-url-has-a-skills-directory-that","errorCode":null,"errorMessage":"skill catalog {$url} has a skills/ directory that resolves outside the cloned catalog; refusing to inspect it","messagePattern":"skill catalog (.+?) has a skills/ directory that resolves outside the cloned catalog; refusing to inspect it","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/skills/mod.rs","lineNumber":2425,"sourceCode":"                        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(\n                \"cli-skills-install-skill-not-in-catalog-empty\",\n                &[(\"skill\", skill_name), (\"url\", url)]\n            ));\n        }\n\n        let skill_dir = skills_root.join(skill_name);\n        let entry_meta = match std::fs::symlink_metadata(&skill_dir) {\n            Ok(metadata) => metadata,\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {\n                let available = list_contained_catalog_skill_names(&skills_root);\n                if available.is_empty() {\n                    anyhow::bail!(crate::i18n::get_required_cli_string_with_args(","sourceCodeStart":2407,"sourceCodeEnd":2443,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/skills/mod.rs#L2407-L2443","documentation":"Defense-in-depth after the symlink check: the canonicalized skills/ root must remain inside the canonicalized clone root (Path::starts_with on canonical paths). If canonicalization resolves outside — e.g. the entry was swapped for a symlink between the metadata check and canonicalize (TOCTOU), or exotic mount/aliasing behavior — the install aborts rather than inspect foreign directories.","triggerScenarios":"The skills entry replaced by an out-of-tree symlink between the two checks; unusual filesystem layouts where canonical paths diverge from the clone root.","commonSituations":"Rare in practice: races during install or adversarial catalogs. A plain retry almost always succeeds.","solutions":["Retry the install — transient races resolve on a second attempt","If persistent, clone the catalog manually and inspect skills/ with ls -la and realpath","Treat a persistent case as a hostile or broken catalog and stop using that URL"],"exampleFix":null,"handlingStrategy":"retry","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(\"resolves outside the cloned catalog\") => {\n        // likely a TOCTOU race: retry once; if it persists, audit the catalog\n        // (clone manually, ls -la skills, realpath skills) and drop the URL\n    }\n    r => r,\n}","preventionTips":["Retry once on this error — races resolve; persistent hits mean a hostile catalog","Pin catalog URLs to known-good commits or trusted hosts","Avoid installing catalogs while other processes modify the same workspace"],"tags":["skills","catalog","security","path-traversal"],"backgroundTag":"path-traversal-detected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}