{"record":{"id":"8449692e734ecf32","repo":"can1357/oh-my-pi","slug":"skill-url-requires-a-skill-name-skill-name","errorCode":null,"errorMessage":"skill:// URL requires a skill name: skill://<name>","messagePattern":"skill:// URL requires a skill name: skill://<name>","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/skill-protocol.ts","lineNumber":56,"sourceCode":"\t\tnormalized.includes(\"/..\")\n\t) {\n\t\tthrow new Error(\"Path traversal (..) is not allowed in skill:// URLs\");\n\t}\n}\n\n/**\n * Handler for skill:// URLs.\n */\nexport class SkillProtocolHandler implements ProtocolHandler {\n\treadonly scheme = \"skill\";\n\treadonly immutable = true;\n\n\tasync resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {\n\t\tconst skills = context?.skills ?? getActiveSkills();\n\n\t\tconst skillName = url.rawHost || url.hostname;\n\t\tif (!skillName) {\n\t\t\tthrow new Error(\"skill:// URL requires a skill name: skill://<name>\");\n\t\t}\n\n\t\tconst skill = skills.find(s => s.name === skillName);\n\t\tif (!skill) {\n\t\t\tconst available = skills.map(s => s.name);\n\t\t\tconst availableStr = available.length > 0 ? available.join(\", \") : \"none\";\n\t\t\tthrow new Error(`Unknown skill: ${skillName}\\nAvailable: ${availableStr}`);\n\t\t}\n\n\t\tlet targetPath: string;\n\t\tconst urlPath = url.pathname;\n\t\tconst hasRelativePath = urlPath && urlPath !== \"/\" && urlPath !== \"\";\n\n\t\tif (hasRelativePath) {\n\t\t\tconst relativePath = decodeURIComponent(urlPath.slice(1));\n\t\t\tvalidateRelativePath(relativePath);\n\t\t\ttargetPath = path.join(skill.baseDir, relativePath);\n","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/skill-protocol.ts#L38-L74","documentation":"The skill:// protocol resolves a URL's host component to a skill name. If the URL has neither a rawHost nor a hostname (e.g. just 'skill://' or 'skill:///path'), there is no skill to look up, so resolve() throws immediately with the expected URL form in the message. The scheme requires the shape skill://<name> or skill://<name>/<path>.","triggerScenarios":"resolve() called with an InternalUrl whose rawHost and hostname are both empty: 'skill://', 'skill:///folder/file.md', or a URL where parsing dropped the host (e.g. 'skill:/name' malformed input).","commonSituations":"Building the URL from a template with an undefined/empty skill variable; stripping the host during string manipulation; hand-written URLs omitting the skill name and jumping straight to a path.","solutions":["Include the skill name as the URL host: skill://my-skill or skill://my-skill/sub/path","Check the variable holding the skill name for undefined/empty before constructing the URL","List available skills (e.g. via complete() or getActiveSkills()) and pick a valid name"],"exampleFix":"// before\nconst url = `skill://${skillName ?? ''}/notes.md`;\n// after\nif (!skillName) throw new Error('skillName is required');\nconst url = `skill://${skillName}/notes.md`;","handlingStrategy":"validation","validationCode":"const m = /^skill:\\/\\/([^/]+)/.exec(url);\nif (!m || !m[1]) {\n  throw new Error(`skill URL needs a host skill name: ${url}`);\n}\nconst skillName = m[1];","typeGuard":"function hasSkillHost(url: { rawHost?: string; hostname: string }): boolean {\n  return Boolean(url.rawHost || url.hostname);\n}","tryCatchPattern":"try {\n  return await handler.resolve(url, ctx);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('requires a skill name')) {\n    // reconstruct the URL with a valid skill host\n  }\n  throw err;\n}","preventionTips":["Always template skill:// URLs as skill://${name} or skill://${name}/${path}","Assert the name variable is non-empty before interpolation","Never build skill:// URLs from naive string splitting that can drop the host"],"tags":["url","validation","skills"],"backgroundTag":"missing-url-component","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}