{"record":{"id":"2e5983563d0a2574","repo":"can1357/oh-my-pi","slug":"invalid-skill-url-url","errorCode":null,"errorMessage":"Invalid skill:// URL: ${url}","messagePattern":"Invalid skill:// URL: (.+?)","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/bash-skill-urls.ts","lineNumber":56,"sourceCode":"export interface InternalUrlExpansionOptions {\n\tskills: readonly Skill[];\n\tattachments?: readonly ImageAttachmentEntry[];\n\tnoEscape?: boolean;\n\tinternalRouter?: InternalUrlResolver;\n\tlocalOptions?: LocalProtocolOptions;\n\tcwd?: string;\n\tsessionFile?: string;\n\tensureLocalParentDirs?: boolean;\n}\n\n/**\n * Resolve a single skill:// URL to its absolute filesystem path.\n * Does NOT read file content or verify existence.\n */\nexport function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): string {\n\tconst parsed = /^skill:\\/\\/([^/?#]+)(\\/[^?#]*)?(?:[?#].*)?$/.exec(url);\n\tif (!parsed) {\n\t\tthrow new ToolError(`Invalid skill:// URL: ${url}`);\n\t}\n\n\tlet rawSkillSegment = parsed[1];\n\tif (!rawSkillSegment) {\n\t\tthrow new ToolError(`skill:// URL requires a skill name: ${url}`);\n\t}\n\t// Decode percent-encoded colons (%3A) used for namespaced skill names\n\ttry {\n\t\trawSkillSegment = decodeURIComponent(rawSkillSegment);\n\t} catch {\n\t\t// Leave as-is if decoding fails\n\t}\n\n\t// Resolve skill name by longest-prefix match against registered skills.\n\t// This handles namespaced skills (\"plugin:skill\") where the URI may also\n\t// carry a colon-delimited suffix (e.g., \":1-5\" line range).\n\tconst { skill, suffix } = matchSkillName(rawSkillSegment, skills);\n\tif (!skill) {","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/bash-skill-urls.ts#L38-L74","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Format the URL exactly as skill://<skill-name>[/<path>][?#fragment].","Check the string starts with \"skill://\" before calling; route other schemes to their own resolvers.","Encode the skill name and path segments (encodeURIComponent) so no regex-breaking characters leak in."],"exampleFix":"// before\nresolveSkillUrlToPath(\"skills/my-skill/SKILL.md\", skills);\n\n// after\nresolveSkillUrlToPath(\"skill://my-skill/SKILL.md\", skills);","handlingStrategy":"validation","validationCode":"if (!/^skill:\\/\\/[^/?#]+(\\/[^?#]*)?([?#].*)?$/.test(url)) {\n  throw new Error(`not a well-formed skill:// URL: ${url}`);\n}","typeGuard":"function isSkillUrl(u: string): boolean {\n  return u.startsWith(\"skill://\") && /^skill:\\/\\/[^/?#]+/.test(u);\n}","tryCatchPattern":"try {\n  return resolveSkillUrlToPath(url, skills);\n} catch (e) {\n  if (e instanceof ToolError && e.message.startsWith(\"Invalid skill:// URL\")) {\n    // fall back to another resolver or report the malformed URL to the caller\n  } else throw e;\n}","preventionTips":["Build URLs with a helper instead of string concatenation.","Route by scheme prefix before calling resolvers (skill:// vs file:// vs https://).","Percent-encode name and path segments when constructing the URL."],"tags":["url-parsing","validation","skills"],"backgroundTag":"malformed-url","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}