{"record":{"id":"6a37f83ed7adec68","repo":"zeroclaw-labs/zeroclaw","slug":"skill-skill-in-url-resolves-outside-the-cl","errorCode":null,"errorMessage":"skill '{$skill}' in {$url} resolves outside the cloned catalog; refusing to install","messagePattern":"skill '(.+?)' in (.+?) resolves outside the cloned catalog; refusing to install","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/skills/mod.rs","lineNumber":2479,"sourceCode":"                        skill_dir.display()\n                    )\n                });\n            }\n        };\n        if entry_meta.file_type().is_symlink() {\n            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(\n                \"cli-skills-install-catalog-skill-symlink\",\n                &[(\"skill\", skill_name), (\"url\", url)]\n            ));\n        }\n        let selected = skill_dir.canonicalize().with_context(|| {\n            format!(\n                \"failed to canonicalize selected skill {}\",\n                skill_dir.display()\n            )\n        })?;\n        if !selected.starts_with(&skills_root) {\n            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(\n                \"cli-skills-install-catalog-skill-escapes\",\n                &[(\"skill\", skill_name), (\"url\", url)]\n            ));\n        }\n        if !selected.is_dir() {\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(\n                    \"cli-skills-install-skill-not-in-catalog-empty\",\n                    &[(\"skill\", skill_name), (\"url\", url)]\n                ));\n            }\n            anyhow::bail!(crate::i18n::get_required_cli_string_with_args(\n                \"cli-skills-install-skill-not-in-catalog\",\n                &[\n                    (\"skill\", skill_name),\n                    (\"url\", url),\n                    (\"available\", &available.join(\", \")),","sourceCodeStart":2461,"sourceCodeEnd":2497,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/skills/mod.rs#L2461-L2497","documentation":"Thrown by the catalog skill installer after canonicalizing the selected skills/<skill> path. It verifies the canonical path still starts with the catalog's skills root; if a skill name containing `..` or separator tricks resolves outside the clone, the install is refused. This is a classic path-traversal guard applied to the skill-name argument.","triggerScenarios":"Passing a skill name with traversal or separators to the catalog install API, e.g. `my-skill/../../elsewhere`, `../../etc/passwd`, or any name whose canonicalized dir escapes the cloned catalog's skills/ root. The check is `!selected.starts_with(&skills_root)` after `skill_dir.canonicalize()`.","commonSituations":"Scripts that build skill names from user input or file paths; typos that paste a path instead of a bare skill name; adversarial input fuzzing the CLI; copy-pasted names with trailing `/..`.","solutions":["Pass a plain skill name that matches one directory directly under the catalog repo's skills/ (no slashes, no dots).","Run the install with a valid name and read the 'Available skills' list in the resulting error to pick the correct name.","If you maintain the catalog, ensure every advertised skill is a directory directly under skills/."],"exampleFix":"# before\nzeroclaw skills install \"../shared/my-skill\" --catalog https://github.com/org/catalog\n\n# after\nzeroclaw skills install my-skill --catalog https://github.com/org/catalog","handlingStrategy":"validation","validationCode":"fn is_bare_skill_name(name: &str) -> bool {\n    !name.is_empty()\n        && !name.contains('/')\n        && !name.contains('\\\\')\n        && name != \".\"\n        && name != \"..\"\n        && !name.contains(\"..\")\n}","typeGuard":null,"tryCatchPattern":"match install_catalog_skill(url, skill) {\n    Err(e) if e.to_string().contains(\"resolves outside the cloned catalog\") => {\n        eprintln!(\"rejected skill name {skill:?}: use a plain directory name under skills/\");\n    }\n    rest => rest?,\n}","preventionTips":["Treat skill names as identifiers, not paths: validate against ^[A-Za-z0-9._-]+$ at the CLI boundary.","Never build skill names by concatenating user-supplied path fragments.","Fail loudly on names containing separators before any clone or install happens."],"tags":["skills","catalog","install","path-traversal","security","input-validation"],"backgroundTag":"path-traversal","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}