can1357/oh-my-pi · error · ToolError

Path traversal is not allowed in skill:// URLs

Error message

Path traversal is not allowed in skill:// URLs

What it means

Even with a well-formed relative path, the resolved absolute target must stay inside the skill's base directory. The function resolves both paths and rejects any result that escapes the base (the classic ".." symlink-or-path traversal defense), throwing this ToolError on escape.

Source

Thrown at packages/coding-agent/src/tools/bash-skill-urls.ts:105

	let relativePath: string;
	try {
		relativePath = decodeURIComponent(rawPath.slice(1));
	} catch {
		throw new ToolError(`Invalid skill:// URL path encoding: ${url}`);
	}
	try {
		validateRelativePath(relativePath);
	} 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;
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove ".." traversal from the URL path; target only files inside the skill directory.
  2. If you need a file outside the skill, use the appropriate tool (read tool / plain path) with the proper permissions.
  3. Check that the skill's baseDir is where you expect; the URL is interpreted relative to it.

Example fix

// before
resolveSkillUrlToPath("skill://my-skill/../../secrets.env", skills);

// after
resolveSkillUrlToPath("skill://my-skill/config.env", skills);
Defensive patterns

Strategy: validation

Validate before calling

import * as path from "node:path";
const rel = decodeURIComponent(new URL(url).pathname.slice(1));
const resolved = path.resolve(skill.baseDir, rel);
if (!resolved.startsWith(path.resolve(skill.baseDir) + path.sep)) {
  throw new Error(`refusing: ${url} escapes skill directory`);
}

Try / catch

try {
  return resolveSkillUrlToPath(url, skills);
} catch (e) {
  if (e instanceof ToolError && e.message.includes("Path traversal")) {
    // do not retry; treat as a security rejection and report to the caller
  } else throw e;
}

Prevention

When it happens

Trigger: Any skill:// URL whose final resolved path (after path.join/resolve) lands outside skill.baseDir — typically "skill://name/../../outside.txt" or a baseDir-relative escape via symlinks not covered by containRoot.

Common situations: Prompt-injection or a wandering agent tries to read files outside a skill package; a legitimately relocated skill directory makes previously valid relative paths resolve oddly; hardcoded traversal in generated commands.

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/5cc2e847209c7703. Report an issue: GitHub.