jackwener/OpenCLI · error · ArgumentError
Invalid skill path: ${raw}
Error message
Invalid skill path: ${raw} What it means
normalizeSkillPath rejects absolute paths (leading '/') and any path containing a '..' segment after backslash-normalization, throwing ArgumentError with a hint to use a path relative to an OpenCLI skill directory. This is the first line of defense against path traversal in skill reads.
Source
Thrown at src/skills.ts:107
return { name: normalizedTarget, pathInSkill: relpath };
}
const slash = normalizedTarget.indexOf('/');
if (slash === -1) {
return { name: normalizedTarget, pathInSkill: '' };
}
return {
name: normalizedTarget.slice(0, slash),
pathInSkill: normalizedTarget.slice(slash + 1),
};
}
function normalizeSkillPath(raw: string): string {
const normalized = raw.trim().replace(/\\/g, '/');
if (!normalized || normalized.includes('\0')) {
throw new ArgumentError('Skill path must be non-empty.');
}
if (normalized.startsWith('/') || normalized.split('/').some((part) => part === '..')) {
throw new ArgumentError(`Invalid skill path: ${raw}`, 'Use a path relative to an OpenCLI skill directory.');
}
return path.posix.normalize(normalized);
}
function parseFrontmatter(content: string): SkillFrontmatter {
if (!content.startsWith('---\n')) return {};
const end = content.indexOf('\n---', 4);
if (end < 0) return {};
try {
const parsed = yaml.load(content.slice(4, end));
return parsed && typeof parsed === 'object' ? parsed as SkillFrontmatter : {};
} catch {
return parseLooseFrontmatter(content.slice(4, end));
}
}
function parseLooseFrontmatter(raw: string): SkillFrontmatter {
const out: Record<string, string> = {};View on GitHub (pinned to 49907e53dc)
Solutions
- Rewrite the path as relative to the skill root with no '..' segments (e.g. 'references/x.md')
- If the target is in another skill, call the command again with that other skill name instead of traversing
- Sanitize user input before calling: strip leading '/' and reject/resolve '..' segments
- Use path.posix.normalize locally to preview what the library will compute
Example fix
// before
await readOpenCliSkill('grok', '/home/me/notes.md');
// after
fs.copyFileSync('/home/me/notes.md', path.join(skillRoot, 'references/notes.md'));
await readOpenCliSkill('grok', 'references/notes.md'); Defensive patterns
Strategy: validation
Validate before calling
const norm = p.replace(/\\/g, '/').trim();
if (norm.startsWith('/') || norm.split('/').includes('..')) throw new Error(`skill path must be relative without '..': ${p}`); Type guard
function isRelativeInSkillPath(p: string): boolean {
const norm = p.trim().replace(/\\/g, '/');
return norm.length > 0 && !norm.startsWith('/') && !norm.split('/').includes('..');
} Try / catch
try {
const skill = await readOpenCliSkill(name, p);
} catch (e) {
if (e instanceof ArgumentError && /Invalid skill path/i.test(e.message)) console.error(`Use a path relative to the skill directory: ${e.hint ?? ''}`);
else throw e;
} Prevention
- Sanitize user-supplied paths before passing them in
- To reach files outside a skill, copy them in or target the other skill explicitly
- Preview resolution with path.posix.normalize during development
When it happens
Trigger: Passing '/etc/passwd', 'C:/foo' after backslash conversion, '../other-skill/SKILL.md', or 'a/../../b' as the in-skill path. Any segment exactly equal to '..' triggers the throw, even if it would coincidentally resolve inside the skill.
Common situations: Copying absolute paths from elsewhere in the codebase; building paths with path.join(base, userInput) where userInput contains '..'; reusing code that resolved paths on Windows; trying to read another skill's files by traversing upward.
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: ${relativePath}
- 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/fad951521cf44cee.
Report an issue: GitHub.