{"record":{"id":"45c61021f21fa5a7","repo":"can1357/oh-my-pi","slug":"vault-url-must-resolve-to-a-file-parsed-url","errorCode":null,"errorMessage":"vault:// URL must resolve to a file: ${parsed.url}","messagePattern":"vault:// URL must resolve to a file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/internal-urls/vault-protocol.ts","lineNumber":910,"sourceCode":"\t\t\tcontent,\n\t\t\tcontentType: getContentType(realTargetPath),\n\t\t\tsize: Buffer.byteLength(content, \"utf-8\"),\n\t\t\tsourcePath: realTargetPath,\n\t\t};\n\t}\n\n\tasync #writeFile(\n\t\tparsed: Extract<ParsedVaultUrl, { kind: \"fs-file\" }>,\n\t\tcontent: string,\n\t\tcontext?: WriteContext,\n\t): Promise<void> {\n\t\tconst { root, targetPath } = await this.#resolveFsTarget(parsed, context);\n\t\ttry {\n\t\t\tconst realTargetPath = await fs.promises.realpath(targetPath);\n\t\t\tensureWithinRoot(realTargetPath, root);\n\t\t\tconst stat = await fs.promises.stat(realTargetPath);\n\t\t\tif (stat.isDirectory()) {\n\t\t\t\tthrow new Error(`vault:// URL must resolve to a file: ${parsed.url}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!isEnoent(error)) throw error;\n\t\t\tconst parentDir = path.dirname(targetPath);\n\t\t\tconst existingAncestor = await findExistingAncestor(parentDir, root);\n\t\t\tensureWithinRoot(existingAncestor, root);\n\t\t\tawait fs.promises.mkdir(parentDir, { recursive: true });\n\t\t\tconst realParent = await fs.promises.realpath(parentDir);\n\t\t\tensureWithinRoot(realParent, root);\n\t\t}\n\t\tawait Bun.write(targetPath, content);\n\t}\n\n\tasync #runCli(\n\t\tparsed: Extract<ParsedVaultUrl, { kind: \"file-op\" | \"vault-op\" }>,\n\t\tcontext?: ResolveContext,\n\t): Promise<InternalResource> {\n\t\tconst invocation = buildObsidianCliInvocation(parsed);","sourceCodeStart":892,"sourceCodeEnd":928,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/internal-urls/vault-protocol.ts#L892-L928","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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()."],"exampleFix":"// before\nawait handler.write(parseInternalUrl(\"vault://_/notes\"), text); // notes is a dir\n// after\nawait handler.write(parseInternalUrl(\"vault://_/notes/todo.md\"), text);","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\ntry {\n  if ((await fs.stat(targetPath)).isDirectory()) {\n    throw new Error(`Refusing to write: ${targetPath} is a directory; append a file name`);\n  }\n} catch (err) {\n  if (!(err as NodeJS.ErrnoException)?.code?.startsWith(\"ENOENT\") && err instanceof Error && !err.message.includes(\"Refusing\")) throw err;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await handler.write(url, content);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"must resolve to a file:\")) {\n    // append a default file name and retry\n    return handler.write(new InternalUrl(`${url.href.replace(/\\/$/, \"\")}/untitled.md`), content);\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["vault","filesystem","write-conflict"],"backgroundTag":"cannot-write-directory","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}