Plugin compatibility

How well each Obsidian community plugin holds up when the vault is served by obsidian-remote-ssh. Updated as we test in the dev vault.

How to read this table

The plugin patches app.vault.adapter and routes filesystem calls through the remote daemon. A plugin works if every operation that touches vault files goes through app.vault.read / write / list / watch / getResourcePath. A plugin breaks if it bypasses those — typically by importing Node’s fs directly, by reading app.vault.adapter.basePath and joining file paths against it, or by using internal Obsidian APIs we don’t intercept.

We can usually predict each plugin’s status from its access pattern without running it; the Status column distinguishes:

StatusMeaning
✅ verifiedsmoke-tested in the dev vault, the typical workflow works
🟡 expectedarchitectural read says it should work, not yet smoke-tested
⚠️ degradedworks but has a known UX problem (latency, full-vault reads, etc.)
❌ brokenknown to bypass the patched adapter; remote vault unreachable
❔ unknownhaven’t looked at the source / haven’t tested

Numbers in the table reflect a ~/work/VaultDev-class remote (SSH RTT under ~30 ms, vault under a few hundred files).

Compatibility matrix

PluginAccess patternStatusNotes
Dataviewreads every MD file via app.vault.cachedRead for the index, then queries against app.metadataCache✅ verified-by-harness (#124 F11)Initial index build does N reads on connect. ReadCache absorbs subsequent queries. Watch for noticeable startup delay on bigger vaults. F11 harness scenario (plugin/tests/compat/dataview.test.ts) drives dv.pages()-shape queries against a 5-page fixture and asserts frontmatter scalars + aggregations round-trip through metadataCache.getFileCache().
Templaterreads templates from a configured folder, evaluates JS, writes through app.vault.modify / create. tp.file.path(false) and the child_process.exec cwd path read adapter.basePath.✅ verified-by-harness (#124 F12)basePath now resolves to the shadow-vault local root via #170, so tp.file.path(false) returns a path whose fs.readFileSync finds mirrored content. JS user functions that import Node fs and write under basePath land in the shadow dir, which propagates to the remote. F12 harness scenario (plugin/tests/compat/templater.test.ts) drives tp.file.create_new against 3 fixture templates (date placeholder, frontmatter + title, no-date) and asserts vault.create + vault.modify round-trip with metadataCache reflecting the new frontmatter.
Kanbanstores boards as MD files with YAML frontmatter; standard vault read/write on every drag. Clipboard image paste joins (adapter as any).basePath with the attachment path and calls fs.copyFile.🟡 expected (smoke pending after #170)Each card move = 1 write. Network latency may show as a noticeable lag on big boards; otherwise fine. Clipboard paste is fixed by #170: basePath now resolves to the shadow-vault local root, so fs.copyFile lands in the shadow dir.
Thinoreads/writes daily-note-style files; standard vault API🟡 expectedPure vault API user. No special concerns.
CommanderUI plugin: ribbon icons, hotkeys, command macros. Doesn’t touch vault files for its own state (uses plugin data via loadData/saveData, which goes through the patched adapter)🟡 expectedIf a custom command invokes a different plugin, the wrapped plugin’s compatibility applies.
Emoji Shortcodespure UI typing helper. No filesystem access✅ verified-by-architectureNot affected by adapter patching at all.
Heatmap Calendarreads MD files to count occurrences, aggregates frontmatter🟡 expected (similar to Dataview)Heavy first-pass read on connect. ReadCache mitigates re-renders.
Meta Bindinput bindings on YAML / inline frontmatter; reads / writes via vault API🟡 expectedEach input change writes the host note. Latency is noticeable but functional.
Omnisearchfull-text indexer: reads every file in the vault on init + on changes⚠️ degraded (expected)This is the most network-bound plugin in the list. Initial index build on a remote vault can take seconds-to-minutes depending on size. After the warm cache, queries are local. Recommendation: only enable Omnisearch when the connection is stable.
QuickLatexrenders LaTeX inline. Pure UI, no FS access✅ verified-by-architectureNot affected.
Importerconverts external formats (Evernote .enex, etc.) to MD using path.join(getBasePath(), folder.path) as outputDir for the Yarle Evernote converter (Node fs.writeFile)🟡 expected (smoke pending after #170)getBasePath() now returns the shadow-vault local root via #170, so the converter writes files into the shadow dir; the file-watcher propagates them to the remote. Initial conversion of a large .enex may produce many writes; watch for queue lag.
Copilotreads getBasePath?.() then falls back to basePath for local-context AI indexing (src/miyo/miyoUtils.ts)🟡 expected (smoke pending after #170)Either form now resolves to the shadow-vault local root via #170, so Copilot’s local-context indexing operates against the synced copy. The remote is the source of truth; the index reflects whatever has been mirrored to the shadow dir.
Git (Vinzent03)desktop hardcodes SimpleGit (spawns local git against getBasePath()); mobile uses IsomorphicGit via MyAdapter(vault.adapter) (pure JS, no shell-out)❌ broken on desktop / fixable upstreamThe desktop path silently mis-routes commits to the shadow git repo, not the remote. The isomorphic-git path would work transparently against our remote adapter but is gated behind Platform.isDesktopApp with no toggle. We won’t ship a workaround — see #150 for the rationale. Users wanting git on a remote vault should use the integrated terminal pane (#149) or file a feature request at Vinzent03/obsidian-git for a forceIsomorphicGit toggle.
Excalidrawdrawings stored as .excalidraw.md (JSON) or embedded markdown; embedded images go through getResourcePath. pathToFileURL(adapter.basePath) is used as a vault-membership prefix check (src/utils/fileUtils.ts:343).✅ verified-by-harness (#124 F13)The RPC transport is required for the ResourceBridge to serve images. On SFTP transport, embedded images fall back to a broken data: URL. First read of a large .excalidraw.md pulls the whole JSON; subsequent edits stream cleanly. After #170 the prefix check stays internally consistent (both sides see the shadow path). F13 harness scenario (plugin/tests/compat/excalidraw.test.ts) covers .excalidraw.md text round-trip + binary attachment CRUD with byte-exact equality on a 1 KB cyclic blob and a 4 KB PNG-magic + xorshift32 payload.
Remotely SavegetBasePath().split("?")[0] as a vault-instance ID for cloud-sync conflict detection (src/main.ts:1736)🟡 expectedShadow-vault path is fine: gives a stable per-machine ID after #170. Cloud-sync semantics aren’t affected.
Tasksscans metadataCache.getFileCache(file).listItems across every markdown file for - [ ] checkboxes; aggregates open / done counts✅ verified-by-harness (#124 F14)Pure metadataCache reader, no basePath access. F14 harness scenario (plugin/tests/compat/tasks.test.ts) drives the per-file aggregation against a 10-file fixture vault (mixed open/done counts, frontmatter-tagged entries, empty/no-task negative-controls) and asserts vault-wide totals (18 open / 16 done / 34 total / 8 with-tasks) plus DataviewJS-shape filters (project-tag, completion ratio, top-3 by open count).

Updating this table

When you actually exercise a plugin in the dev vault:

  1. Connect to the remote, patch the adapter (auto-patch is on by default).
  2. Enable the plugin.
  3. Run the operations listed under What to test below for that plugin.
  4. Move the row’s status from 🟡 / ❔ to ✅ if it worked, or ❌ / ⚠️ if it didn’t, with a one-line note about what failed and what you saw in <vault>/.obsidian/plugins/remote-ssh/console.log.

What to test (per plugin)

Quick smoke checklists. None of these are exhaustive — the goal is to catch the obvious “the plugin doesn’t see remote files” or “the plugin fights the adapter” failure modes.

Dataview

  • First connect: open a note that has a dataview block — does it render?
  • Modify a referenced note from a different machine, watch the block re-render after the fs.changed → reconcile cycle.

Templater

  • Insert a template that runs a JS user function. Does it execute?
  • Templates that read another vault file (via tp.file.find_tfile): do they resolve?

Kanban

  • Create a new board, drag cards between lanes, save. Does the .md on the remote update?
  • Reload the vault — board state intact?

Excalidraw

  • Create a new drawing. Does the .excalidraw.md land on the remote?
  • Embed an image — does the image render in the canvas? (Requires RPC transport.)

Thino

  • Add a quick note, check the daily file on the remote.
  • Edit a past entry; does the modification land?

Commander

  • Bind a custom command to a hotkey. Does invoking it work? (No vault-side test required since Commander itself doesn’t touch the FS.)

Heatmap Calendar

  • Render a heatmap of notes-per-day. Does it count correctly on first load?
  • Modify a few past notes; does the heatmap refresh?

Meta Bind

  • Place a meta-bind input in a note, change its value, confirm the host note’s frontmatter updates on the remote.

Omnisearch

  • Trigger a search. Note startup time on initial index build.
  • Re-search; should be fast (hits the local index).
  • After remote-side edits, are new files findable within ~30 s?

QuickLatex

  • Render an inline math formula. (No vault-side test required.)

Emoji Shortcodes

  • Type :smile: in a note, expect the emoji to expand. (No vault-side test required.)

Known footguns (cross-plugin)

Things that aren’t a specific plugin but trip plugins in general:

  • app.vault.adapter.basePath and getBasePath() resolve to the shadow vault’s local root (e.g. ~/.obsidian-remote/vaults/<P-id>/), not the remote SSH path. Plugins that join paths against basePath and feed them to Node fs directly read mirrored content and write into the shadow dir; the file-watcher then propagates writes back to the remote. This is the natural value of FileSystemAdapter.basePath in the shadow window, and #170 patches both forms onto the replacement adapter explicitly so the contract is stable across Obsidian version upgrades. See the basePath compat survey section below (#133, 2026-04-29) for the top-20 plugin survey, and #170 for the implementation. Exception: plugins that shell out to a local binary (notably obsidian-Git’s SimpleGit desktop path) operate on the shadow git repo rather than the remote one — patching can’t fix this from our side. obsidian-Git’s bundled IsomorphicGit mode would work transparently against our adapter but is gated to mobile-only by the upstream; see the table row on line 46 for the user-facing recommendation.
  • Worker threads spawned by plugins are independent JS contexts — they don’t see our patched app.vault.adapter. Plugins that pass file paths to a worker for parsing (some search-heavy plugins do this) will break against the remote. Same diagnosis: no general fix.
  • fs.watch from Node doesn’t reach the remote. Our patched adapter feeds app.vault.adapter.on('modify', …)-style listeners through the daemon’s fs.watch notifications, but a plugin that installs its own fs.watch against basePath only watches the local empty directory.
  • Static asset URLs (app://local/<path>) — a few plugins build these manually instead of going through getResourcePath. The manually-built URL points at the local FS and won’t render. The ResourceBridge fixes this only when getResourcePath is the entry point. The app://local URL survey (#174, 2026-05-01) found zero usage across all top-20 plugins; only niche image-processing plugins outside the top-20 use this pattern (see survey section below). No webview-side URL rewriting is needed at this time.

Why we can’t auto-test all of this

Every plugin lives in its own JS context with its own internal state, and the meaningful failures only surface in interactive use (“I clicked X and it didn’t render”). A unit-test approximation would exercise our adapter, not the plugin’s actual code path. So this doc is maintained by hand from manual smoke testing — when something unexpected breaks, please update the row.

basePath compat survey (#133, 2026-04-29)

Investigation tracker for issue #133. We surveyed the top-20 most-installed community plugins (sorted by lifetime downloads from obsidianmd/obsidian-releases’s community-plugin-stats.json on 2026-04-29) for direct reads of app.vault.adapter.basePath / getBasePath(). The goal is to decide what basePath should return on a remote vault — the issue is investigation only; no code changes ship with this survey.

Headline

  • 6 / 20 plugins read basePath (or the equivalent getBasePath() method) from the adapter.
  • 3 are high-risk (fs-read): Templater, Kanban, Importer.
  • 1 is incompatible without an upstream change: Git (Vinzent03) hardcodes SimpleGit (shells out to local git) on desktop. Its bundled IsomorphicGit path uses a vault.adapter wrapper and would work against our remote adapter, but the gating (Platform.isDesktopApp) has no toggle. Tracked in #150 as won’t-do; see table row on line 46.
  • The method form getBasePath() is more common in real usage than the property .basePath (Templater, Importer, Copilot, Remotely Save, Git all prefer it). Both must be patched if we ship a fix.

Per-plugin findings

Categories: none (no usage), display-only (UI / metadata), fs-read (joined and passed to Node fs), fs-stat, passthrough (handed back to a patched adapter method), other (URL prefix compare, child-process cwd, etc.).

PluginInstallsWhereCategoryRiskMitigation
Excalidraw5.9Msrc/utils/fileUtils.ts:343pathToFileURL(adapter.basePath) for drag-drop “is this file in the vault?” prefix-matchothermediumShadow-vault path keeps the prefix check internally consistent
Templater4.2MInternalModuleFile.ts:256/262tp.file.path(false) joins basePath with the target path; UserSystemFunctions.ts:23 uses it as child_process.exec cwdfs-read + exec cwdhighPatch basePath/getBasePath() to return shadow-vault path; user scripts run against the synced shadow copy
Dataview4.1Mnone in plugin source (only hit was a bundled hot-reload dev tool)nonenonen/a — uses metadataCache only
Tasks3.4Mnone in plugin sourcenonenonen/a
Advanced Tables2.8Mnonenonenonen/a
Calendar2.6Mnonenonenonen/a
Git (Vinzent03)2.5Msrc/main.ts:466path.join(getBasePath(), filePath) for electron.shell.showItemInFolder; simpleGit.ts:44 uses getBasePath() as simple-git baseDir (spawns local git); gitManager/myAdapter.ts:10-37 wraps vault.adapter for IsomorphicGit (mobile-only path)fs-read + child-processhigh (fixable upstream)SimpleGit (desktop) shells out to local git against a local path; patching basePath would silently mis-route. IsomorphicGit (mobile, gated Platform.isDesktopApp) routes through vault.adapter and would work transparently against our remote adapter — but the gating has no toggle. We won’t ship a workaround; see #150 and table row on line 46
Style Settings2.3Mnonenonenonen/a
Kanban2.2Msrc/components/Item/helpers.ts:450(adapter as any).basePath joined with attachment path, fed to fs.copyFile on Electron clipboard image pastefs-readhighPatch returns shadow-vault path; fs.copyFile lands in shadow vault and syncs up
Iconize2.0Mnonenonenonen/a
Remotely Save1.9Msrc/main.ts:1736getBasePath().split("?")[0] as a vault-instance ID for cloud-sync conflict detectiondisplay-onlylowShadow-vault path is fine; gives a stable per-machine ID
QuickAdd1.7Mnonenonenonen/a
Minimal Theme Settings1.5Mnonenonenonen/a
Omnisearch1.4Mnonenonenonen/a
Editing Toolbar1.4Mnonenonenonen/a
Copilot1.3Msrc/miyo/miyoUtils.ts:105-110 — defensive getBasePath?.() then basePath fallback; flows into local-context AI machinerypassthroughmediumShadow-vault path lets Copilot index the synced local copy
Importer1.2Msrc/formats/evernote-enex.ts:34path.join(getBasePath(), folder.path) as outputDir for the Yarle Evernote converter (Node fs writes)fs-readhighSame shape as Kanban; shadow-vault path lands writes in the synced copy
Outliner1.2Mnonenonenonen/a
Homepage1.1Mnonenonenonen/a
Recent Files1.0Mnonenonenonen/a

Recommendations

  • Patch both basePath (getter) and getBasePath() (method) to return the shadow-vault local path. This single change makes Templater, Kanban, Importer, and Copilot work transparently — path.join(basePath, …) plus Node fs.* writes land in the shadow vault, which the file-watcher syncs up to the remote.
  • Returning the shadow-vault path is also safe for the display/metadata cases (Excalidraw URL-prefix compare, Remotely Save instance ID): both want a stable, internally consistent local path, which the shadow-vault path provides.
  • obsidian-Git is the one casualty — desktop hardcodes SimpleGit, which shells out to a local git binary, so even with basePath patched it would operate on the shadow vault rather than the canonical remote one. obsidian-Git’s bundled IsomorphicGit path does go through vault.adapter and would work against our remote adapter transparently, but it’s gated Platform.isDesktopApp with no toggle. We won’t ship a workaround (see #150); users who want git on a remote vault should use the integrated terminal pane (#149).
  • 14 / 20 top plugins read no basePath at all, so the blast radius of the current “do nothing” stance is ~30% of the most- installed plugins. That is too large to ignore but small enough that a single uniform patch (no per-plugin shim layer) is the high-leverage choice.
  • Next step is a follow-up implementation issue, not a change in this PR. Scope: extend PATCHED_METHODS (plugin/src/main.ts:49) to include basePath and getBasePath, route both to the shadow vault’s local root, and add an app://local/<path> rewrite check for plugins that build asset URLs by hand from basePath (out of scope of this survey but worth keeping on the radar).

Implementation status (2026-04-30)

  • #170 (this PR) ships the survey’s primary recommendation. PATCHED_METHODS now includes basePath and getBasePath, both routing to the shadow-vault local root captured at patch time from the host FileSystemAdapter.getBasePath(). AdapterPatcher was extended to handle property-getter members (the previous function-only path didn’t cover the basePath accessor).
  • #174 tracks the separate app://local/<path> URL-rewrite follow-up; out of scope for #170.
  • Smoke verification of Templater / Kanban / Importer / Copilot in a real dev vault remains a manual step; the matrix above keeps these at 🟡 expected (smoke pending after #170) until run.

Method

  • Registry source: community-plugins.json + community-plugin-stats.json from obsidianmd/obsidian-releases, fetched 2026-04-29.
  • Top-20 selected by lifetime downloads desc.
  • Per-repo search: GitHub code-search API restricted to TS/JS sources; matches inside bundled hot-reload dev tools or sample_vault/.../main.js test fixtures were excluded.
  • One-to-two representative usage sites recorded per plugin — exhaustive coverage was not the goal; the categorization is what drives the recommendation.

app://local URL survey (#174, 2026-05-01)

Investigation tracker for issue #174. We surveyed the same top-20 plugin set for manual construction of app://local/<path> URLs — the Electron protocol that Obsidian uses to serve local vault assets. Plugins that build these by hand (instead of calling getResourcePath) bypass the ResourceBridge and render broken images on a remote vault.

Headline

  • 0 / 20 top plugins construct app://local URLs by hand.
  • Dataview mentions the protocol in its CHANGELOG / docs only (no runtime code).
  • The pattern exists only in niche image-processing plugins outside the top-20 (see below).

Top-20 results

PluginInstallsapp://local in source?
Excalidraw5.9Mno
Templater4.2Mno
Dataview4.1Mno (docs only)
Tasks3.4Mno
Advanced Tables2.8Mno
Calendar2.6Mno
Git (Vinzent03)2.5Mno
Style Settings2.3Mno
Kanban2.2Mno
Iconize2.0Mno
Remotely Save1.9Mno
QuickAdd1.7Mno
Minimal Theme Settings1.5Mno
Omnisearch1.4Mno
Editing Toolbar1.4Mno
Copilot1.3Mno
Importer1.2Mno
Outliner1.2Mno
Homepage1.1Mno
Recent Files1.0Mno

Outside top-20: plugins that DO use app://local

These were found via broad GitHub code search ("app://local" in Obsidian-related TypeScript/JavaScript repos). All are niche image-handling or export plugins:

PluginFile(s)PatternCategory
obsidian-image-toolkitsrc/util/markdowParse.tsURL pattern matching / parsing for image rendering(a) URL parse — reads, not constructs
oz-image-in-editorsrc/util/obsidianHelper.ts, src/cm5/, src/cm6/Builds app://local/ + basePath for inline image preview in editor(b) genuine bypass
obsidian-marp-pluginsrc/convertImage.tsResolves vault images to absolute path for Marp slide export(b) genuine bypass
obsidian-bookmastersrc/BookVault.tsLocal book file rendering(b) genuine bypass
obsidian-image-convertersrc/FolderAndFilenameManagement.tsImage path resolution during format conversion(a) URL parse

Recommendations

  • No action needed for top-20 plugins. The app://local footgun exists in theory but does not affect any high-install plugin.
  • A webview-side URL rewriter is not justified at this time. The implementation cost (intercept <img>/<video>/<audio>/ <iframe> requests matching app://local/<shadow-vault-path> and rewrite to ResourceBridge URL) is moderate, and the affected plugin set is tiny.
  • If a user reports a broken image in a specific plugin, the per-plugin fix is to check whether the plugin calls getResourcePath (works via ResourceBridge) or constructs app://local by hand (broken). The latter can be patched plugin-side or — if the plugin is popular enough — by adding the webview rewriter at that point.
  • Re-survey when the top-20 list shifts or when app://local usage increases (unlikely — Obsidian’s own getResourcePath is the documented API and most plugin authors use it).

Method

  • Same top-20 set as the basePath survey (2026-04-29).
  • GitHub code-search API: "app://local" repo:<owner>/<name>.
  • Broad search (no repo filter) also run to find outside-top-20 hits; results filtered to Obsidian plugin repos by path and context.
  • Matches in obsidian.d.ts type stubs, bundled main.js of other plugins committed to vault configs, and test fixtures were excluded.

Compat harness (#124 Phase E)

The compat harness lives at plugin/tests/compat/ and provides a duck-typed Vault + MetadataCache for plugin-driven scenario tests without a real Obsidian process. Approach B from #124 — fast, deterministic, vitest-native.

Files

FilePurpose
CompatVault.tsCompatVault, CompatTFile, CompatTFolder, CompatMetadataCache.
fixtures.tsloadFixtures(vault, dir) walks dir for .md files and seeds the vault.
fixtures/*.md4 hand-crafted markdown fixtures (frontmatter / headings / tasks / plain).
harness.test.ts11 smoke tests verifying the harness itself.

Hot duck-type surface (the contract this harness honours)

vault.getMarkdownFiles()         → TFile[] of every '.md' file
vault.getAbstractFileByPath(p)   → TFile | TFolder | null
vault.read(file)                 → text contents (Promise<string>)
vault.cachedRead(file)           → text contents (Promise<string>)
vault.create(path, data)         → TFile (Templater)
vault.modify(file, data)         → void  (Templater)
vault.createBinary(path, data)   → TFile (Excalidraw)
vault.readBinary(file)           → ArrayBuffer (Excalidraw)
vault.delete(file)               → void
vault.on / offref / trigger      → events ('create' | 'modify' | 'delete' | 'rename')
metadataCache.getFileCache(file) → CachedMetadata (Dataview, Tasks)

CachedMetadata carries frontmatter (YAML scalars: string / number / boolean / null), headings[] (level 1-6 with text), and listItems[] (checkbox tasks with task: ' ' for open or 'x' for done). Other fields from obsidian.d.ts are intentionally omitted — they’re added when a plugin scenario asks for them.

Adding a new scenario

  1. Drop a new <plugin>.test.ts next to harness.test.ts.
  2. Construct a fresh CompatVault, loadFixtures(vault, fixturesDir()).
  3. Drive the plugin-equivalent calls (e.g. for Dataview: iterate vault.getMarkdownFiles() and read each metadataCache.getFileCache(file)).
  4. Assert the expected aggregated output.
  5. If the scenario hits a vault.X method that’s not in the surface, add it to CompatVault.ts first — keep the harness adds-as-needed rather than mocking the entire obsidian.d.ts.

The roadmap in #124 covers F11 (Dataview), F12 (Templater), F13 (Excalidraw), F14 (Tasks). Each lands as a separate PR.