Remote SSH

by sotashimozono
5
4
3
2
1
Score: 58/100

Description

Edit Remote Vaults over SSH/SFTP from Obsidian — like VS Code Remote-SSH

Reviews

No reviews yet.

Stats

45
stars
1,801
downloads
7
forks
115
days
0
days
7
days
545
total PRs
2
open PRs
54
closed PRs
489
merged PRs
52
total issues
7
open issues
45
closed issues
1,179
commits

Latest Version

7 days ago

Changelog

✨ Features

  • e2e: Put the E2E environment in a container, and fix what that found(f203723)
  • rpc: Notice a daemon that stops answering without the wire dropping(4360901)
  • ssh: Name the route to the target, and write down what it owes(2ca7601)

🐛 Bug Fixes

  • test-env: Close the silent failures review found in the tailnet harness(cc449ad)
  • test-env: Start-tailnet crashed on its own stderr capture; quiet the RST on Windows(7e768d5)
  • e2e: Drop the right sshd when the reconnect spec simulates a drop(78c533b)
  • rpc: Notice when the daemon dies, and when we hang up on it(627c4b2)
  • ssh: Keep the reason a connection died, instead of discarding it(25a5826)
  • rpc: Use window timers in the heartbeat, as Obsidian's lint rule asks(6ae8a93)
  • test-env: Stop leaving 300 MB behind, and stop going red on a mirror(437a371)
  • adapter: Rebind the path prefix with the client, not just the client(75d4279)
  • ssh: A bastion whose target dies left the session looking live(27d5607)
  • e2e: Let the containerised tailnet run build the daemon(59eca89)
  • test: Stop the fs.watch tests crashing the Windows worker(97088fb)
  • test: Take the native watch handle out of the unit suite(b15c696)
  • test: Put the reader harness through the real watch filter(df73891)
  • ui: One notice per disconnect, and say what died(6d38095)
  • rpc: Actually notice a daemon that stopped answering(16a2922)
  • daemon: Restart without announcing a connection loss(0d9a30a)
  • rpc: Decide ownership by identity, not by a window in time(2279963)
  • rpc: Put a deadline on the handshake(f140ec7)
  • rpc: Judge liveness by progress, not by a pending count(72da333)
  • rpc: An abandoned call is not the daemon's fault(2f452f1)

📚 Documentation

  • comments: Cut 96 comment lines I wrote yesterday, and write down the bar(eda4ec1)
  • Reattach documentation that documents nothing(84685f3)
  • shadow: Compress the round-trip commentary, and drop what the split made false(0a6b3cb)
  • Compress four files, and correct five things that were untrue(5f33126)
  • A second, harsher pass over five files(2bd7cb6)
  • Name a function that exists, and put back a reason I deleted(3b54de5)
  • Ten comments that were not true of the code beside them(f01a5c7)

🔧 Refactor

  • shadow: Split ShadowVaultBootstrap into what it actually does(774fcbc)
  • shadow: Lift the post-connect config sync out of runAutoConnect(c9c0c86)
  • adapter: Make patch() reachable, then reach it(8cb5084)
  • vault: Let renameOne name its own steps(51c2719)
  • daemon: Move ensureDaemonBinary out of main.ts(8023bcf)
  • reconnect: Move the reconnect decision somewhere testable(24ec76b)
  • shadow: One atomic write, and one segment sanitiser(25b796f)
  • main: Lift the command palette out of onload(39ddf79)
  • rpc: Stop handing out the ability to hang up(f5fc9ab)
  • reconnect: A notice helper, not a decision object(b4194f5)

🚧 CI

  • e2e: Run the core specs over the tailnet, manually, to prove the wiring(950b6cd)
  • coverage: Report what the integration suites reach(f6873fd)
  • e2e: Gate the containerised tailnet run on promotion into main(6bd2a24)

🧪 Tests

  • Run the suites over a private tailnet as a second environment(802809e)
  • rpc: Pin that the heartbeat is started, and stopped on disconnect(1cbe01d)
  • ssh: Lift the connection lifecycle out of connect(), and hold it to something(fe1e561)
  • transport: Run the jump contract only where the bastion is reachable(daff7d4)
  • ssh: Cover both ways a jump tunnel dies(e61ab99)
  • adapter: Drive patch() for the first time(48301fd)
  • shadow: Reach the watcher the real run uses(cd1772f)
  • shadow: Close the last gaps in the config-sync module(6577d9f)
  • daemon: Cover the paths the move made visible(df3de64)
  • daemon: The execute bit is a POSIX claim(c65278c)
  • adapter: The close bit, the bridge fetcher, and one method to delete(91a29a1)
  • shadow: Make the fs.watch adapter reachable without a handle(6b9a5e4)
  • shadow: Cover the watch opener by injecting it, not by opening one(d9873d1)
  • logger: Exercise log rotation, which nothing had(68e4d63)
  • Reach six functions no suite had ever executed(5fbf1dc)
  • Reach the code that decides where offline writes live(6c2a25c)
  • Exercise the quiet death, not just the loud one(f730ea8)
  • Run the image path instead of asserting about a mock(454b112)
  • Pin the heartbeat's intervals and its pending-count wiring(18d0660)
  • Assert which callback, which params, and what teardown leaves(fbde0b8)
  • Drive the reconnect from the plugin, where the wiring lives(7f34b25)

✨ Features

  • ssh: Name the agent identities the plugin had to skip (#536)(72d91fc)
  • ssh: Authenticate with an agent-held OpenSSH certificate (#536)(3e33248)

🐛 Bug Fixes

  • adapter: Bound concurrent reads and park them across a reconnect (1.1.9-beta.2)(7c6e52c)
  • ssh: Strip the signature algorithm for plain agent keys(c6ce9d3)
  • e2e: Three harness defects that have never actually worked(e38abba)
  • adapter: Park a read across a reconnect, and drop the in-flight cap(be1ef22)
  • ssh: RSA certificates never authenticated, and signing had a 2s cap(d568996)
  • adapter: Wake reads parked on an adapter being discarded(1e9036e)
  • adapter: Retiring an adapter is not the same as being connected(0e2753b)
  • ssh: Agent auth must not depend on ed25519 being available(9d8c9b4)

📚 Documentation

  • user-guide: 50k notes hangs Obsidian, it does not merely take long (1.1.9-beta.0)(5b86fb4)
  • adapter: Two comments that stopped being true(3690832)

🧪 Tests

  • integration: Reproduce the cache-overflow regime without a 5 GB vault(0047323)
  • e2e: Assert the fixture's preconditions, and say where connect stopped(4eff1f6)
  • e2e: Drive connect the same way everywhere, and keep the server's log(9243133)
  • e2e: Refuse to run against a vault with no plugin in it(ca178e4)
  • ssh: Cover the two branches the disposal fix left bare(923f4e8)
  • ssh: Exercise the fallthrough the first version only pretended to(11e02d9)
  • ssh: Cover the two remaining ways a sample key can be useless(f30fdd6)

🧹 Chores

  • version: 1.1.9-beta.3(2fd30bc)
  • Drop the stray Quartz LICENSE.txt from the repo root(c4a2094)
  • version: 1.1.9-beta.5(e9261f2)
  • version: 1.1.9-beta.6(e24f498)

⏪ Reverts

  • adapter: Back out the raw-fs #429 work (#481–#488) (beta.14)(1ef4f88)
  • vault,shadow: Drop the exclusion Notice and config-dir detection(3be7b7e)

⚡ Performance

  • rpc: Send each frame in one write on both ends (beta.21)(2e46d2f)

✨ Features

  • vault: Allow selected dot-folders per SSH profile(a797515)
  • vault: Restore the remote tree at startup so Obsidian keeps its index (#513) (beta.20)(0406b39)

🐛 Bug Fixes

  • e2e: Seed the scale fixture with docker cp, not onto the bind mount(28450fc)
  • e2e: Give a vault the same id on every launch(0ccfae6)
  • e2e: Stop the probes reporting failures as fast successes(4629d26)
  • vault: Never reconcile against a walk that degraded or missed a folder(fd288e6)
  • vault: Say when the ignore list hid something, instead of just hiding it(c7bf1a7)
  • shadow: An unreadable plugin list is not an uninstall(77f4f2c)
  • vault: Restore the tree without freezing startup, under current settings(762ddf1)
  • shadow: Use the config dir the shadow vault actually has (beta.25)(4abaed4)

📚 Documentation

  • user-guide: What vault size this actually handles (beta.27)(39de7a1)

🚧 CI

  • go: Build with Go 1.27 (beta.15)(90716ef)
  • go: Hold at Go 1.26 so macOS 12 remotes keep the daemon(eab0dff)

🧪 Tests

  • vault: Cover reconnect indexing of allowed dot-folders(7885f25)
  • e2e: Check smoke 2/3 against Obsidian's registries(a3a64d5)
  • e2e: Pin #519, a root hub link into an unexpanded sub-repo (beta.17)(73909b9)
  • e2e: Scale benchmark for big vaults over a shaped link (#513) (beta.18)(ad67297)
  • e2e: Time every vault.readBinary in the scale benchmark (beta.19)(4147129)
  • e2e: Report what metadataCache kept through startup in the scale runs(8005c86)
  • e2e: Close Obsidian instead of signalling it between scale passes(f73acff)
  • e2e: Track metadataCache fileCache size and hashes per sample(1767127)
  • e2e: Report app.appId and the IndexedDB databases per launch(ea0a9fe)
  • e2e: Time the adapter directly, outside Obsidian queue(5e25307)
  • e2e: Add timer controls to the adapter probe(7bec1a8)
  • integration: Time one RPC round trip from plain Node (#513)(0bea3a4)
  • shadow: Cover the unreadable and missing plugin-list paths(d35cf9e)

README file from

Github

Remote SSH for Obsidian

Edit an Obsidian vault that lives on a remote server — over plain SSH. Think of VS Code's Remote-SSH, but for Obsidian: open a vault that sits on your home server, VPS, work box, or cluster, and edit it from a real Obsidian window. Files, attachments, search, and live updates — all served straight from the remote, over your own SSH connection.

No cloud. No sync service. No full copy left on your laptop. Your notes stay on the machine you control; this one only ever holds what you're actively looking at.

Demo: connect to a remote vault and edit it live

End-to-end demo, recorded in CI by demo-capture.yml — connect to a remote host and edit its vault from a normal Obsidian window.


Install

Obsidian → Settings → Community plugins → Browse → search "Remote SSH" → Install → Enable.

That's the whole install. The first time you connect with the (recommended) RPC transport, the plugin fetches the matching helper daemon from its signed GitHub release — with a one-time confirmation, verified by sha256 — and uploads it to your host for you. Prefer no remote-side binary? Stay on the SFTP transport and skip the daemon entirely.

BRAT (early/beta builds)

BRAT auto-installs and auto-updates pre-release builds straight from this repo:

  1. Install Obsidian42 - BRAT from Community plugins → Browse.
  2. BRAT settings → Add Beta plugin → paste sotashimozono/obsidian-remote-ssh → leave version blank.
  3. Toggle Remote SSH on under Community plugins.

Manual

Download main.js, manifest.json, and styles.css from the Releases page, drop them into <your-vault>/.obsidian/plugins/remote-ssh/, and enable the plugin. (The daemon still auto-downloads on first RPC connect; for an air-gapped remote, grab the matching obsidian-remote-server-<os>-<arch> binary from the same release and place it in …/remote-ssh/server-bin/.)


Why you'd want this

You already keep your notes — or your code, or your research — on a machine that isn't this one. A home server. A cloud VPS. The lab cluster. You SSH in all the time. You'd love to use Obsidian on those files without copying the whole vault down to every laptop and phone, and without handing it to a sync provider.

That's the entire point of this plugin:

  • 🔑 Plain SSH, your keys, your config. Password, private key, SSH agent, or a ProxyJump bastion — all read from the same ~/.ssh/config your terminal already uses. No account to create, no third-party server to trust.
  • ☁️ No cloud middleman. Files move directly between this machine and your host over the encrypted SSH tunnel. Nobody else ever holds your notes — and there's no telemetry or analytics; nothing leaves your machine except to the host you typed into a profile.
  • 💻 No full local replica. Sync tools mirror your entire vault onto every device. This doesn't: the vault stays on the server, and your laptop only fetches the files you actually open (cached locally while you work, not a synced second copy of everything).
  • 🧩 It's still Obsidian. The remote vault opens in a normal Obsidian window — real File Explorer, search, command palette, and most of the plugins you already use (Dataview, Templater, Excalidraw, Tasks, …).

What you can do with it

  • Edit a remote vault as if it were local. SSH into your server; the vault stays there, you get the full Obsidian editing experience here. No manual rsync, no Dropbox dance.
  • Keep huge vaults where they belong. A 50k-file research vault or a media-heavy attachment vault doesn't need to land on every device — only the files you open are fetched.
  • Let the server stay the source of truth. Cron jobs, scripts, an LLM pipeline, or your own git can keep operating on the canonical files; you're editing the same files, not a copy that races those writers.
  • Work from several machines. Live updates push between connected clients via fs.watch; if two saves collide, a 3-way merge UI (ancestor / mine / theirs) opens instead of silently clobbering.
  • Survive flaky networks. Disconnects spool your writes to an offline queue and drain them on reconnect; the status bar shows the pending count.
  • Use a remote-side terminal. An integrated terminal pane runs on the remote, right next to the notes it's about.

Good fits

  • A homelab / VPS where your notes already live, and you want them to stay there.
  • A research / work cluster (e.g. an HPC login node) holding data and notes you SSH into anyway.
  • Vaults that are too large or too private to replicate everywhere or to hand to a managed sync service — for legal, privacy, or compliance reasons.

Quickstart

About 3 minutes if you already have an SSH host.

  1. Add a profile. Settings → Remote SSH → + Add. Enter host, port, username, auth method (privateKey / password / agent), transport (RPC recommended), and the remote vault path (relative paths resolve under $HOME — notes/main → ~/notes/main).
  2. Click Connect on the profile row, or run Remote SSH: Connect to remote vault from the command palette.
  3. A new Obsidian window opens — your "shadow vault". Same UI you know, but every file in it lives on the remote. Start editing.

To leave: close the shadow window, or run Remote SSH: Disconnect inside it. Your original window is never touched.

First time connecting a brand-new profile? Obsidian only learns about the new vault at startup, so the very first connect may need one restart of Obsidian before the window appears — then reconnect. It's a one-time step per profile; every later connect opens immediately.


Features

Feature Notes
🪟 Opens in its own Obsidian window The window you started from is untouched; the remote vault is a first-class window with its own File Explorer, search, and command palette.
⚡ Sub-second cold-open, even for 10k-file vaults A single fs.walk RPC fetches the whole tree in one round-trip (per-folder fs.list fallback for SFTP).
🖼️ Image / PDF / video rendering A local-only HTTP bridge (random localhost port + bearer token) streams binary content to the Obsidian webview; images go through a fs.thumbnail cache (RPC transport).
🔁 Live multi-client sync fs.watch notifies every connected client when another writer changes a file; explorer + open editors update within ~1 s.
🪢 3-way conflict resolution If the remote file moved under your edit, a merge modal opens with ancestor / mine / theirs panes (plain text; binaries fall back to a 2-choice modal).
📥 Offline write queue Writes during a disconnect spool to a local queue and drain automatically on reconnect; the status bar shows the pending count.
🩹 Automatic reconnect with backoff SSH drops trigger a retry loop (default 5, exponential backoff to 30 s); reads come from cache during retries, writes spool to the queue.
🔌 Jump host / ProxyJump Multi-hop SSH through bastions, from the same ~/.ssh/config your terminal uses.
🔐 Signed daemon binaries Every release daemon is Sigstore-cosign-signed; the plugin also runs a sha256 round-trip check on every deploy and refuses a mismatch.

How it compares (the short version)

Most "edit my vault from anywhere" options copy the vault around — Obsidian Sync (managed, encrypted, on Obsidian's servers), file-sync tools (Syncthing / Dropbox / iCloud), or obsidian-git (a git repo you pull/push). Each is great when replicating the vault is what you actually want.

This plugin is for the opposite case: the vault should live only on a remote host you control, with no second copy and no third party — and you want to edit it in place. If that's your situation, this is the niche it's built for; if not, one of the above is simpler.

Full breakdown (when to pick which): docs → comparison.


What stays on your computer

By design, the canonical vault never leaves your SSH host. On the local side:

Capability Why it's needed Scope
Network The SSH/SFTP connection Only the host / port / optional jump host in your profile
Filesystem (outside the vault) Read your SSH key + ~/.ssh/config to authenticate; cache the daemon binary ~/.ssh/* and the plugin's own server-bin/ cache only
System identity Sensible defaults for the client id + remote username os.hostname() / os.userInfo().username (both overridable); SSH_AUTH_SOCK etc. to find your agent/config
Process execution Start the daemon on the remote over SSH Remote host only — no local shell execution
Clipboard (write) The "Copy diagnostics" button Only on explicit click; the clipboard is never read

No telemetry, no analytics. Locally you keep only a working cache of the files you've opened — never a full replica of the vault.


Security

Daemon binaries are signed with Sigstore cosign (keyless OIDC); verify any release binary yourself with the one-line cosign verify-blob in SECURITY.md. On every deploy the plugin re-checks the uploaded binary by sha256 and refuses to start a mismatch.

To report a vulnerability, use a private GitHub Security Advisory — policy in SECURITY.md. Please don't open a public issue for security bugs.


Plugin compatibility

The shadow vault patches app.vault.adapter, so plugins that go through Obsidian's vault API work transparently — Dataview, Templater, Daily Notes, Tasks, Calendar, Excalidraw (RPC transport), and most others. Plugins that bypass the adapter (importing Node fs directly, e.g. Omnisearch's indexer) see an empty local directory instead of your remote vault.

Full matrix: docs → plugin compatibility.


Troubleshooting

The console log is the first thing to check (JSONL, one event per line):

~/.obsidian-remote/vaults/<profile-id>/.obsidian/plugins/remote-ssh/console.log
jq 'select(.level == "error")' console.log    # just the errors
tail -20 console.log | jq -c '{ts, level, msg}'

A few common ones:

  • No new window opens after connecting a new profile — Obsidian only registers new vaults at startup. Fully quit and reopen Obsidian, then Connect again (one-time per profile; tracked in #411).
  • Shadow window opens but File Explorer is empty — the auto-connect failed. Check the shadow window's console log and run Remote SSH: Reconnect from the command palette.
  • Images / PDFs don't render — those need the RPC transport; check the profile's transport setting.
  • Connect succeeds but the daemon won't come up — the session falls back to SFTP (vault read/write still work; daemon-only features are off). See the daemon log: ssh <host> 'cat ~/.obsidian-remote/server.log'.

More: docs → troubleshooting.


How it works (technical)

Obsidian doesn't expose a public way to rebuild the vault model from a different storage adapter mid-session, so the plugin opens a separate shadow window whose vault is constructed from the remote tree at startup — every plugin in that window sees a normal-looking vault from frame zero.

flowchart LR
    subgraph Original["Original window"]
      U[Click Connect on profile P] --> M[ShadowVaultManager]
    end
    M --> B["ShadowVaultBootstrap<br/>materialise ~/.obsidian-remote/vaults/&lt;P&gt;/<br/>install plugin · seed autoConnectProfileId=P"]
    B --> R["ObsidianRegistry<br/>register vault in obsidian.json"]
    R --> W["WindowSpawner<br/>obsidian://open?path=…"]
    W -.spawn.-> L
    subgraph Shadow["Shadow window"]
      L[onLayoutReady → connect to P] --> P[Patch app.vault.adapter → remote FS client]
      P --> WK[fs.walk RPC / fs.list fallback]
      WK --> V[VaultModelBuilder → File Explorer]
    end
    subgraph Transport["SSH transport"]
      P -. fs ops .-> RPC[RpcRemoteFsClient]
      P -. or .-> SFTP[SftpRemoteFsClient]
      RPC --> D[(Go daemon · framed JSON-RPC over a unix-socket stream)]
      SFTP --> SSH[(ssh2 SFTP)]
      D --> FS[(Remote vault files)]
      SSH --> FS
    end

Two transports per profile:

  • RPC (recommended). A small Go daemon (obsidian-remote-server) runs on the remote; the plugin auto-fetches the signed binary, uploads it, and starts it. Vault FS ops flow as length-framed JSON-RPC over a forwarded unix socket. Required for image/PDF/video rendering, sub-second cold-open, thumbnails, and live fs.watch updates.
  • SFTP. Direct SFTP over ssh2, no remote-side install — loses the daemon-only features above.

Design deep-dives: shadow-vault architecture · performance · conflicts & offline queue.


Contributing

Contributions welcome — dev setup, branch + commit conventions, and how to run the test suite are in CONTRIBUTING.md. Bugs and feature requests: Issues.

Inspired by VS Code's Remote-SSH; the wire format is LSP-style framed JSON-RPC over a unix-socket-forwarded stream — the same shape language servers use, just for filesystem ops.


Support

Remote SSH is, for now, maintained by a single individual. Sponsoring it helps sustain the continued maintenance and ongoing issue responses over time, which genuinely matters for a one-person project. Hugely appreciated 🙏

(A donate link also lives right in the plugin's settings page.)


Project status

Released and available in the Obsidian Community Plugins store. Daemon binaries are cosign-signed and end-to-end tested against Linux + macOS remotes.

release downloads docs (stable) License: MIT

CI Integration Security codecov Docs docs (dev preview) Obsidian 1.5+ Go 1.25+ Node 22.22+ Platforms: linux · macOS · amd64 · arm64