can1357/oh-my-pi · error
Path traversal is not allowed
Error message
Path traversal is not allowed
What it means
This is a second, post-join defense in resolve(): even after validateRelativePath() passes, the joined path is resolved with path.resolve() and checked to still start with the skill's resolved baseDir. It catches edge cases the segment check misses (e.g. symlinks within baseDir pointing out, unusual normalization). If the resolved absolute path escapes skill.baseDir, resolve() refuses with this generic traversal error.
Source
Thrown at packages/coding-agent/src/internal-urls/skill-protocol.ts:78
if (!skill) {
const available = skills.map(s => s.name);
const availableStr = available.length > 0 ? available.join(", ") : "none";
throw new Error(`Unknown skill: ${skillName}\nAvailable: ${availableStr}`);
}
let targetPath: string;
const urlPath = url.pathname;
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;
}
View on GitHub (pinned to 9690622007)
Solutions
- Reference real files inside the skill directory, not symlinks pointing outside it
- Inspect where skill.baseDir and the target actually resolve (path.resolve, fs.realpath) to find the escape
- If you legitimately need external content, expose it as its own skill or resource rather than symlinking into a skill
- Move the external file into the skill's directory
Example fix
// before
// skills/my-skill/data -> symlink to /etc ; resolve('skill://my-skill/data/passwd')
// after
// copy the needed files physically into the skill directory and resolve them there Defensive patterns
Strategy: validation
Validate before calling
const resolved = path.resolve(path.join(skill.baseDir, rel));
const base = path.resolve(skill.baseDir);
if (!resolved.startsWith(base + path.sep) && resolved !== base) {
throw new Error(`skill path escapes baseDir: ${resolved}`);
} Try / catch
try {
return await handler.resolve(url, ctx);
} catch (err) {
if (err instanceof Error && err.message === 'Path traversal is not allowed') {
// audit the skill directory for symlinks pointing outside
}
throw err;
} Prevention
- Keep skill directories free of symlinks to external targets
- Pre-verify with path.resolve + startsWith before constructing URLs
- Prefer physical copies over symlinks inside skill packages
When it happens
Trigger: Resolving skill://<name>/<path> where path.join(skill.baseDir, relativePath), after path.resolve(), lands outside path.resolve(skill.baseDir) — e.g. via a symlinked directory inside the skill whose target is external (when the skill has no containRoot), or platform-specific normalization quirks.
Common situations: A symlink inside the skill directory pointing to an external file; baseDir itself being a symlink whose resolution interacts with the relative path; paths crafted with separators that evade the '..' segment check but still escape after resolution (e.g. on Windows with mixed separators).
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- ${scheme}:// path escapes its root: ${rawPath}
- {scheme}:// path escapes its root: {path}
- #{scheme}:// path escapes its root: #{path}
- local:// URL escapes local root
- Archive symlink escapes extraction dir: ${link.path} -> ${li
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/ad5467e43b4778ac.
Report an issue: GitHub.