{"record":{"id":"e22f6bedea774790","repo":"jackwener/OpenCLI","slug":"invalid-skill-path-relativepath","errorCode":null,"errorMessage":"Invalid skill path: ${relativePath}","messagePattern":"Invalid skill path: (.+?)","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"src/skills.ts","lineNumber":60,"sourceCode":"}\n\nexport function readOpenCliSkill(target: string, relpath = '', packageRoot?: string): OpenCliSkillReadResult {\n  const { name, pathInSkill } = parseSkillTarget(target, relpath);\n  if (!name.startsWith('opencli-')) {\n    throw new ArgumentError(`Unknown OpenCLI skill: ${name}`, 'Run \"opencli skills list\" to see available OpenCLI skills.');\n  }\n\n  const skillsRoot = getSkillsRoot(packageRoot);\n  const skillRoot = path.join(skillsRoot, name);\n  if (!isDirectory(skillRoot) || !fs.existsSync(path.join(skillRoot, 'SKILL.md'))) {\n    throw new ArgumentError(`Unknown OpenCLI skill: ${name}`, 'Run \"opencli skills list\" to see available OpenCLI skills.');\n  }\n\n  const relativePath = normalizeSkillPath(pathInSkill || 'SKILL.md');\n  const absolutePath = path.resolve(skillRoot, relativePath);\n  const relativeToRoot = path.relative(skillRoot, absolutePath);\n  if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {\n    throw new ArgumentError(`Invalid skill path: ${relativePath}`, 'Skill paths must stay inside the selected OpenCLI skill.');\n  }\n  if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {\n    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.');\n  }\n\n  return {\n    skill: name,\n    path: relativePath,\n    content: fs.readFileSync(absolutePath, 'utf8'),\n  };\n}\n\nfunction readSkillInfo(skillsRoot: string, name: string): OpenCliSkillInfo | null {\n  const skillMdPath = path.join(skillsRoot, name, 'SKILL.md');\n  if (!fs.existsSync(skillMdPath)) return null;\n  const content = fs.readFileSync(skillMdPath, 'utf8');\n  const fm = parseFrontmatter(content);\n  return {","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/skills.ts#L42-L78","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nawait readOpenCliSkill('my-skill', '../shared/notes.md');\n// after\nawait readOpenCliSkill('my-skill', 'references/notes.md');","handlingStrategy":"validation","validationCode":"const p = path.posix.normalize(userPath.replace(/\\\\/g, '/').trim());\nif (!p || p.split('/').includes('..') || p.startsWith('/')) throw new Error('path must be relative and stay inside the skill');","typeGuard":null,"tryCatchPattern":"try {\n  const skill = await readOpenCliSkill(name, p);\n} catch (e) {\n  if (e instanceof ArgumentError && e.code === 'ARGUMENT') console.error(`Bad skill path '${p}': ${e.hint ?? e.message}`);\n  else throw e;\n}","preventionTips":["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"],"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"}