can1357/oh-my-pi · error · Error

Path "${original}" uses internal scheme "${prefix}" and must

Error message

Path "${original}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.

What it means

resolveToCwd refuses to treat strings that begin with an internal scheme prefix (e.g. `session://`, `issue://`, `local://`) as ordinary filesystem paths. Internal URLs must go through their dedicated protocol handlers; bypassing them would resolve a bogus relative directory under the cwd.

Source

Thrown at packages/coding-agent/src/tools/path-utils.ts:490

 * because `write` addresses a whole file, not a partial range, and silently
 * stripping it would write to a path the caller never named. Non-URL paths and
 * URLs without a selector pass through unchanged.
 */
export function peelWriteUrlSelector(rawPath: string): string {
	const { path, sel } = splitInternalUrlSel(rawPath);
	if (sel === undefined) return rawPath;
	// Case-insensitive to match read's selector grammar (parseSel + the /i regexes above).
	if (/^(?:raw|conflicts)$/i.test(sel)) return path;
	throw new ToolError(
		`write does not accept the trailing selector ":${sel}" — it writes a whole file. ` +
			`Remove ":${sel}", or if the filename truly ends with it, percent-encode the ":" as %3A.`,
	);
}

function assertNotInternalUrl(expanded: string, original: string): void {
	for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
		if (expanded.startsWith(prefix)) {
			throw new Error(
				`Path "${original}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.`,
			);
		}
	}
}

export function normalizeLocalScheme(filePath: string): string {
	return filePath.replace(/^(local:)\/(?!\/)/, "$1//");
}

export function isInternalUrlPath(filePath: string): boolean {
	const normalized = normalizeLocalScheme(filePath);
	const expandedAndNormalized = normalizeLocalScheme(expandPath(normalized));
	for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
		if (expandedAndNormalized.startsWith(prefix)) return true;
	}
	return false;
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Route the value through the tool/API that supports internal URLs (the one that uses InternalUrlRouter), not the filesystem path API.
  2. If you actually mean a local path, strip the internal scheme prefix and pass the real filesystem path.
  3. Check the origin of the value (config, session state) — it likely comes from an internal-URL context and is being fed to the wrong tool.

Example fix

// before
resolveToCwd("session://abc123/transcript.jsonl", cwd)
// after — use the protocol-aware resolver
internalRouter.resolve("session://abc123/transcript.jsonl", { cwd })
Defensive patterns

Strategy: validation

Validate before calling

const INTERNAL_SCHEMES = ["session://", "issue://", "local://", "history://", "agent://", "skill://", "pr://"];
if (INTERNAL_SCHEMES.some(s => p.startsWith(s))) {
  throw new Error(`Route ${p} through the protocol-aware API, not the filesystem path API`);
}

Type guard

const isInternalUrl = (p) => /^[a-z][a-z0-9+.-]*:\/\//i.test(p) && /^(session|issue|local|history|agent|skill|pr):\/\//i.test(p);

Try / catch

try { return resolveToCwd(p, cwd); } catch (e) {
  if (String(e.message).includes('internal scheme')) return internalRouter.resolve(p, { cwd });
  throw e;
}

Prevention

When it happens

Trigger: Passing a path starting with one of TOP_LEVEL_INTERNAL_URL_PREFIXES into a tool or helper that routes through resolveToCwd/assertNotInternalUrl instead of the internal-URL router — e.g. using `session://...` as a plain file path in grep/search/write.

Common situations: Agent or script mixes internal URL schemes with filesystem paths; a stored config value retains a scheme prefix after the protocol feature was removed or renamed; hand-building paths from parsed internal URLs.

Related errors


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