can1357/oh-my-pi · error · ToolError
skill:// path resolves outside the plugin root: ${url}
Error message
skill:// path resolves outside the plugin root: ${url} What it means
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.
Source
Thrown at packages/coding-agent/src/tools/bash-skill-urls.ts:114
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new ToolError(message);
}
const 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 ToolError("Path traversal is not allowed in skill:// URLs");
}
// Agent Plugin skills (§4.1): the resource must canonically resolve within
// the plugin root. Fail closed: a dangling or unresolvable path is rejected
// rather than handed to bash, where writing through it could create the
// outside target. Symlinks may target other files inside the same package.
if (skill.containRoot) {
const contained = resolveContainedPathSync(skill.containRoot, resolvedPath);
if (contained.status === "outside") {
throw new ToolError(`skill:// path resolves outside the plugin root: ${url}`);
}
if (contained.status === "missing") {
throw new ToolError(`skill:// path does not exist: ${url}`);
}
return contained.realPath;
}
return resolvedPath;
}
/**
* Match a raw skill segment against registered skills using longest-prefix match.
* Handles colons in both skill names (namespacing) and suffixes (line ranges).
*
* For "superpowers:brainstorming:1-5" with skill "superpowers:brainstorming":
* -> skill = superpowers:brainstorming, suffix = "1-5"
* For "brainstorming" with skill "brainstorming":
* -> skill = brainstorming, suffix = undefinedView on GitHub (pinned to 9690622007)
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.
Example fix
// before ln -s ~/notes.txt $PLUGIN/skills/a/notes.txt // after cp ~/notes.txt $PLUGIN/skills/a/notes.txt # symlink → real file inside root
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from "node:fs";
const target = resolveSkillUrlToPathUnchecked(url, skills); // your own pre-check
const real = fs.realpathSync.native(target);
if (!real.startsWith(fs.realpathSync(skill.containRoot))) {
throw new Error(`symlink target outside plugin root: ${real}`);
} Try / catch
try {
return resolveSkillUrlToPath(url, skills);
} catch (e) {
if (e instanceof ToolError && e.message.includes("outside the plugin root")) {
// flag the plugin package as mis-packaged; do not retry the URL
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ${scheme}:// path escapes its root: ${rawPath}
- 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
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/4a2932a0322cd671.
Report an issue: GitHub.