# 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)