can1357/oh-my-pi · error
skill:// path resolves outside the plugin root: ${url.href}
Error message
skill:// path resolves outside the plugin root: ${url.href} What it means
For Agent Plugin skills (spec §4.1) that declare a containRoot, resolve() additionally verifies the requested path canonically resolves (via resolveContainedPath, which follows symlinks) inside the plugin's contain root. If the real path lands outside that root, resolve() fails closed with this error that includes the offending URL. Symlinks may only target files within the same plugin package.
Source
Thrown at packages/coding-agent/src/internal-urls/skill-protocol.ts:86
const hasRelativePath = urlPath && urlPath !== "/" && urlPath !== "";
if (hasRelativePath) {
const relativePath = decodeURIComponent(urlPath.slice(1));
validateRelativePath(relativePath);
targetPath = path.join(skill.baseDir, relativePath);
const resolvedPath = path.resolve(targetPath);
const resolvedBaseDir = path.resolve(skill.baseDir);
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
throw new Error("Path traversal is not allowed");
}
// Agent Plugin skills (§4.1): the resource must canonically resolve
// within the plugin root; a dangling or unresolvable path fails closed.
// Symlinks may target other files inside the same package.
if (skill.containRoot) {
const contained = await resolveContainedPath(skill.containRoot, resolvedPath);
if (contained.status === "outside") {
throw new Error(`skill:// path resolves outside the plugin root: ${url.href}`);
}
if (contained.status === "missing") {
throw new Error(`File not found: ${resolvedPath}`);
}
targetPath = contained.realPath;
}
} else {
targetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;
}
let stats: fsTypes.Stats;
try {
stats = await fs.stat(targetPath);
} catch (error) {
if (isEnoent(error)) {
throw new Error(`File not found: ${targetPath}`);
}
throw error;View on GitHub (pinned to 9690622007)
Solutions
- Restructure the plugin so all skill-referenced files (including symlink targets) live inside the plugin contain root
- Replace out-of-package symlinks with copies of the files inside the plugin package
- If the resource belongs to another plugin/skill, reference it via that skill's own skill:// URL
- Check the plugin layout on disk (fs.realpath on the target) to confirm where the canonical path lands
Example fix
// before // plugin/skills/my-skill/shared.md -> ../../../shared/docs.md (outside plugin root) // after cp ../../../shared/docs.md plugin/skills/my-skill/shared.md # physical copy inside contain root
Defensive patterns
Strategy: validation
Validate before calling
import { resolveContainedPath } from '../discovery/contained-path';
const target = path.resolve(skill.baseDir, rel);
const contained = await resolveContainedPath(skill.containRoot, target);
if (contained.status !== 'inside') {
throw new Error(`target resolves outside plugin root (${contained.status})`);
} Try / catch
try {
return await handler.resolve(url, ctx);
} catch (err) {
if (err instanceof Error && err.message.includes('resolves outside the plugin root')) {
// flag the plugin for repackaging; skip resolving this path
}
throw err;
} Prevention
- Package plugin assets physically inside the plugin root — no external symlinks
- Run resolveContainedPath checks in CI for every file a skill references
- After moving/reinstalling a plugin, re-verify that symlink targets still resolve inside the contain root
When it happens
Trigger: Resolving skill://<name>/<path> on a plugin skill where the path (or a symlink along it) resolves via fs.realpath to a location outside skill.containRoot — e.g. a symlink to a shared directory elsewhere on disk, or a path that escapes the plugin package after canonicalization.
Common situations: Plugin authors symlinking shared assets from outside the package (e.g. ../../shared/theme.md) into their skill; linking to node_modules outside the contain root; a moved or re-rooted plugin install where symlinks now point outside; build artifacts generated outside the plugin root and linked in.
Related errors
- The managed-skills root is a symlink; refusing to operate ou
- Managed skill "${name}" SKILL.md is a symlink; refusing to o
- Managed skill "${name}" resolves through a symlink; refusing
- Managed skill "${safe}" is a symlink; refusing to delete out
- ${scheme}:// path escapes its root: ${rawPath}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/69f852d3c3e51ea9.
Report an issue: GitHub.