{"record":{"id":"4a2932a0322cd671","repo":"can1357/oh-my-pi","slug":"skill-path-resolves-outside-the-plugin-root-4a2932","errorCode":null,"errorMessage":"skill:// path resolves outside the plugin root: ${url}","messagePattern":"skill:// path resolves outside the plugin root: (.+?)","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/bash-skill-urls.ts","lineNumber":114,"sourceCode":"\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tthrow new ToolError(message);\n\t}\n\n\tconst targetPath = path.join(skill.baseDir, relativePath);\n\tconst resolvedPath = path.resolve(targetPath);\n\tconst resolvedBaseDir = path.resolve(skill.baseDir);\n\tif (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {\n\t\tthrow new ToolError(\"Path traversal is not allowed in skill:// URLs\");\n\t}\n\t// Agent Plugin skills (§4.1): the resource must canonically resolve within\n\t// the plugin root. Fail closed: a dangling or unresolvable path is rejected\n\t// rather than handed to bash, where writing through it could create the\n\t// outside target. Symlinks may target other files inside the same package.\n\tif (skill.containRoot) {\n\t\tconst contained = resolveContainedPathSync(skill.containRoot, resolvedPath);\n\t\tif (contained.status === \"outside\") {\n\t\t\tthrow new ToolError(`skill:// path resolves outside the plugin root: ${url}`);\n\t\t}\n\t\tif (contained.status === \"missing\") {\n\t\t\tthrow new ToolError(`skill:// path does not exist: ${url}`);\n\t\t}\n\t\treturn contained.realPath;\n\t}\n\n\treturn resolvedPath;\n}\n\n/**\n * Match a raw skill segment against registered skills using longest-prefix match.\n * Handles colons in both skill names (namespacing) and suffixes (line ranges).\n *\n * For \"superpowers:brainstorming:1-5\" with skill \"superpowers:brainstorming\":\n *   -> skill = superpowers:brainstorming, suffix = \"1-5\"\n * For \"brainstorming\" with skill \"brainstorming\":\n *   -> skill = brainstorming, suffix = undefined","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/bash-skill-urls.ts#L96-L132","documentation":"For skills with a containRoot (e.g. agent-plugin packages), the resolved path must canonically resolve inside the plugin root, checked via resolveContainedPathSync (which follows symlinks/realpaths). If realpath resolution lands outside the root, this ToolError is thrown — fail-closed so a write through bash cannot create a target outside the package. Symlinks to other files inside the same package remain allowed.","triggerScenarios":"A skill:// URL for a containRoot skill whose target — after symlink resolution — points outside the plugin root, e.g. a symlink inside the skill pointing at /etc/hosts, or a baseDir placed outside containRoot.","commonSituations":"A plugin package contains a symlink to a user home file; a dev setup links the skill directory to an external folder; a mis-packaged plugin where baseDir and containRoot diverge.","solutions":["Retarget the symlink so it points at a file within the plugin root.","Replace the out-of-root symlink with a copy of the target inside the package.","Fix the plugin packaging so baseDir lies within containRoot.","If access to the external file is genuinely needed, use a non-skill mechanism with explicit permission."],"exampleFix":"// before\nln -s ~/notes.txt $PLUGIN/skills/a/notes.txt\n// after\ncp ~/notes.txt $PLUGIN/skills/a/notes.txt  # symlink → real file inside root","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs\";\nconst target = resolveSkillUrlToPathUnchecked(url, skills); // your own pre-check\nconst real = fs.realpathSync.native(target);\nif (!real.startsWith(fs.realpathSync(skill.containRoot))) {\n  throw new Error(`symlink target outside plugin root: ${real}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return resolveSkillUrlToPath(url, skills);\n} catch (e) {\n  if (e instanceof ToolError && e.message.includes(\"outside the plugin root\")) {\n    // flag the plugin package as mis-packaged; do not retry the URL\n  } else throw e;\n}","preventionTips":["Audit plugin packages for symlinks pointing outside the package before publishing.","Copy external files into the package instead of symlinking them.","Keep skill.baseDir inside containRoot when authoring plugins."],"tags":["security","symlink","sandbox","skills"],"backgroundTag":"symlink-escape-blocked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}