cd /news/ai-agents/obsidian-vault-on-a-raspberry-pi-obs… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-133267] src=gist.github.com β†— pub= topic=ai-agents verified=true sentiment=↑ positive

Obsidian vault on a Raspberry Pi: obsidian-headless sync + FastMCP server for Claude (Tailscale Funnel, GitHub OAuth)

A developer built a two-container setup on a Raspberry Pi that keeps a live Obsidian vault synced via the official obsidian-headless client while exposing it to Claude through a roughly 100-line FastMCP server with five tools (list_notes, read_note, search_notes, write_note, append_note). Access is gated by GitHub OAuth with a single-user allowlist, untrusted folders are read-only to the model, and the phone's Claude app reaches the server over a public HTTPS URL via Tailscale Funnel.

by read6 min views14 publishedSep 9, 2026

Two containers keep a live copy of the Obsidian vault on the Pi and let Claude read and write it:

  • obsidian-sync β€” the officialobsidian-headless client (ob sync --continuous ) in a community arm64 image. It joins Obsidian Sync as one more device, so the Pi's copy is always current, and anything Claude edits on the Pi shows up on the laptop and phone within seconds.
  • obsidian-mcp β€” a ~100-line FastMCP server (obsidian-mcp/server.py ) with five tools (list_notes ,read_note ,search_notes ,write_note ,append_note ) over that same folder. Login is GitHub OAuth, and only the GitHub user inMCP_ALLOWED_GITHUB_LOGIN gets through.MCP_UNTRUSTED_FOLDERS (web clippings, third-party text) are read-only for Claude and search returns no text from them, so a clipped page that reads like an instruction can't ride into a chat unseen. Claude Code on the laptop and the Claude app on the phone both reach it over HTTPS via Tailscale.obsidian-mcp/README.md has the full story: tools, login, connecting each Claude, revoking access, troubleshooting.

The vault lives on the Pi's SD card at config/obsidian/vault, not on the NAS: it's ~50 MB, inotify doesn't fire over NFS (so continuous sync would miss edits made on the Pi), and Obsidian Sync already keeps version history, so backup.sh ignores it on purpose. .obsidian/ (plugin state, workspace layout) is hidden from the MCP tools.

laptop: Claude Code  ──tailnet──▢  https://mypi….ts.net/mcp  ──▢ obsidian-mcp ──▢ vault ◀── obsidian-sync ◀──▢ Obsidian Sync
phone:  Claude app   ──▢ Anthropic servers ──public (Funnel)──▢  (same URL)                                       β–²
phone:  Claude app   ──▢ Anthropic relay  ──▢ `claude remote-control` running ON the Pi in the vault folder β”€β”€β”€β”€β”€β”˜

The Claude app never talks to an MCP server itself β€” Anthropic's servers do, on its behalf. A tailnet-only URL is invisible to them, so the phone connector needs Tailscale Funnel (a public HTTPS URL). The GitHub login + allowlist is what actually guards the notes: the URL alone gets a stranger a GitHub consent page and then "not allowed". (To take it off the public internet later: sudo tailscale funnel --https=443 off, then sudo tailscale serve --bg --https=443 127.0.0.1:8100 for tailnet-only, and use Remote Control for the phone.)

Obsidian Sync token β€” on the Pi, interactive (asks email, password, 2FA code). Run thisbefore the first deploy: it also createsconfig/obsidian/{vault,sync} aspi , which the sync container needs (see Gotchas).

make notes-token

Put it in .env asOBSIDIAN_AUTH_TOKEN , withOBSIDIAN_VAULT_NAME (the vault's name in Obsidian β†’ Settings β†’ Sync) andOBSIDIAN_VAULT_PASSWORD (its end-to-end password). See.env.example . 2. GitHub OAuth app β€” github.com β†’ Settings β†’ Developer settings β†’ OAuth Apps β†’ New:

  • Homepage URL: https://mypi.your-tailnet.ts.net
  • Authorization callback URL: https://mypi.your-tailnet.ts.net/auth/callback Put the Client ID + a generated client secret in .env (MCP_GITHUB_CLIENT_ID ,MCP_GITHUB_CLIENT_SECRET ), plusMCP_JWT_SIGNING_KEY=$(openssl rand -hex 32) .
  1. Homepage URL:

Deploy from the laptop (./deploy.sh ), then watch the first sync pull the vault down:

make logs svc=obsidian-sync     # first run: sync-setup, then "watching for changes"
ls config/obsidian/vault

Public HTTPS via Tailscale Funnel β€” on the Pi, once. NeedsMagicDNS andHTTPS certificates turned on in the Tailscale admin console (DNS page) first.

make notes-funnel     # = sudo tailscale funnel --bg --https=443 127.0.0.1:8100

The first time, it prints a login.tailscale.com/f/funnel?node=... link and thenwaits (it looks stuck). Open the link, click Allow, and it finishes by itself. Thenwait five minutes before connecting anything: the name only enters public DNS once Funnel is on, and resolvers that asked earlier cache "no such name" for that long. This is host state, not in git: after a fresh OS install, run it again. Check from anywhere (phone on 4G is a good test):

curl https://mypi.your-tailnet.ts.net/.well-known/oauth-authorization-server   # JSON = alive
claude mcp add --transport http --scope user obsidian https://mypi.your-tailnet.ts.net/mcp

--scope user = available in every project, not just the folder you ran it in. Then inside Claude Code: /mcp β†’ obsidian β†’ Authenticate β†’ GitHub login in the browser.

Connectors belong to the account, so add it once and every app has it. Mac app or claude.ai β†’ Settings β†’ Connectors β†’ Add custom connector β†’ URL https://mypi.your-tailnet.ts.net/mcp β†’ Add β†’ Connect β†’ GitHub login. Then in any chat (Mac, web, or phone): + β†’ Connectors β†’ switch obsidian on, per chat. Step-by-step with the phone specifics: obsidian-mcp/README.md.

Claude Code can run on the Pi in the vault folder and be driven from the phone or claude.ai/code β€” the full agent, editing files directly. No MCP, no Funnel, works on Pro/Max.

curl -fsSL https://claude.ai/install.sh | bash     # native arm64 build
sudo apt install -y tmux
claude                                             # then /login (claude.ai account), then exit

make notes-rc                                      # then: tmux attach -t notes-rc, scan the QR

The session shows up in the Claude app under Code. Edits land in config/obsidian/vault as pi and obsidian-sync pushes them to Obsidian Sync straight away. --permission-mode default is set on purpose: every edit and every command asks for a tap. This session has none of the MCP's guards (no writable-folder rule, no hidden-character filter), it can write anywhere in the vault, and it reads the vault's CLAUDE.md as instructions. The tap is the guard. See Prompt injection.

Clipped web pages and pasted transcripts land in the vault, and a note whose text reads like an instruction can steer a later Claude turn (that's why MCP_UNTRUSTED_FOLDERS are read-only and text-less in search). ./scan-vault.py runs on the Pi once a day and looks in the usual hiding places: invisible Unicode, HTML comments, CSS-hidden text, chat-template markers, "ignore previous instructions" phrasing, long encoded blobs. No AI in the loop, on purpose: a checker that reads the notes with an LLM can be tricked by the very text it is looking for.

The MCP handles the hidden-text trick itself at both edges: invisible Unicode is stripped from what Claude reads and refused in what it writes, and chat-template markers are refused on write. The scan is for what those can't catch: visible text that reads like an order.

It writes Scratchpad/Injection scan.md (note, line number and a label; never the text, so the report can't carry a payload into a chat) and Obsidian Sync brings it to the laptop. Expect a handful of harmless hits (clipped articles, templates, notes about AI); look at anything you don't recognise.

make scan                          # run now; exit 1 if anything was flagged
./scan-vault.py --self-test        # the built-in check

Cron, as pi (crontab -e):

10 4 * * *  /home/pi/pi-nas/scan-vault.py >> /home/pi/pi-nas/scan.log 2>&1   # daily 04:10
  • Two writers, one file. Claude on the Pi and you on the laptop editing the same note at the same moment β†’CONFLICT_STRATEGY=merge (Obsidian's default) merges line by line rather than forking the file. Version history in Obsidian β†’ Sync has every prior state.
  • Changed the MagicDNS name? It's in four places:BASE_URL indocker-compose.yml , the GitHub OAuth app's callback URL, every connected Claude (re-add the connector), and theobsidian-mcp health link inpi-dashboard/public/index.html (WEBUI map).
  • New MCP_JWT_SIGNING_KEY = every Claude has to log in again. Same if you remove the<project>_obsidian-mcp-config Docker volume (it holds the OAuth registrations + tokens; it's a named volume, not./config/ , because it must be owned bypi and Docker creates missing bind-mount dirs as root).
  • Vault password wrong β†’ obsidian-sync exits at start with a decryption error; checkmake logs svc=obsidian-sync . It's the vault's own E2E password, not your Obsidian account one.
  • obsidian-sync dies at "Logging in" with a permission error β†’config/obsidian/sync is owned by root. Docker creates missing bind-mount dirs as root, and the image only fixes ownership whenPUID differs from its built-in 1000 (ours is 1000). Fix:sudo chown -R pi:pi config/obsidian && make restart svc=obsidian-sync .make notes-token pre-creates the dirs so a normal first run never hits this.
── more in #ai-agents 4 stories Β· sorted by recency
── more on @obsidian 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/obsidian-vault-on-a-…] indexed:0 read:6min 2026-09-09 Β· β€”