A self-hosted setup that turns a plain Obsidian vault into shared, persistent memory for several AI agents and every device I own, at the same time, without giving any single client exclusive ownership of the files.
I run more than one Claude agent against my own notes: one with a coding-assistant role in a terminal, one that does research and thinking, and one that acts as the vault's own curator. They all need to read and write the same notes without stepping on each other or on me editing the same files from my phone. Most personal-knowledge-base setups solve sync for one user on one app. This solves it for multiple write-capable agents plus multiple human devices, some of which can run the real Obsidian app and some of which can only reach a browser, sharing one canonical copy that nothing treats as read-only.
flowchart TD
subgraph Devices["Human devices"]
Phone["Native Obsidian app\n(phone, laptop, desktop)"]
Browser["Any browser,\nno app installed"]
end
Phone -->|LiveSync plugin,\nreal-time sync| CouchDB[(CouchDB)]
CouchDB -->|materializes to a local folder| Bridge["Syncthing\n(workstation)"]
Bridge -->|mirrors into the canonical copy| Vault["/srv/obsidian-vault\ncanonical flat-file copy"]
Browser -->|reads/writes directly, no install| SilverBullet["SilverBullet\n(web app / PWA)"]
SilverBullet --> Vault
Vault --> MCP["obsidian-mcp\n(headless MCP server)"]
MCP -->|append-only tool scoping| Agents["AI agents\n(curator + contributors)"]
Vault --> Snapshot["git snapshot\n(nightly, only on change)"]
Four things had to be true at once, and each drove a different piece:
- AI agents need programmatic access without a GUI.
obsidian-mcpis filesystem-native, not a plugin bridge into a running Obsidian process, so it works headless on a server that never opens the app. It exposes the vault as MCP tools. - Some devices run the real Obsidian app; some don't. Phone and desktop clients get real-time sync through the Self-hosted LiveSync plugin talking to CouchDB. Anything without the app, a work laptop, a borrowed machine, gets SilverBullet, a browser-based editor that reads and writes the same flat files directly. Neither path is second-class.
- LiveSync's writes need to reach the one copy agents read. CouchDB's replication
model materializes files into a local folder on whichever device runs it; it doesn't
itself deliver them to the server's flat-file copy that
obsidian-mcp, SilverBullet, and the git snapshot all read. A Syncthing instance closes that gap: it watches the LiveSync-materialized folder on a workstation and mirrors it into the server's canonical/srv/obsidian-vault, so that copy is never stale, without ever running the Obsidian desktop app (an Electron app) on the server itself. - Multiple writers need a rule, not just a lock. File-level sync tools resolve
concurrent edits with timestamps, not judgment. The actual safety comes from a written
governance model above the filesystem layer, not from the sync mechanism, see
CONSTITUTION.md.
infra/— every config as a deployable template: theobsidian-mcpsystemd unit and its two drop-ins, the Caddy reverse-proxy fix, CouchDB, SilverBullet, a Syncthing compose file, and the git-snapshot timer. Parameterized with.env.examplefiles and placeholder paths, no real hostnames or credentials.docs/SETUP.md— a device-agnostic walkthrough: deploy the server side once, then add any number of devices down either path (native app + LiveSync, or browser-only via SilverBullet), then register any number of agents under the append-only model.CONSTITUTION.md— the governance document that makes shared write access by multiple agents safe: one curator with restructuring authority, append-only contributors, provenance on every write, conflicts flagged rather than silently overwritten.
Append-only enforcement lives at the protocol layer, not agent discipline.
obsidian-mcp's OBSIDIAN_TOOLS environment variable can deny specific tools per
instance. Contributor agents run with !note_write,!note_delete,!note_move denied,
so they can create, insert, and patch, but cannot overwrite or delete a whole note,
whatever their system prompt says. The rule is enforced by what the process can call,
not by asking the model nicely.
Two sync mechanisms, deliberately, not one compromise. CouchDB/LiveSync gives native apps real-time, conflict-aware sync built for exactly this use case. Syncthing's only job is bridging that into the server's own copy. Running one general-purpose sync tool for both would have meant picking between LiveSync's plugin-only reach and Syncthing's lack of a real per-note merge model, instead of getting each tool's actual strength.
A privacy layer that doesn't require the desktop app. A personal journal folder
needs to stay out of what agents can discover, but the whole design goal was avoiding a
GUI dependency. OBSIDIAN_EXCLUDE_PATHS gives an index-level exclusion, confirmed via
the server's own vault_info tool reporting a non-zero excluded-note count, rather than
a filesystem ACL that would need the app running to define.
Read-only browser access gets the same files, not a converted copy. SilverBullet reads the same Markdown that Obsidian and the agents use. No format migration, no export step, no second source of truth to keep in sync with the first.
Three real failures, kept because the actual operating experience is better evidence than a clean demo.
The reverse proxy 403'd for no visible reason. obsidian-mcp is built on the rmcp
Rust SDK, which validates the inbound Host header against a hardcoded allowlist
(localhost, 127.0.0.1, ::1) as DNS-rebinding protection, with no flag to extend it.
Any proxy that forwards the original external Host header gets rejected. The fix was a
small Caddy instance sitting between the public-facing proxy and obsidian-mcp, whose
only job is rewriting the Host header back to loopback before forwarding
(infra/caddy/Caddyfile).
The service was quietly eating a full CPU core. Load average sat around 3.3 on a
4-core box with nothing obviously wrong in the logs. The cause: the filesystem watcher
was indexing its own index directory, a self-triggering re-index loop, compounded by
watching churny dotfolders (.git/, .stfolder/) that change constantly without
representing real content. The first fix widened the exclude list
(cpu-fix.conf); on reflection the watcher wasn't
buying anything this deployment needed, since agent access goes through MCP tool calls
and the only external writer is already-synced files landing from Syncthing, so a second
pass disabled it outright
(no-watch.conf). Load average dropped to ~0.06,
reclaiming roughly a quarter of the box.
LiveSync failed for hours with a misleading error. "invalid UTF-8 JSON" and "failed to obtain PBKDF2 salt" pointed at CORS, HTTP version negotiation, and the sync passphrase itself, none of which were the problem; all were tested and ruled out first. The actual cause only showed up in the client's own dev console: the plugin's "Database URI" field must be the bare host with no path, and the database name goes only in the separate "Database name" field. Putting the database name in both produced a malformed nested path that CouchDB's own document routing turned into a generic 400 instead of a clear error. Once diagnosed, first sync ran clean end to end.
One canonical copy, not a distributed database. /srv/obsidian-vault on one server
is the single source of truth. Git snapshots give rollback history and Syncthing/LiveSync
give live redundancy across devices, but there's no automatic failover if that one
server goes down. Acceptable for a personal vault; not the design to reach for at team
scale.
Conflict resolution is timestamp-based, not semantic. Syncthing and LiveSync both
resolve concurrent edits by last-write-wins plus a .sync-conflict-* file, not a real
merge. Fine for a personal vault where simultaneous edits to the same note are rare;
would need a different approach for genuinely concurrent multi-writer editing.
Semantic search is deliberately not part of this. Keyword search is sufficient at this vault's size. If that changes, the plan is to run embeddings locally rather than send vault content to a cloud API, but that's not built.
CONSTITUTION.md's first principle draws a line: judgment goes in shared memory,
mechanics stay local to whichever tool executes them. This repo is everything on the
memory side of that line, where facts live, who can write them, how they reach every
device and agent. It stops there on purpose. The other side, how a single agent turns a
fact from this vault into a correct action without its own reasoning errors compounding
across multi-step work, is a separate architecture (short version: probabilistic
reasoning plans, deterministic scripts execute), and it deserves its own writeup rather
than a section bolted onto this one.
obsidian-mcp (Rust), CouchDB 3.3, the Self-hosted LiveSync Obsidian plugin,
Syncthing, SilverBullet, Caddy, systemd, git. Deployed on a self-managed Linux server,
Tailscale for private network access, no public ports anywhere in the chain.