jackwener/OpenCLI · error · ArgumentError

Invalid skill path: ${relativePath}

Error message

Invalid skill path: ${relativePath}

What it means

readOpenCliSkill in src/skills.ts resolves a user-supplied path inside an OpenCLI skill directory. After normalizing, it computes the path relative to skillRoot and rejects anything that escapes the skill root (starts with '..' or is absolute). It throws ArgumentError (code ARGUMENT, exit code 2, usage error) to prevent path-traversal reads outside the skill directory.

Source

Thrown at src/skills.ts:60

}

export function readOpenCliSkill(target: string, relpath = '', packageRoot?: string): OpenCliSkillReadResult {
  const { name, pathInSkill } = parseSkillTarget(target, relpath);
  if (!name.startsWith('opencli-')) {
    throw new ArgumentError(`Unknown OpenCLI skill: ${name}`, 'Run "opencli skills list" to see available OpenCLI skills.');
  }

  const skillsRoot = getSkillsRoot(packageRoot);
  const skillRoot = path.join(skillsRoot, name);
  if (!isDirectory(skillRoot) || !fs.existsSync(path.join(skillRoot, 'SKILL.md'))) {
    throw new ArgumentError(`Unknown OpenCLI skill: ${name}`, 'Run "opencli skills list" to see available OpenCLI skills.');
  }

  const relativePath = normalizeSkillPath(pathInSkill || 'SKILL.md');
  const absolutePath = path.resolve(skillRoot, relativePath);
  const relativeToRoot = path.relative(skillRoot, absolutePath);
  if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
    throw new ArgumentError(`Invalid skill path: ${relativePath}`, 'Skill paths must stay inside the selected OpenCLI skill.');
  }
  if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
    throw new ArgumentError(`Skill file not found: ${name}/${relativePath}`, 'Run "opencli skills list <skill>" is not supported yet; read SKILL.md or a known references/... file.');
  }

  return {
    skill: name,
    path: relativePath,
    content: fs.readFileSync(absolutePath, 'utf8'),
  };
}

function readSkillInfo(skillsRoot: string, name: string): OpenCliSkillInfo | null {
  const skillMdPath = path.join(skillsRoot, name, 'SKILL.md');
  if (!fs.existsSync(skillMdPath)) return null;
  const content = fs.readFileSync(skillMdPath, 'utf8');
  const fm = parseFrontmatter(content);
  return {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Use a path relative to the skill directory only (e.g. 'SKILL.md' or 'references/foo.md'), with no '..' segments and no leading '/'
  2. If you need a file in a different skill, re-run the command targeting that skill instead of traversing with '..'
  3. Check the resolved path: run path.resolve(skillRoot, yourPath) locally and confirm it stays under skillRoot
  4. Inspect the skill's actual layout (SKILL.md and references/) to find the correct in-skill path

Example fix

// before
await readOpenCliSkill('my-skill', '../shared/notes.md');
// after
await readOpenCliSkill('my-skill', 'references/notes.md');
Defensive patterns

Strategy: validation

Validate before calling

const p = path.posix.normalize(userPath.replace(/\\/g, '/').trim());
if (!p || p.split('/').includes('..') || p.startsWith('/')) throw new Error('path must be relative and stay inside the skill');

Try / catch

try {
  const skill = await readOpenCliSkill(name, p);
} catch (e) {
  if (e instanceof ArgumentError && e.code === 'ARGUMENT') console.error(`Bad skill path '${p}': ${e.hint ?? e.message}`);
  else throw e;
}

Prevention

When it happens

Trigger: Calling the skills read command with a path containing '..' segments that resolve outside the skill root (e.g. '../../../etc/passwd' if not caught earlier by normalizeSkillPath), or any path that resolves to an absolute location outside skillRoot. Raised from readOpenCliSkill when relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot).

Common situations: Typos like '../SKILL.md' when intending a sibling file; scripting that joins a wrong base dir; attempting to read files outside the skill (credentials, other skills) which the library intentionally blocks as a path-traversal guard; Windows backslash paths that normalize into traversal.

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 jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/e22f6bedea774790. Report an issue: GitHub.