can1357/oh-my-pi · error
vault:// URL must resolve to a directory: ${parsed.url}
Error message
vault:// URL must resolve to a directory: ${parsed.url} What it means
When resolving a vault:// URL that was parsed as a directory (#readDirResource), the handler stats the real target path and requires it to actually be a directory. If the path exists but is a regular file (or other non-directory), this error is thrown with the original URL in the message. It catches mismatches between the URL's implied kind and the on-disk reality.
Source
Thrown at packages/coding-agent/src/internal-urls/vault-protocol.ts:834
context?: ResolveContext | WriteContext,
): Promise<{ root: string; targetPath: string }> {
const root = await this.#resolveVaultRoot(parsed.ref, context);
const resolvedRoot = await fs.promises.realpath(root);
const targetPath = parsed.relativePath ? path.resolve(resolvedRoot, parsed.relativePath) : resolvedRoot;
ensureWithinRoot(targetPath, resolvedRoot);
return { root: resolvedRoot, targetPath };
}
async #listDir(
parsed: Extract<ParsedVaultUrl, { kind: "fs-dir" | "fs-file" }>,
context?: ResolveContext,
): Promise<InternalResource> {
const { root, targetPath } = await this.#resolveFsTarget(parsed, context);
const realTargetPath = await fs.promises.realpath(targetPath);
ensureWithinRoot(realTargetPath, root);
const stat = await fs.promises.stat(realTargetPath);
if (!stat.isDirectory()) {
throw new Error(`vault:// URL must resolve to a directory: ${parsed.url}`);
}
const entries = await fs.promises.readdir(realTargetPath, { withFileTypes: true });
entries.sort((a, b) => a.name.localeCompare(b.name));
const baseRelative = parsed.relativePath ? `${parsed.relativePath}/` : "";
const lines = entries.map(entry => {
const entryRelativePath = `${baseRelative}${entry.name}`;
const isDir = entry.isDirectory();
const href = formatVaultPathForLink(parsed.ref, entryRelativePath, isDir);
return `- [${entry.name}${isDir ? "/" : ""}](${href})`;
});
const listing = lines.length === 0 ? "(empty)" : lines.join("\n");
const titlePath = parsed.relativePath ? `/${parsed.relativePath}/` : "/";
const content = `# Vault ${parsed.ref.display}${titlePath}\n\n${entries.length} entr${entries.length === 1 ? "y" : "ies"}:\n\n${listing}\n`;
return {
url: parsed.url,
content,
contentType: "text/markdown",
size: Buffer.byteLength(content, "utf-8"),View on GitHub (pinned to 9690622007)
Solutions
- Drop the directory form and resolve the URL as a file (no trailing slash) if the target is a file.
- Verify the on-disk path is actually a directory (ls/stat) and correct the URL path.
- Use the directory listing error to discover the correct target name, then rebuild the URL.
Example fix
// before
await resolveInternalUrl("vault://_/notes/"); // notes is actually a file
// after
await resolveInternalUrl("vault://_/notes"); // resolves as a file Defensive patterns
Strategy: type-guard
Validate before calling
import * as fs from "node:fs";
const stat = fs.statSync(targetPath, { throwIfNoEntry: false });
if (stat && !stat.isDirectory()) throw new Error(`${targetPath} is a file; drop the directory form of the vault:// URL`); Type guard
function isDirectoryStat(stat: fs.Stats | undefined): stat is fs.Stats & { isDirectory(): true } {
return !!stat && stat.isDirectory();
} Try / catch
try {
return await resolveInternalUrl(dirUrl);
} catch (err) {
if (err instanceof Error && err.message.includes("must resolve to a directory")) {
return resolveInternalUrl(dirUrl.replace(/\/$/, "")); // retry as file
}
throw err;
} Prevention
- Don't add trailing slashes to vault:// URLs unless you know the target is a directory.
- Stat ambiguous extensionless paths before choosing dir vs file URL form.
- Handle extensionless files explicitly in path-building code.
When it happens
Trigger: Resolving a vault:// URL as a directory (parsed kind fs-dir / trailing-slash or dir-kind parse) when the target path exists on disk as a regular file, e.g. vault://_/notes where notes is a file named "notes" with no extension.
Common situations: URLs built with a trailing slash from stale templates after the target changed from directory to file; ambiguous extensionless paths guessed to be directories; symlinks resolving to files.
Related errors
- vault:// URL must resolve to a file or directory: ${parsed.u
- Vault file not found: ${parsed.url}
- vault:// URL must resolve to a file: ${parsed.url}
- unknown filetype: {ft_debug}
- Is a directory
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/ee7de446b440a04b.
Report an issue: GitHub.