Pattern for editing an Obsidian vault from Claude Code on the Web — web UI or mobile — while the Mac is off, using Obsidian Sync as the single source of truth.
Yes, that’s two em-dashes in this intro!
Wanna be able to spin up a Claude Code remote session and have it access my Obsidian vault.
Yes.
Cloud sessions need a git repo to clone. The vault isn’t in git, and shouldn’t be. Because I don’t want it in git. Because I’m using Obsidian Sync already. Because I don’t want to mess around with troubleshooting the sync-via-git from mobile. Because :scream:
So, we need to sync the vault on a remote environment.
Well, yes, and no. The CLI that comes with the Obisidian app needs the Obsidian app to be running. But there is also obsidian-headless that does exactly what we need.
BUAAHAHAAAHA HAA HAA HAA :D :) :| :’( :’O
Sure! Claude-generated content below.
Trigger warning, though: 100% unsupervised patching of Node internals to work with a HTTP proxy! I am looking away, for sure!
Anyways:
A minimal bootstrap repo, separate from the vault, containing only cloud-session wiring:
SessionStart → ob sync --path ./vault (blocking pull)
Claude works on ./vault/
Stop → ob sync --path ./vault (blocking push)
obsidian-headless (official npm package) does the sync — Node CLI, no desktop app, works on Linux. Credentials are injected via per-repo env vars in the cloud session config, which sync to mobile automatically.
Claude Code cloud sandboxes route all traffic through an HTTPS proxy (HTTPS_PROXY is set). Node 22’s built-in fetch and WebSocket both use an internal bundled undici that ignores HTTPS_PROXY entirely and cannot be patched from user-land. The dispatcher lives at node:internal/deps/undici/undici — not the public undici package, a separate instance.
ob login fails at DNS resolution (EAI_AGAIN). ob sync connects but hangs indefinitely on the WebSocket.
A preload script, loaded via NODE_OPTIONS=--require, replaces both globals before ob loads:
// fetch — npm undici with ProxyAgent
const { ProxyAgent, setGlobalDispatcher, fetch: undiciFetch } = require(root + '/undici');
const dispatcher = new ProxyAgent(proxyUrl);
setGlobalDispatcher(dispatcher);
globalThis.fetch = (url, opts) => undiciFetch(url, { ...opts, dispatcher });
// WebSocket — ws@8 + https-proxy-agent@5 (v6+ is ESM-only)
const WS = require(root + '/ws');
const { HttpsProxyAgent } = require(root + '/https-proxy-agent');
globalThis.WebSocket = class ProxiedWebSocket extends EventTarget { ... }
obsidian-headless captures var gr = WebSocket at module load time, so replacing the global before require('/path/to/ob') is enough — no patching of the module itself.
The WebSocket shim translates between ws@8’s Node-style event emitter API and the browser WebSocket API that obsidian-headless expects: isBinary flag for text/binary frame distinction, Object.assign(new Event('close'), { code, reason }) in lieu of CloseEvent (not available in Node), and direct on* property setters.
/ The Crap, Cut
I COULDN’T! So I asked Claude the magic question:
What version of Node are we using? Is there a newer version that supports HTTP_PROXY?
🦄🌈✨🦄✨🌈✨🦄🌈✨🦄🦄
v22.19.0+
ob CLI flags differ from what the README implied at time of writing:
| Wrong | Correct |
|---|---|
ob sync-setup --vault <local> --remote-vault <name> |
ob sync-setup --path <local> --vault <name> |
ob sync-setup --vault-password |
ob sync-setup --password |
ob sync --vault |
ob sync --path |
Auth token lives at ~/.config/obsidian-headless/auth_token, not ~/.obsidian-headless/config.json.
Nothing, it works. Here, touch some grass 🌱🍀🌾🌾🪲🌱
Or: https://github.com/frnhr/vault-cloud-bootstrap