can1357/oh-my-pi · error · ToolError

Invalid skill:// URL: ${url}

Error message

Invalid skill:// URL: ${url}

What it means

resolveSkillUrlToPath parses skill:// URLs with a strict regex (^skill://name/path[?query][#fragment]). If the string does not match that shape at all, the function throws this ToolError instead of guessing. This keeps malformed internal URLs from reaching the filesystem layer.

Source

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

export interface InternalUrlExpansionOptions {
	skills: readonly Skill[];
	attachments?: readonly ImageAttachmentEntry[];
	noEscape?: boolean;
	internalRouter?: InternalUrlResolver;
	localOptions?: LocalProtocolOptions;
	cwd?: string;
	sessionFile?: string;
	ensureLocalParentDirs?: boolean;
}

/**
 * Resolve a single skill:// URL to its absolute filesystem path.
 * Does NOT read file content or verify existence.
 */
export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): string {
	const parsed = /^skill:\/\/([^/?#]+)(\/[^?#]*)?(?:[?#].*)?$/.exec(url);
	if (!parsed) {
		throw new ToolError(`Invalid skill:// URL: ${url}`);
	}

	let rawSkillSegment = parsed[1];
	if (!rawSkillSegment) {
		throw new ToolError(`skill:// URL requires a skill name: ${url}`);
	}
	// Decode percent-encoded colons (%3A) used for namespaced skill names
	try {
		rawSkillSegment = decodeURIComponent(rawSkillSegment);
	} catch {
		// Leave as-is if decoding fails
	}

	// Resolve skill name by longest-prefix match against registered skills.
	// This handles namespaced skills ("plugin:skill") where the URI may also
	// carry a colon-delimited suffix (e.g., ":1-5" line range).
	const { skill, suffix } = matchSkillName(rawSkillSegment, skills);
	if (!skill) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Format the URL exactly as skill://<skill-name>[/<path>][?#fragment].
  2. Check the string starts with "skill://" before calling; route other schemes to their own resolvers.
  3. Encode the skill name and path segments (encodeURIComponent) so no regex-breaking characters leak in.

Example fix

// before
resolveSkillUrlToPath("skills/my-skill/SKILL.md", skills);

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

Strategy: validation

Validate before calling

if (!/^skill:\/\/[^/?#]+(\/[^?#]*)?([?#].*)?$/.test(url)) {
  throw new Error(`not a well-formed skill:// URL: ${url}`);
}

Type guard

function isSkillUrl(u: string): boolean {
  return u.startsWith("skill://") && /^skill:\/\/[^/?#]+/.test(u);
}

Try / catch

try {
  return resolveSkillUrlToPath(url, skills);
} catch (e) {
  if (e instanceof ToolError && e.message.startsWith("Invalid skill:// URL")) {
    // fall back to another resolver or report the malformed URL to the caller
  } else throw e;
}

Prevention

When it happens

Trigger: Calling resolveSkillUrlToPath (or resolveInternalUrlToPath / resolvedPath / resolved) with a URL lacking the skill:// scheme, e.g. "skill:/name", "https://skill/name", "skill://", or a URL with unescaped whitespace/newlines that break the regex.

Common situations: A model hallucinates a different URL scheme in a bash command; config stores a hand-typed skill URL with a typo; a caller passes a plain file path where a skill URL was expected.

Related errors


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