Why not just script the Ghost Admin API yourself?
If you’re the kind of person who’s reading this page, you’ve already had the thought: I’m a developer, the Ghost Admin API is right there, why would I pay for a tool to do something I can write in an afternoon? It’s a fair question, and the honest answer is that you absolutely can. For a one-off job — pull every post, run a find-and-replace, push them back — a script is the right tool, and you should write it. This page isn’t here to talk you out of that.
What it’s here to do is lay out, fairly, what changes when “a one-off job” becomes “a workflow I rely on.” Because the gap between those two things is where most home-grown sync scripts quietly fall apart, and it’s exactly the gap Specter is built to fill.
The part that’s genuinely easy
Authenticating is the cheerful bit. A Ghost Admin API key is an id:secret pair, and you turn it into a short-lived JWT: HS256, the secret decoded from hex, a five-minute expiry, and an /admin/ audience claim. A dozen lines with any JWT library, or the Web Crypto API if you’d rather not pull a dependency. Once you’ve got that token in an Authorization: Ghost <token> header, the posts endpoint hands you JSON. (If you haven’t done this part yet, the Ghost Admin API key guide walks through where the key lives and the Content-key mistake nearly everyone makes first.)
So far, so simple. The trouble starts the moment you try to turn that JSON into something you’d actually want to edit.
Where it stops being a weekend project
Ghost doesn’t store your posts as markdown. Depending on the post’s age and which editor touched it last, the body comes back as Lexical, as mobiledoc, or as rendered HTML — sometimes a mix across your archive. To get clean local markdown you have to detect which format each post is in, pull the right field, and convert it down faithfully, then convert your edits back into a format Ghost will accept on write. Round-trip that carelessly and you’ll watch your code blocks, callouts, and image cards degrade a little more with every sync until a post you’ve edited three times no longer resembles what you wrote.
Then there’s everything that isn’t body text. Title, slug, tags, status, feature image, custom excerpt — all of that has to live somewhere locally, which means frontmatter, and it means keeping that frontmatter in step with Ghost on both sides without clobbering the fields the user edits by hand.
And that’s still just pulling. The genuinely hard part is two-way sync, because two-way means you have to answer a question a one-way script never asks: which side changed? You need to track each post’s last-known Ghost timestamp against its local modification time, decide whether the remote moved, the local moved, or both moved since you last looked — and when both moved, you cannot silently pick a winner. Last-write-wins is how people lose an afternoon’s editing to a background sync they forgot was running. A workflow you trust has to stop and ask, which means detecting the conflict, surfacing the two versions with their timestamps, and letting you choose keep-local, keep-remote, or skip — per post, not in bulk.
Before any of that touches your real content, you want a dry run. A preview that tells you “12 posts would be created locally, 3 would update Ghost, 1 is a conflict” before a single byte is written, so a bad regex in your AI pass doesn’t quietly overwrite forty posts. Building a real dry run is more than a --dry-run flag; it means running the entire planning pass against actual state while intercepting every write, so the preview reflects what would genuinely happen rather than what you hope would.
Past that sit the unglamorous parts that decide whether a tool is pleasant to live with. File watching, so edits sync as you save instead of when you remember to run the script — with debouncing so a burst of saves doesn’t fire a burst of API calls, and write-finished detection so you don’t push a half-written file. Rate-limit and error handling, so a 429 or a flaky connection backs off and retries instead of leaving your blog half-synced. Pagination across a large archive. And the maintenance tax: Ghost keeps evolving its editor and content formats, so the converter you finished today is something you’ll be babysitting for as long as you depend on it.
What Specter actually is
Specter is that whole list, built and maintained, sitting in your menu bar. JWT auth from your admin key, faithful Lexical / mobiledoc / HTML conversion both directions, frontmatter that round-trips title, tags, status, feature image, and excerpt, real change-detection that knows which side moved, conflict prompts when both did, a dry-run preview before anything commits, debounced file watching, and the ongoing work of keeping all of it current as Ghost changes underneath it. It syncs posts, and only posts — but it syncs them well.
Crucially, it isn’t a wall around the API. Underneath the app is a CLI, ghost-sync, with pull, push, sync, and watch — so the scripts you genuinely do want to write can call a sync engine that already handles the formats and the conflicts, instead of reimplementing them. You keep the scripting; you skip the babysitting.
The honest framing is this: Specter is the maintained version of the thing you’d otherwise build, and then maintain forever. If your need is a single batch edit, write the script — you’ll be done before lunch. If your need is to keep a folder of markdown and a live Ghost blog in sync, edit it with whatever tools you like, and not think about the plumbing again, that’s the part worth not building twice. (For what that day-to-day looks like once the sync is just there, see using AI to edit Ghost posts.)