upstash/context7 · critical

Skill name " " escapes the skills root

Error message

Skill name "${skillName}" escapes the skills root

What it means

assertSkillNameInRoot validates a skill name before resolving it under the skills root directory. After checking the name against isSafeSkillName, it resolves the joined path and verifies the resolved directory is still the skills root and the basename equals the original name. If a crafted name (e.g. containing '..' or separators) resolves outside the root, it throws to block path traversal.

Solutions

  1. Sanitize the skill name before calling: strip path separators and '..' segments, or reject names not matching /^[A-Za-z0-9._-]+$/
  2. Ensure the name is a plain single path segment (no '/' or '\\', not '..')
  3. If the intent was a nested path, pass only the final directory name and manage nesting inside the library
  4. Log/inspect the rejected name to find where unsanitized input enters the CLI

Example fix

// before
removeCommand(`../${userInput}`);
// after
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(userInput)) throw new Error('invalid skill name');
removeCommand(userInput);
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
function isValidSkillName(name) { return typeof name === 'string' && SAFE_SKILL_NAME.test(name) && name !== '..' && name !== '.'; }

Type guard

function isSafeSkillName(name: unknown): name is string {
  return typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
}

Try / catch

try {
  const dir = skillDir(userInput);
} catch (e) {
  if (e.message.includes('escapes the skills root') || e.message.startsWith('Unsafe skill name')) {
    throw new UserFacingError(`Invalid skill name: ${userInput}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling skillDir/targetPath/removeCommand with a skill name like '../other', 'a/b', an absolute path, or any string containing path separators or '..' segments, so that resolve(root, skillName) lands outside the skills root.

Common situations: User-supplied skill names passed to CLI remove/skill commands without sanitization; automation scripts interpolating paths into skill names; names copied from URLs or file paths; locale/encoding oddities introducing separator characters.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of upstash/context7@4416fb855b (2026-09-16). Data as JSON: /api/errors/10fa15d5366bb808. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/utils/skill-name.ts:21

const SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;

export function isSafeSkillName(name: string): boolean {
  if (typeof name !== "string") return false;
  if (name.length === 0 || name.length > 128) return false;
  if (name === "." || name === "..") return false;
  if (name.includes("\0")) return false;
  if (!SAFE_NAME.test(name)) return false;
  return true;
}

export function assertSkillNameInRoot(skillsRoot: string, skillName: string): string {
  if (!isSafeSkillName(skillName)) {
    throw new Error(`Unsafe skill name: ${JSON.stringify(skillName)}`);
  }
  const root = resolve(skillsRoot);
  const target = resolve(root, skillName);
  if (dirname(target) !== root || basename(target) !== skillName) {
    throw new Error(`Skill name "${skillName}" escapes the skills root`);
  }
  return target;
}

View on GitHub (pinned to 4416fb855b)