can1357/oh-my-pi · error
skill:// URL must resolve to a file or directory: ${url.href
Error message
skill:// URL must resolve to a file or directory: ${url.href} What it means
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.
Source
Thrown at packages/coding-agent/src/internal-urls/skill-protocol.ts:111
} else {
targetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;
}
let stats: fsTypes.Stats;
try {
stats = await fs.stat(targetPath);
} catch (error) {
if (isEnoent(error)) {
throw new Error(`File not found: ${targetPath}`);
}
throw error;
}
if (stats.isDirectory()) {
return buildDirectoryResource(url.href, targetPath);
}
if (!stats.isFile()) {
throw new Error(`skill:// URL must resolve to a file or directory: ${url.href}`);
}
const content = await Bun.file(targetPath).text();
return {
url: url.href,
content,
contentType: getContentType(targetPath),
size: Buffer.byteLength(content, "utf-8"),
sourcePath: targetPath,
notes: [],
};
}
async complete(): Promise<UrlCompletion[]> {
return getActiveSkills().map(skill => ({
value: skill.name,
...(skill.description ? { description: skill.description } : {}),
}));View on GitHub (pinned to 9690622007)
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://.
Defensive patterns
Strategy: validation
Validate before calling
import * as fs from "node:fs/promises";
const stats = await fs.stat(targetPath);
if (!stats.isFile() && !stats.isDirectory()) {
throw new Error(`Refusing skill:// URL: ${targetPath} is a special file`);
} Type guard
function isFileOrDir(stats: { isFile(): boolean; isDirectory(): boolean }): boolean {
return stats.isFile() || stats.isDirectory();
} Try / catch
try {
return await handler.resolve(url);
} catch (err) {
if (err instanceof Error && err.message.includes("must resolve to a file or directory")) {
// read the special file via bash/child process instead
return readViaShell(targetPath);
}
throw err;
} Prevention
- 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.
When it happens
Trigger: `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.
Common situations: 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.
Related errors
- unknown filetype: {ft_debug}
- local:// URL must resolve to a file or directory: ${url.href
- File not found: ${resolvedPath}
- File not found: ${targetPath}
- Is a directory
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/1db79111a2e1ed73.
Report an issue: GitHub.