{"record":{"id":"10fa15d5366bb808","repo":"upstash/context7","slug":"skill-name-skillname-escapes-the-skills-root","errorCode":null,"errorMessage":"Skill name \"${skillName}\" escapes the skills root","messagePattern":"Skill name \"(.+?)\" escapes the skills root","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/utils/skill-name.ts","lineNumber":21,"sourceCode":"const SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;\n\nexport function isSafeSkillName(name: string): boolean {\n  if (typeof name !== \"string\") return false;\n  if (name.length === 0 || name.length > 128) return false;\n  if (name === \".\" || name === \"..\") return false;\n  if (name.includes(\"\\0\")) return false;\n  if (!SAFE_NAME.test(name)) return false;\n  return true;\n}\n\nexport function assertSkillNameInRoot(skillsRoot: string, skillName: string): string {\n  if (!isSafeSkillName(skillName)) {\n    throw new Error(`Unsafe skill name: ${JSON.stringify(skillName)}`);\n  }\n  const root = resolve(skillsRoot);\n  const target = resolve(root, skillName);\n  if (dirname(target) !== root || basename(target) !== skillName) {\n    throw new Error(`Skill name \"${skillName}\" escapes the skills root`);\n  }\n  return target;\n}\n","sourceCodeStart":3,"sourceCodeEnd":25,"githubUrl":"https://github.com/upstash/context7/blob/ca15df0443ee770506fc4eb270d1efc71d483933/packages/cli/src/utils/skill-name.ts#L3-L25","documentation":"A second, defense-in-depth guard in assertSkillNameInRoot(), run only after isSafeSkillName() already passed. It resolves(root, skillName) and requires dirname(target)===root AND basename(target)===skillName. Because the SAFE_NAME regex already forbids '/' and '\\\\', under normal conditions this branch is effectively unreachable; firing it signals a platform-specific path resolution anomaly (e.g. drive letters, normalized UNC components) that slipped past the regex.","triggerScenarios":"A skill name that the OS path resolver normalizes differently than the lexical regex — e.g. a Windows drive-qualified or UNC-ish component, a reserved name, or a normalization quirk where dirname/basename no longer round-trip to the original name.","commonSituations":"Cross-platform bug where a name passes on POSIX but resolves oddly on Windows; corrupted/templated name input; essentially never seen in practice — treat as a canary for unexpected path-normalization behavior.","solutions":["Log the exact skillName, root, resolved target, and platform when this fires — the regex should have caught it.","Simplify the name to plain ASCII alphanumerics and re-test.","If reproducible, file a bug: either SAFE_NAME is too permissive for that platform or resolve() is doing something unexpected.","Avoid reserved filenames and any path-like characters entirely."],"exampleFix":"// before — name that round-trips abnormally on the host OS\n// (no clean user fix; this guard is defense-in-depth)\n\n// after — use a plain name that the regex already permits and that resolves cleanly\nassertSkillNameInRoot(root, \"my-skill\");","handlingStrategy":"validation","validationCode":"// Round-trip check that mirrors the internal guard, so you can fail fast with context.\nimport { resolve, dirname, basename } from \"node:path\";\nimport { isSafeSkillName } from \"../utils/skill-name\";\nfunction safeResolve(skillsRoot: string, name: string): string {\n  if (!isSafeSkillName(name)) throw new Error(`Unsafe name: ${name}`);\n  const root = resolve(skillsRoot);\n  const target = resolve(root, name);\n  if (dirname(target) !== root || basename(target) !== name) {\n    throw new Error(`Name \"${name}\" resolves unexpectedly on ${process.platform}; use a plainer name.`);\n  }\n  return target;\n}","typeGuard":"function resolvesInsideRoot(skillsRoot: string, name: string): boolean {\n  if (!isSafeSkillName(name)) return false;\n  const root = resolve(skillsRoot);\n  const target = resolve(root, name);\n  return dirname(target) === root && basename(target) === name;\n}","tryCatchPattern":"try {\n  assertSkillNameInRoot(skillsRoot, candidate);\n} catch (e) {\n  const msg = (e as Error).message;\n  if (msg.includes(\"escapes the skills root\")) {\n    // Should be near-unreachable; log platform + inputs and use a plain name.\n    console.error({ platform: process.platform, skillsRoot, candidate });\n    return fallbackToPlainName();\n  }\n  throw e;\n}","preventionTips":["Use plain ASCII alphanumeric names plus only ._- to make this guard a non-event.","If it ever fires, capture platform + root + name — it indicates path-normalization drift worth a bug report.","Avoid reserved filenames and path-like characters entirely."],"tags":["validation","security","skill-install","defense-in-depth"],"backgroundTag":null,"analyzedSha":"ca15df0443ee770506fc4eb270d1efc71d483933","analyzedAt":"2026-08-12T13:31:48.440Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}