{"record":{"id":"1db79111a2e1ed73","repo":"can1357/oh-my-pi","slug":"skill-url-must-resolve-to-a-file-or-directory","errorCode":null,"errorMessage":"skill:// URL must resolve to a file or directory: ${url.href}","messagePattern":"skill:// URL must resolve to a file or directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/skill-protocol.ts","lineNumber":111,"sourceCode":"\t\t} else {\n\t\t\ttargetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;\n\t\t}\n\n\t\tlet stats: fsTypes.Stats;\n\t\ttry {\n\t\t\tstats = await fs.stat(targetPath);\n\t\t} catch (error) {\n\t\t\tif (isEnoent(error)) {\n\t\t\t\tthrow new Error(`File not found: ${targetPath}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\tif (stats.isDirectory()) {\n\t\t\treturn buildDirectoryResource(url.href, targetPath);\n\t\t}\n\t\tif (!stats.isFile()) {\n\t\t\tthrow new Error(`skill:// URL must resolve to a file or directory: ${url.href}`);\n\t\t}\n\n\t\tconst content = await Bun.file(targetPath).text();\n\t\treturn {\n\t\t\turl: url.href,\n\t\t\tcontent,\n\t\t\tcontentType: getContentType(targetPath),\n\t\t\tsize: Buffer.byteLength(content, \"utf-8\"),\n\t\t\tsourcePath: targetPath,\n\t\t\tnotes: [],\n\t\t};\n\t}\n\n\tasync complete(): Promise<UrlCompletion[]> {\n\t\treturn getActiveSkills().map(skill => ({\n\t\t\tvalue: skill.name,\n\t\t\t...(skill.description ? { description: skill.description } : {}),\n\t\t}));","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/skill-protocol.ts#L93-L129","documentation":"The skill:// protocol only serves regular files and directories. After stat succeeds, if the target is neither a directory nor a regular file — e.g. a FIFO, socket, device node, or other special file — the handler rejects it, because reading content via `Bun.file().text()` would block or produce garbage for such entries.","triggerScenarios":"`SkillProtocolHandler.resolve()` where the resolved `targetPath` stats as something other than `isFile()` or `isDirectory()` — for example a named pipe, unix socket, or character/block device placed (or symlinked) at the skill's file path or at a contained path inside the plugin root.","commonSituations":"A plugin ships or symlinks a special file (e.g. a socket or FIFO) where SKILL.md or a referenced resource is expected; a broken provisioning step created a device node; someone linked `/dev/stdout`-style entries into the skill directory.","solutions":["Check what the path actually is with `ls -la` / `stat`; replace it with a regular file or directory.","If the target is a symlink to a special file, point it at a real text file instead.","Regenerate/reinstall the plugin so its resources are regular files.","Use the bash tool directly to read the special file rather than skill://."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\nconst stats = await fs.stat(targetPath);\nif (!stats.isFile() && !stats.isDirectory()) {\n  throw new Error(`Refusing skill:// URL: ${targetPath} is a special file`);\n}","typeGuard":"function isFileOrDir(stats: { isFile(): boolean; isDirectory(): boolean }): boolean {\n  return stats.isFile() || stats.isDirectory();\n}","tryCatchPattern":"try {\n  return await handler.resolve(url);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"must resolve to a file or directory\")) {\n    // read the special file via bash/child process instead\n    return readViaShell(targetPath);\n  }\n  throw err;\n}","preventionTips":["Inspect plugin contents with `ls -la` to spot FIFOs/sockets/devices before use.","Don't symlink /dev entries or sockets into skill directories.","Keep plugin resources as regular text files or directories."],"tags":["filesystem","skill-protocol","file-type"],"backgroundTag":"unsupported-resource-type","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}