18 KiB
Safe JS API mirror checklist
Source checked: local obsidian package typings, node_modules/obsidian/obsidian.d.ts, version 1.12.3.
This is a planning checklist for the host methods that Safe JS should mirror. Keep the worker-facing API smaller than Obsidian's real API: expose JSON data and primitive values only, never live Obsidian objects.
Permission contract
- A method listed under
vault:readmust not create, modify, move, trash, or delete anything in the vault. - A read method may use Obsidian objects internally, but must return plain DTOs such as
{ path, type, stat }, strings, numbers, booleans, arrays, or JSON objects. - Never return
TFile,TFolder,TAbstractFile,Vault,DataAdapter,WorkspaceLeaf,Editor,View, DOM nodes, or callback handles to worker code. - Do not expose
app.vault.adapterdirectly. Adapter methods include both reads and writes and can make permission boundaries harder to audit. - Normalize and validate all vault paths at the RPC boundary. Reject absolute paths, parent traversal, empty file paths, and paths that resolve outside the vault model.
- Do not mirror Obsidian methods that require host callbacks, such as
Vault.processorFileManager.processFrontMatter, by evaluating worker-supplied functions on the host. - Event subscriptions are deferred until there is a lifecycle design for unsubscribe, plugin unload cleanup, and backpressure.
- If
network:requestis ever combined with vault or editor read permissions, the approval UI and docs must clearly warn that script output can leave the device. - Run a focused permission audit before expanding the RPC surface, especially for network requests, vault writes/deletes, editor writes, workspace navigation, and storage scope boundaries.
Focused audit note, 2026-05-15:
- Verified that RPC dispatch enforces each method's declared permission before request parsing or handler execution.
- Verified that vault read/create/modify/move/delete handlers use the vault path validation boundary and do not expose
app.vault.adapter. - Verified that network RPC methods require
network:request, are limited to HTTP/HTTPS URLs, and the approval UI warns when network access is combined with vault, metadata, workspace, or editor reads. - Tightened workspace read DTOs so recent/active/layout file paths are filtered through the Obsidian config-folder guard and layout output does not return arbitrary view state.
- Tightened storage key validation so script-provided keys cannot address the internal index key or collide with the scoped storage namespace from global storage.
core:read
Non-mutating app and environment information. These methods must not reveal full filesystem paths.
api.app.getVaultName()mirrorsapp.vault.getName().api.app.isDarkMode()mirrorsapp.isDarkMode().api.app.requireApiVersion(version)mirrorsrequireApiVersion(version).api.app.getLanguage()mirrorsgetLanguage().api.platform.get()mirrorsPlatformbooleans as a plain object.
Do not mirror:
app.vault.configDir: low value and exposes config folder naming.FileSystemAdapter.getBasePath(): exposes the local filesystem path and is desktop-only.app.secretStorage: secrets are out of scope for note-owned scripts.
vault:read
Read vault structure and file contents. These are allowed to call Vault read methods internally, but never Vault or DataAdapter write methods.
api.vault.read(path)mirrorsapp.vault.cachedRead(file)and returns{ path, content }.api.vault.readFresh(path)mirrorsapp.vault.read(file)and returns{ path, content }.api.vault.readBinary(path)mirrorsapp.vault.readBinary(file)and returns base64 or bytes encoded as JSON.api.vault.list(path?)mirrorsapp.vault.getAllLoadedFiles()and returns file/folder DTOs.api.vault.stat(path)mirrorsapp.vault.getAbstractFileByPath(path)plusTFile.stat.api.vault.exists(path)mirrorsapp.vault.getAbstractFileByPath(path) !== null.api.vault.getFile(path)mirrorsapp.vault.getFileByPath(path)and returns a file DTO ornull.api.vault.getFolder(path)mirrorsapp.vault.getFolderByPath(path)and returns a folder DTO ornull.api.vault.getRoot()mirrorsapp.vault.getRoot()and returns a folder DTO.api.vault.getFiles()mirrorsapp.vault.getFiles()and returns file DTOs.api.vault.getMarkdownFiles()mirrorsapp.vault.getMarkdownFiles()and returns file DTOs.api.vault.getFolders(options?)mirrorsapp.vault.getAllFolders(includeRoot?)and returns folder DTOs.
Read-only implementation guardrail:
vault:readhandlers may call onlygetName,getFileByPath,getFolderByPath,getAbstractFileByPath,getRoot,read,cachedRead,readBinary,getAllLoadedFiles,getAllFolders,getMarkdownFiles, andgetFiles.vault:readhandlers must not callcreate,createBinary,createFolder,delete,trash,rename,modify,modifyBinary,append,appendBinary,process,copy, anyDataAdaptermutator, or anyFileManagermutator.
Do not mirror under vault:read:
app.vault.getResourcePath(file): returns a browser resource URI for vault content. Consider a separatevault:resourcepermission if needed.app.vault.adapter.read/list/stat/exists: preferVaultmethods so the boundary stays inside Obsidian's vault model.- Vault events: defer until subscription cleanup and data volume limits are designed.
metadata:read
Read Obsidian's parsed cache. This can reveal filenames, links, tags, headings, blocks, frontmatter, and note structure, so keep it separate from pure vault existence checks.
api.metadata.getFileCache(path)mirrorsapp.metadataCache.getFileCache(file)and returns a JSON-safe cache DTO.api.metadata.getCache(path)mirrorsapp.metadataCache.getCache(path)and returns a JSON-safe cache DTO.api.metadata.getFirstLinkpathDest(linkpath, sourcePath)mirrorsapp.metadataCache.getFirstLinkpathDest(...)and returns a file DTO ornull.api.metadata.fileToLinktext(path, sourcePath, options?)mirrorsapp.metadataCache.fileToLinktext(...).api.metadata.getResolvedLinks()mirrorsapp.metadataCache.resolvedLinks.api.metadata.getUnresolvedLinks()mirrorsapp.metadataCache.unresolvedLinks.api.metadata.getAllTags(path)mirrorsgetAllTags(cache).api.metadata.resolveSubpath(path, subpath)mirrorsresolveSubpath(cache, subpath).api.frontmatter.getInfo(content)mirrorsgetFrontMatterInfo(content).api.frontmatter.parseAliases(frontmatter)mirrorsparseFrontMatterAliases(frontmatter).api.frontmatter.parseTags(frontmatter)mirrorsparseFrontMatterTags(frontmatter).api.frontmatter.parseStringArray(frontmatter, key)mirrorsparseFrontMatterStringArray(frontmatter, key).api.frontmatter.parseEntry(frontmatter, key)mirrorsparseFrontMatterEntry(frontmatter, key).
Do not mirror:
- Metadata events: defer until subscription cleanup and throttling are designed.
LinkValue.parseFromString(app, ...): returns an Obsidian value object. Use plain link parsing DTOs instead.
vault:create
Create new vault entries without modifying existing content.
api.vault.create(path, content, options?)mirrorsapp.vault.create(...).api.vault.createBinary(path, data, options?)mirrorsapp.vault.createBinary(...).api.vault.createFolder(path)mirrorsapp.vault.createFolder(...).api.vault.copy(path, newPath)mirrorsapp.vault.copy(...).
Implementation guardrail:
vault:createmust fail if the target path already exists, except where Obsidian's mirrored method already guarantees that behavior.vault:createmust not overwrite or append existing file contents.
vault:modify
Modify existing vault file content.
api.vault.modify(path, content, options?)mirrorsapp.vault.modify(...).api.vault.modifyBinary(path, data, options?)mirrorsapp.vault.modifyBinary(...).api.vault.append(path, content, options?)mirrorsapp.vault.append(...).api.vault.appendBinary(path, data, options?)mirrorsapp.vault.appendBinary(...).api.frontmatter.replace(path, frontmatter, options?)is a safe DTO alternative toapp.fileManager.processFrontMatter(...).
Do not mirror directly:
app.vault.process(file, fn, options?): would require a host callback. Use explicit read/modify RPCs or a declarative operation instead.app.fileManager.processFrontMatter(file, fn, options?): would require a host callback. Useapi.frontmatter.replaceor specific patch operations instead.
vault:move
Rename or move files and folders.
api.vault.rename(path, newPath)mirrorsapp.vault.rename(...).api.fileManager.renameFile(path, newPath)mirrorsapp.fileManager.renameFile(...)and may update links according to user settings.
Implementation guardrail:
- Document that
api.fileManager.renameFilecan modify links in other notes and should not be treated as a simple path metadata change.
vault:delete
Trash or delete vault entries.
api.vault.trash(path, system)mirrorsapp.vault.trash(...).api.vault.delete(path, options?)mirrorsapp.vault.delete(...).api.fileManager.trashFile(path)mirrorsapp.fileManager.trashFile(...).
Do not mirror:
app.fileManager.promptForDeletion(file): host UI flow is better owned by Safe JS permission approval or a dedicated confirmation API.
workspace:read
Read workspace state and active file identity. This can reveal filenames and user activity.
api.workspace.getActiveFile()mirrorsapp.workspace.getActiveFile()and returns a file DTO ornull.api.workspace.getLastOpenFiles()mirrorsapp.workspace.getLastOpenFiles().api.workspace.getLeavesOfType(viewType)mirrorsapp.workspace.getLeavesOfType(...)and returns leaf DTOs.api.workspace.getLayout()mirrorsapp.workspace.getLayout()after stripping plugin-private or non-JSON values.api.workspace.getActiveViewInfo()mirrors safe parts ofgetActiveViewOfType, active leaf, and active editor state.
Do not mirror:
WorkspaceLeaf,View, orMarkdownViewobjects. Return DTOs only.- Workspace events: defer until subscription cleanup and throttling are designed.
workspace:navigate
Change Obsidian UI focus, panes, or opened files without editing vault content.
api.workspace.openLinkText(linktext, sourcePath, options?)mirrorsapp.workspace.openLinkText(...).api.workspace.openFile(path, openState?)mirrorsWorkspaceLeaf.openFile(...)through a host-selected leaf.api.workspace.revealLeaf(leafId)mirrorsapp.workspace.revealLeaf(...).api.workspace.setActiveLeaf(leafId, options?)mirrorsapp.workspace.setActiveLeaf(...).api.workspace.newLeaf(options?)mirrors safe cases ofapp.workspace.getLeaf(...).
Implementation guardrail:
workspace:navigatemethods must only open existing files or views. If an unresolved link or path would create a note, requirevault:createtoo or reject the call.workspace:navigatemethods must not call vault create, modify, move, trash, or delete methods as a side effect.
Do not mirror:
app.workspace.changeLayout(workspace): arbitrary layout mutation is too broad.app.workspace.detachLeavesOfType(viewType): destructive UI operation; add only with a specific use case.- Popout methods as mobile-safe defaults:
moveLeafToPopoutandopenPopoutLeafare desktop-only.
editor:read
Read the active editor, including unsaved content.
api.editor.getValue()mirrorseditor.getValue().api.editor.getLine(line)mirrorseditor.getLine(...).api.editor.lineCount()mirrorseditor.lineCount().api.editor.lastLine()mirrorseditor.lastLine().api.editor.getSelection()mirrorseditor.getSelection().api.editor.getRange(from, to)mirrorseditor.getRange(...).api.editor.getCursor(side?)mirrorseditor.getCursor(...).api.editor.listSelections()mirrorseditor.listSelections().api.editor.hasFocus()mirrorseditor.hasFocus().api.editor.getScrollInfo()mirrorseditor.getScrollInfo().api.editor.wordAt(pos)mirrorseditor.wordAt(...).api.editor.posToOffset(pos)mirrorseditor.posToOffset(...).api.editor.offsetToPos(offset)mirrorseditor.offsetToPos(...).
editor:write
Modify active editor content or editor UI state. This can change unsaved note content and may later be persisted by Obsidian.
api.editor.setValue(content)mirrorseditor.setValue(...).api.editor.setLine(line, text)mirrorseditor.setLine(...).api.editor.replaceSelection(replacement, origin?)mirrorseditor.replaceSelection(...).api.editor.replaceRange(replacement, from, to?, origin?)mirrorseditor.replaceRange(...).api.editor.setCursor(pos)mirrorseditor.setCursor(...).api.editor.setSelection(anchor, head?)mirrorseditor.setSelection(...).api.editor.setSelections(ranges, main?)mirrorseditor.setSelections(...).api.editor.scrollTo(x?, y?)mirrorseditor.scrollTo(...).api.editor.scrollIntoView(range, center?)mirrorseditor.scrollIntoView(...).api.editor.focus()mirrorseditor.focus().api.editor.blur()mirrorseditor.blur().api.editor.undo()mirrorseditor.undo().api.editor.redo()mirrorseditor.redo().api.editor.exec(command)mirrorseditor.exec(...)with an allowlist ofEditorCommandName.
Do not mirror directly:
editor.transaction(tx, origin?)until each transaction field is validated and proven no broader than the explicit editor write methods.editor.processLines(read, write, ignoreEmpty?): requires host callbacks.- CodeMirror
EditorViewaccess througheditorEditorField: too broad for safe JS.
file-manager:read
Read link and destination helpers from FileManager.
api.fileManager.getNewFileParent(sourcePath, newFilePath?)mirrorsapp.fileManager.getNewFileParent(...)and returns a folder DTO.api.fileManager.generateMarkdownLink(path, sourcePath, options?)mirrorsapp.fileManager.generateMarkdownLink(...).
Do not mirror under read:
app.fileManager.getAvailablePathForAttachment(filename, sourcePath?): docs say it ensures the parent directory exists, so treat it as write-capable unless verified otherwise.
ui:notify
Display short user-visible notifications.
api.ui.notice(message, duration?)mirrorsnew Notice(message, duration).
Do not mirror:
- Arbitrary
Modal,Menu,Setting, or DOM construction APIs. They expose DOM objects and lifecycle complexity. setIcon,addIcon,removeIcon,setTooltip, and related DOM helpers. These mutate host UI/global icon state.
network:request
Network is off by default and must be explicitly requested and documented.
api.network.request(urlOrOptions)mirrorsrequest(...).api.network.requestUrl(urlOrOptions)mirrorsrequestUrl(...)and returns a JSON-safe response DTO.
Implementation guardrail:
- Approval copy must say that network access can send script-provided data to external services.
- If combined with
vault:read,metadata:read,workspace:read, oreditor:read, approval copy must say vault or editor data can be exfiltrated.
storage:read
Read Safe JS storage scoped to the script source hash only. Do not expose arbitrary vault localStorage keys. Do NOT expose the permission storage.
api.storage.get(key)mirrors a Safe JS-owned source-scoped wrapper overapp.loadLocalStorage(...).
storage:write
Write Safe JS storage scoped to the script source hash only.
api.storage.set(key, value)mirrors a Safe JS-owned source-scoped wrapper overapp.saveLocalStorage(...).api.storage.delete(key)mirrorsapp.saveLocalStorage(key, null)through a Safe JS-owned source-scoped wrapper.
storage:global-read
Read Safe JS storage shared across approved scripts on this device.
api.globalStorage.get(key)mirrors a Safe JS-owned global wrapper overapp.loadLocalStorage(...).
storage:global-write
Write Safe JS storage shared across approved scripts on this device.
api.globalStorage.set(key, value)mirrors a Safe JS-owned global wrapper overapp.saveLocalStorage(...).api.globalStorage.delete(key)mirrorsapp.saveLocalStorage(key, null)through a Safe JS-owned global wrapper.
Implementation guardrail:
- Prefix all keys with a Safe JS namespace and validate key length and value size.
- Do not expose raw
app.loadLocalStorageorapp.saveLocalStorage.
Helper methods
These mirror pure Obsidian helper functions through normal permission-gated RPC methods.
api.path.normalize(path)mirrorsnormalizePath(path).api.link.parseLinktext(linktext)mirrorsparseLinktext(linktext).api.link.getLinkpath(linktext)mirrorsgetLinkpath(linktext).api.search.prepareSimpleSearch(query, text)mirrorsprepareSimpleSearch(query)(text).api.search.prepareFuzzySearch(query, text)mirrorsprepareFuzzySearch(query)(text).api.yaml.parse(yaml)mirrorsparseYaml(yaml).api.yaml.stringify(value)mirrorsstringifyYaml(value).api.html.toMarkdown(html)mirrorshtmlToMarkdown(html)if it can run without DOM leakage.api.html.sanitize(html)mirrorssanitizeHTMLToDom(html)only if returning sanitized HTML text, not DOM nodes.
Explicitly out of scope
- Full
appaccess. - Full
Vault,DataAdapter,FileManager,Workspace,WorkspaceLeaf,Editor,MetadataCache,Component,Plugin, or DOM object access. - Node, Electron, shell, filesystem paths outside the vault, or Obsidian internals.
- Remote code loading, script auto-update,
evalon host, or fetching and executing code. - APIs that register long-lived host callbacks until a cleanup and resource limit design exists.