can1357/oh-my-pi · error · ToolError

Invalid URL encoding in ${scheme}:// path: ${rawPath}

Error message

Invalid URL encoding in ${scheme}:// path: ${rawPath}

What it means

When resolving an internal-URL path (e.g. `local://a%20b.md`) under its registered root, `resolveUnderRoot` decodes percent-escapes via decodeURIComponent. Malformed escapes (a bare `%` or truncated `%zz` sequence) make decoding throw, and the resolver surfaces this ToolError instead of a raw URIError.

Source

Thrown at packages/coding-agent/src/eval/js/shared/helpers.ts:128

function resolveHelperPath(ctx: HelperContext, rawPath: string, op: "read" | "write"): string {
	const match = INTERNAL_URL_RE.exec(rawPath);
	if (!match) return resolvePath(ctx, rawPath);
	const scheme = match[1].toLowerCase();
	const root = ctx.localRoots()[scheme];
	if (!root) {
		throw new ToolError(`Protocol paths are not supported by ${op}(): ${rawPath}`);
	}
	return resolveUnderRoot(scheme, root, match[2], rawPath);
}

/** Resolve an internal-URL relative path under its root, mirroring the host
 *  local-protocol handler: decode, reject absolute/traversal, confine to root. */
function resolveUnderRoot(scheme: string, root: string, rawRelative: string, rawPath: string): string {
	let relative: string;
	try {
		relative = decodeURIComponent(rawRelative.replaceAll("\\", "/"));
	} catch {
		throw new ToolError(`Invalid URL encoding in ${scheme}:// path: ${rawPath}`);
	}
	const rootPath = path.resolve(root);
	if (relative === "") return rootPath;
	if (path.isAbsolute(relative)) {
		throw new ToolError(`Absolute paths are not allowed in ${scheme}:// URLs: ${rawPath}`);
	}
	const normalized = path.normalize(relative);
	if (normalized.startsWith("..") || normalized.includes("/../") || normalized.includes("/..")) {
		throw new ToolError(`Path traversal (..) is not allowed in ${scheme}:// URLs: ${rawPath}`);
	}
	const resolved = path.resolve(rootPath, normalized);
	if (resolved !== rootPath && !resolved.startsWith(`${rootPath}${path.sep}`)) {
		throw new ToolError(`${scheme}:// path escapes its root: ${rawPath}`);
	}
	return resolved;
}

async function resolveRegularFile(

View on GitHub (pinned to 9690622007)

Solutions

  1. Percent-encode the path segment: `write(`local://${encodeURIComponent(name)}`, data)`.
  2. Remove or escape stray `%` characters in the path.
  3. If building URLs from file names, encodeURIComponent each segment rather than the whole string with unsafe characters.

Example fix

// before
await write(`local://${name}.md`, data); // name = "100% done"
// after
await write(`local://${encodeURIComponent(name)}.md`, data);
Defensive patterns

Strategy: validation

Validate before calling

function safeSegment(name: string): string {
	const enc = encodeURIComponent(name);
	decodeURIComponent(enc); // throws on impossible cases; encodeURIComponent output is always valid
	return enc;
}

Try / catch

try {
	return await read(url);
} catch (err) {
	if (String(err?.message).includes("Invalid URL encoding")) {
		const [scheme, rest] = url.split("://");
		return await read(`${scheme}://${encodeURIComponent(decodeURI(rest))}`);
	}
	throw err;
}

Prevention

When it happens

Trigger: Calling read()/write() with an internal-URL path containing invalid percent-encoding, such as `local://100%.md` or `local://a%2.md`.

Common situations: Interpolating raw user/filename content into a scheme:// URL without encodeURIComponent; copy-pasting URLs that were truncated; template strings containing literal `%` characters.

Related errors


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