{"record":{"id":"fad951521cf44cee","repo":"jackwener/OpenCLI","slug":"invalid-skill-path-raw","errorCode":null,"errorMessage":"Invalid skill path: ${raw}","messagePattern":"Invalid skill path: (.+?)","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"src/skills.ts","lineNumber":107,"sourceCode":"    return { name: normalizedTarget, pathInSkill: relpath };\n  }\n  const slash = normalizedTarget.indexOf('/');\n  if (slash === -1) {\n    return { name: normalizedTarget, pathInSkill: '' };\n  }\n  return {\n    name: normalizedTarget.slice(0, slash),\n    pathInSkill: normalizedTarget.slice(slash + 1),\n  };\n}\n\nfunction normalizeSkillPath(raw: string): string {\n  const normalized = raw.trim().replace(/\\\\/g, '/');\n  if (!normalized || normalized.includes('\\0')) {\n    throw new ArgumentError('Skill path must be non-empty.');\n  }\n  if (normalized.startsWith('/') || normalized.split('/').some((part) => part === '..')) {\n    throw new ArgumentError(`Invalid skill path: ${raw}`, 'Use a path relative to an OpenCLI skill directory.');\n  }\n  return path.posix.normalize(normalized);\n}\n\nfunction parseFrontmatter(content: string): SkillFrontmatter {\n  if (!content.startsWith('---\\n')) return {};\n  const end = content.indexOf('\\n---', 4);\n  if (end < 0) return {};\n  try {\n    const parsed = yaml.load(content.slice(4, end));\n    return parsed && typeof parsed === 'object' ? parsed as SkillFrontmatter : {};\n  } catch {\n    return parseLooseFrontmatter(content.slice(4, end));\n  }\n}\n\nfunction parseLooseFrontmatter(raw: string): SkillFrontmatter {\n  const out: Record<string, string> = {};","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/skills.ts#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait readOpenCliSkill('grok', '/home/me/notes.md');\n// after\nfs.copyFileSync('/home/me/notes.md', path.join(skillRoot, 'references/notes.md'));\nawait readOpenCliSkill('grok', 'references/notes.md');","handlingStrategy":"validation","validationCode":"const norm = p.replace(/\\\\/g, '/').trim();\nif (norm.startsWith('/') || norm.split('/').includes('..')) throw new Error(`skill path must be relative without '..': ${p}`);","typeGuard":"function isRelativeInSkillPath(p: string): boolean {\n  const norm = p.trim().replace(/\\\\/g, '/');\n  return norm.length > 0 && !norm.startsWith('/') && !norm.split('/').includes('..');\n}","tryCatchPattern":"try {\n  const skill = await readOpenCliSkill(name, p);\n} catch (e) {\n  if (e instanceof ArgumentError && /Invalid skill path/i.test(e.message)) console.error(`Use a path relative to the skill directory: ${e.hint ?? ''}`);\n  else throw e;\n}","preventionTips":["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"],"tags":["path-traversal","argument-validation","security","skills"],"backgroundTag":"path-traversal-blocked","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}