vercel-labs/skills · critical

Invalid subpath: "${subpath}" resolves outside the repositor

Error message

Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.

What it means

discoverSkills() validates any provided subpath with isSubpathSafe(basePath, subpath) before searching; if the subpath resolves outside basePath (contains '..' segments that escape), it throws this traversal error. This guards skill discovery inside cloned repos against path injection via the source spec's subpath component.

Source

Thrown at src/skills.ts:189

  const normalizedTarget = normalize(resolve(join(basePath, subpath)));

  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
}

export async function discoverSkills(
  basePath: string,
  subpath?: string,
  options?: DiscoverSkillsOptions
): Promise<Skill[]> {
  const skills: Skill[] = [];
  const seenNames = new Set<string>();
  const parsedSkillPaths = new Set<string>();
  const localLock = await readLocalLock(basePath);
  const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));

  // Validate subpath doesn't escape basePath (prevent path traversal)
  if (subpath && !isSubpathSafe(basePath, subpath)) {
    throw new Error(
      `Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`
    );
  }

  const searchPath = subpath ? join(basePath, subpath) : basePath;

  // Get plugin groupings to map skills to their parent plugin
  // We search for plugin definitions from the base search path
  const pluginGroupings = await getPluginGroupings(searchPath);

  // Helper to assign plugin name if available
  const enhanceSkill = (skill: Skill) => {
    const resolvedPath = resolve(skill.path);
    if (pluginGroupings.has(resolvedPath)) {
      skill.pluginName = pluginGroupings.get(resolvedPath);
    }
    return skill;
  };

View on GitHub (pinned to 435076e789)

Solutions

  1. Strip '..' segments from the subpath before calling discoverSkills (or sanitizeSubpath from source-parser)
  2. Validate user input: allow only [A-Za-z0-9._/-] and reject any '..' component
  3. Pass an absolute directory instead of a traversal-prone relative subpath
  4. Fail fast in your CLI/SDK on suspicious path input rather than forwarding it

Example fix

// before
discoverSkills(repoDir, userInput); // userInput = '../../secrets'
// after
if (userInput.split(/[\\/]/).includes('..')) throw new Error('bad subpath');
discoverSkills(repoDir, userInput);
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative, isAbsolute } from 'node:path';
function isSubpathSafe(base: string, sub: string): boolean {
  if (sub.includes('..')) return false;
  const target = resolve(base, sub);
  const rel = relative(resolve(base), target);
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
}
if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error('Unsafe subpath rejected');

Type guard

function isSubpathTraversal(e: unknown): e is Error {
  return e instanceof Error && /Invalid subpath.*outside the repository directory/.test(e.message);
}

Try / catch

try { await discoverSkills(basePath, subpath); }
catch (e) {
  if (isSubpathTraversal(e)) throw new Error(`Rejected unsafe subpath: ${JSON.stringify(subpath)}`);
  throw e;
}

Prevention

When it happens

Trigger: Calling discoverSkills(basePath, subpath) (ultimately from 'skills add owner/repo/sub..') with a subpath like '../../', 'a/../../..', or one that normalizes outside the repo root.

Common situations: User-supplied subpaths from CLI args or SDK strings like 'skills/../../etc'; URL parsing quirks that keep '..' segments; programmatic callers joining unvalidated user input into the subpath parameter.

Related errors


AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28). Data as JSON: /api/errors/9a32308ce164789d. Report an issue: GitHub.