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
- Use a path relative to the skill directory only (e.g. 'SKILL.md' or 'references/foo.md'), with no '..' segments and no leading '/'
- If you need a file in a different skill, re-run the command targeting that skill instead of traversing with '..'
- Check the resolved path: run path.resolve(skillRoot, yourPath) locally and confirm it stays under skillRoot
- 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
- Always pass paths relative to the skill root with forward slashes
- Reject '..' and absolute paths in your own input handling before calling
- Never feed raw user/URL input directly as the skill path
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
- Invalid skill path: ${raw}
- Expected a trusted HTTPS bilibili.com video URL without cred
- ${label} must be an https LinkedIn URL without credentials o
- output path must not be a symbolic link: ${resolved}
- xiaohongshu/delete-note: note URL must be an exact https://*
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/e22f6bedea774790.
Report an issue: GitHub.