# Automating Obsidian Publish from the Command Line I write this blog in [Obsidian](./obsidian-as-a-writing-and-publishing-system.md), and for a long time publishing meant the same manual ritual: open the **Publish changes** modal, hunt through a list of changed files, tick the right boxes, and click Publish. My vault has thousands of notes but I only publish a curated handful, so that modal is a minefield — one stray "Select all" could push the entire vault live. I wanted to just say "publish this post" and have it happen. ## Why the obvious approaches don't work **There is no official Obsidian Publish API or CLI.** So I tried the next-best things, and each failed for an instructive reason: - **Editing `publish.json`'s `included` list.** Obsidian holds that config in memory and writes it back, so external edits get clobbered. It also doesn't scope the publish modal the way I assumed. - **A keyboard macro (AppleScript).** It can *open* the modal reliably, but it can't tick specific checkboxes in a list of thousands — the order isn't stable and there's no keyboard path to a given row. Both are fragile because they automate the *UI*. I wanted to automate the *protocol*. ## Reading the client's own code The Obsidian desktop app is an Electron app, which means its source ships as a readable `app.js` inside an `.asar` archive. Extracting it and searching for the publish logic revealed exactly how the client talks to the server: ``` POST https://publish-01.obsidian.md/api/upload headers: obs-token : your account token obs-id : the site id (from publish.json) obs-path : URL-encoded, vault-relative file path obs-hash : SHA-256 hex digest of the file's bytes body: the raw file bytes ``` That's the whole thing. Each call uploads or updates **exactly one file** — there's no "publish everything" surface area, so it can't accidentally touch the rest of the vault, and it never deletes anything. A few details mattered: - **The host is `publish-01.obsidian.md`**, read from `publish.json`'s `host` field — not the generic `publish.obsidian.md` domain I first guessed. - **The hash is a plain SHA-256 hex digest** of the file bytes (the server uses it to detect changes). - **The token lives in memory, not on disk.** It's stored in the app's `localStorage` under `obsidian-account`, so I grab it once from the DevTools console: ```js copy(JSON.parse(localStorage.getItem('obsidian-account')).token) ``` ## The 403 that taught me about Cloudflare My first requests came back `403 Forbidden` with `error code: 1010`. That's a Cloudflare signature: it blocks requests whose **User-Agent** doesn't look like a real Obsidian client. Adding an Obsidian user agent fixed it instantly: ``` User-Agent: obsidian/1.12.7 ``` This is a good reminder that "undocumented API" often also means "protected by a bot wall" — and that the fix is to present yourself honestly as the same client the protocol was built for. ## The result Now publishing a post is one command: ```bash python3 publish_obsidian_api.py blog/my-post.md blog/my-post-assets/chart.png ``` It hashes each file, uploads only those, and leaves everything else untouched. (One gotcha worth automating away: a post's embedded images are separate files, so they have to be uploaded alongside the markdown or they render broken.) ## Is this a good idea? It's worth being honest about the tradeoffs. This is an **unofficial, reverse-engineered API**, so: - It can break on any Obsidian update — the host, headers, or required user-agent string could change. - It's against the spirit of "use the app the way it's shipped," even though it only touches my own site with my own credentials. - The token is a real secret; it has to be handled like one. For me the tradeoff is worth it: I publish from the same place I do everything else, and the consequential "which files go live" decision is now explicit in a command rather than a checklist I might fat-finger. But if Obsidian ever ships a real publish API, I'll switch to it immediately. ## Related - [Obsidian as a Writing and Publishing System](./obsidian-as-a-writing-and-publishing-system.md) - [A Raspberry Pi Calendar for the Kitchen Wall](./digital-calendar-raspberry-pi-display.md)