can1357/oh-my-pi · error
vault:// URL must resolve to a file: ${parsed.url}
Error message
vault:// URL must resolve to a file: ${parsed.url} What it means
The write path pre-checks the destination: if realpath+stat succeed and the target is a directory, writing file content is rejected with this error. The check happens inside a try/catch that otherwise tolerates ENOENT (allowing writes to new files whose parents may not exist yet). It prevents silently clobbering a directory path with a file write.
Source
Thrown at packages/coding-agent/src/internal-urls/vault-protocol.ts:910
content,
contentType: getContentType(realTargetPath),
size: Buffer.byteLength(content, "utf-8"),
sourcePath: realTargetPath,
};
}
async #writeFile(
parsed: Extract<ParsedVaultUrl, { kind: "fs-file" }>,
content: string,
context?: WriteContext,
): Promise<void> {
const { root, targetPath } = await this.#resolveFsTarget(parsed, context);
try {
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 file: ${parsed.url}`);
}
} catch (error) {
if (!isEnoent(error)) throw error;
const parentDir = path.dirname(targetPath);
const existingAncestor = await findExistingAncestor(parentDir, root);
ensureWithinRoot(existingAncestor, root);
await fs.promises.mkdir(parentDir, { recursive: true });
const realParent = await fs.promises.realpath(parentDir);
ensureWithinRoot(realParent, root);
}
await Bun.write(targetPath, content);
}
async #runCli(
parsed: Extract<ParsedVaultUrl, { kind: "file-op" | "vault-op" }>,
context?: ResolveContext,
): Promise<InternalResource> {
const invocation = buildObsidianCliInvocation(parsed);View on GitHub (pinned to 9690622007)
Solutions
- Append a concrete file name to the URL: vault://<vault>/dir/note.md.
- Check the target with parseVaultUrl/stat before writing if your code derives paths dynamically.
- If the intent is to create a directory, use a directory-creation flow instead of write().
Example fix
// before
await handler.write(parseInternalUrl("vault://_/notes"), text); // notes is a dir
// after
await handler.write(parseInternalUrl("vault://_/notes/todo.md"), text); Defensive patterns
Strategy: validation
Validate before calling
import * as fs from "node:fs/promises";
try {
if ((await fs.stat(targetPath)).isDirectory()) {
throw new Error(`Refusing to write: ${targetPath} is a directory; append a file name`);
}
} catch (err) {
if (!(err as NodeJS.ErrnoException)?.code?.startsWith("ENOENT") && err instanceof Error && !err.message.includes("Refusing")) throw err;
} Try / catch
try {
await handler.write(url, content);
} catch (err) {
if (err instanceof Error && err.message.includes("must resolve to a file:")) {
// append a default file name and retry
return handler.write(new InternalUrl(`${url.href.replace(/\/$/, "")}/untitled.md`), content);
}
throw err;
} Prevention
- Ensure write targets end in a file name, never a bare directory segment.
- Stat the destination before writing when the path is derived dynamically.
- Use a distinct flow for directory creation vs file writes.
When it happens
Trigger: Calling handler.write(url, content) with a vault:// URL that resolves to an existing directory, e.g. writing to vault://_/notes when "notes" is a directory.
Common situations: Building the write target from a directory path and forgetting a filename; users pasting a folder URL into a write flow; path-joining bugs producing a directory as the final target.
Related errors
- vault:// URL must resolve to a directory: ${parsed.url}
- Vault file not found: ${parsed.url}
- vault:// URL must resolve to a file or directory: ${parsed.u
- unknown filetype: {ft_debug}
- Is a directory
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/45c61021f21fa5a7.
Report an issue: GitHub.