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

  1. Check what the path actually is with `ls -la` / `stat`; replace it with a regular file or directory.
  2. If the target is a symlink to a special file, point it at a real text file instead.
  3. Regenerate/reinstall the plugin so its resources are regular files.
  4. 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

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


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