{"record":{"id":"6fed1a21c906c712","repo":"Hmbown/CodeWhale","slug":"skill-path-escapes-skills-directory","errorCode":null,"errorMessage":"skill path {} escapes skills directory {}","messagePattern":"skill path (.+?) escapes skills directory (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/install.rs","lineNumber":1564,"sourceCode":"        bail!(\"skill name must be a single path-safe segment (got '{name}')\");\n    }\n    if name == \".\" || name == \"..\" || name.contains('/') || name.contains('\\\\') {\n        bail!(\"skill name must be a single path-safe segment (got '{name}')\");\n    }\n    let mut components = Path::new(name).components();\n    if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {\n        bail!(\"skill name must be a single path-safe segment (got '{name}')\");\n    }\n    Ok(name)\n}\n\nfn ensure_target_within_skills_dir(target: &Path, skills_dir: &Path) -> Result<()> {\n    let skills_dir = fs::canonicalize(skills_dir)\n        .with_context(|| format!(\"failed to resolve {}\", skills_dir.display()))?;\n    let target = fs::canonicalize(target)\n        .with_context(|| format!(\"failed to resolve {}\", target.display()))?;\n    if !target.starts_with(&skills_dir) {\n        bail!(\n            \"skill path {} escapes skills directory {}\",\n            target.display(),\n            skills_dir.display()\n        );\n    }\n    Ok(())\n}\n\n/// Strip a leading directory prefix (e.g. `repo-main/`) from a tarball path.\nfn strip_prefix<'a>(path: &'a str, prefix: &str) -> std::borrow::Cow<'a, str> {\n    if prefix.is_empty() {\n        return std::borrow::Cow::Borrowed(path);\n    }\n    let with_slash = format!(\"{prefix}/\");\n    if let Some(rest) = path.strip_prefix(&with_slash) {\n        std::borrow::Cow::Owned(rest.to_string())\n    } else if path == prefix {\n        std::borrow::Cow::Borrowed(\"\")","sourceCodeStart":1546,"sourceCodeEnd":1582,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/skills/install.rs#L1546-L1582","documentation":"Guard in ensure_target_within_skills_dir fired because the computed install target for the skill resolves to a path outside the configured skills directory. The skill name (or archive layout) produced a target containing traversal or an absolute component, so installing it would write outside CodeWhale-owned storage; this is a path-traversal rejection, not a filesystem failure.","triggerScenarios":"Thrown at crates/tui/src/skills/install.rs:1564 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Reject the skill source; its declared name or layout produces an unsafe target path","Ask the skill author to rename the skill to a single path-safe segment","If installing from an archive, verify the top-level directory name contains no '..' or separators","Install to the correct skills directory so the resolved target stays inside it"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}