{"record":{"id":"4d535b2facdc5372","repo":"can1357/oh-my-pi","slug":"absolute-paths-are-not-allowed-in-skill-urls","errorCode":null,"errorMessage":"Absolute paths are not allowed in skill:// URLs","messagePattern":"Absolute paths are not allowed in skill:// URLs","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/skill-protocol.ts","lineNumber":30,"sourceCode":"import * as path from \"node:path\";\nimport { isEnoent } from \"@oh-my-pi/pi-utils\";\nimport { resolveContainedPath } from \"../discovery/contained-path\";\nimport { getActiveSkills } from \"../extensibility/skills\";\nimport { isMarkdownPath } from \"../utils/lang-from-path\";\nimport { buildDirectoryResource } from \"./filesystem-resource\";\nimport type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from \"./types\";\n\nfunction getContentType(filePath: string): InternalResource[\"contentType\"] {\n\tif (isMarkdownPath(filePath)) return \"text/markdown\";\n\treturn \"text/plain\";\n}\n\n/**\n * Validate that a path is safe (no traversal, no absolute paths).\n */\nexport function validateRelativePath(relativePath: string): void {\n\tif (path.isAbsolute(relativePath)) {\n\t\tthrow new Error(\"Absolute paths are not allowed in skill:// URLs\");\n\t}\n\n\tconst normalized = path.normalize(relativePath);\n\tif (\n\t\trelativePath.split(/[\\\\/]/).includes(\"..\") ||\n\t\tnormalized.startsWith(\"..\") ||\n\t\tnormalized.includes(\"/../\") ||\n\t\tnormalized.includes(\"/..\")\n\t) {\n\t\tthrow new Error(\"Path traversal (..) is not allowed in skill:// URLs\");\n\t}\n}\n\n/**\n * Handler for skill:// URLs.\n */\nexport class SkillProtocolHandler implements ProtocolHandler {\n\treadonly scheme = \"skill\";","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/skill-protocol.ts#L12-L48","documentation":"validateRelativePath() guards skill:// URL path components against unsafe paths. Any path that Node's path.isAbsolute() accepts (e.g. '/etc/passwd', 'C:\\\\data') is rejected because skill:// paths must stay relative to the skill's base directory. This is a security boundary preventing absolute-path escapes when the path is later joined with skill.baseDir.","triggerScenarios":"resolve() on skill://<name>/... where the pathname (after decodeURIComponent) starts with '/', 'X:', or '\\\\\\\\server\\\\share'; also direct calls to validateRelativePath(), extractRelativePath(), splitMemoryGlobPattern(), resolveMemoryUrlToPath(), decodeVaultPath(), or validateQueryPath() with an absolute path.","commonSituations":"Concatenating a filesystem absolute path into a skill:// URL instead of a relative one; URL-encoding an absolute path that decodes to a leading slash; building URLs from user-supplied file paths without stripping the skill directory prefix; Windows drive-letter paths leaking into URLs.","solutions":["Pass a path relative to the skill's base directory, e.g. skill://my-skill/examples/foo.md instead of skill://my-skill//abs/path","Strip the skill.baseDir prefix from your absolute path (path.relative) before embedding it in the URL","If the file lives outside the skill directory, access it through file:// rather than skill://"],"exampleFix":"// before\nresolve(`skill://my-skill${path.resolve('/home/me/skills/my-skill/notes.md')}`)\n// after\nconst rel = path.relative(skill.baseDir, '/home/me/skills/my-skill/notes.md');\nresolve(`skill://my-skill/${rel}`)","handlingStrategy":"validation","validationCode":"import * as path from 'node:path';\nexport function toSkillUrl(skillName: string, baseDir: string, absolutePath: string): string {\n  const rel = path.relative(baseDir, absolutePath);\n  if (path.isAbsolute(rel) || rel.startsWith('..')) {\n    throw new Error(`${absolutePath} is not inside skill ${skillName}`);\n  }\n  return `skill://${skillName}/${rel}`;\n}","typeGuard":"function isSafeRelative(p: string): boolean {\n  return !path.isAbsolute(p) && !p.split(/[\\\\/]/).includes('..');\n}","tryCatchPattern":"try {\n  return await handler.resolve(url, ctx);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Absolute paths are not allowed')) {\n    // convert to a relative path or fall back to file://\n  }\n  throw err;\n}","preventionTips":["Never embed filesystem absolute paths in skill:// URLs","Always derive the path with path.relative(skillBaseDir, target)","Use file:// for files outside the skill directory"],"tags":["security","path-traversal","url"],"backgroundTag":"path-traversal-blocked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}