Markdown in. Live newsletter out.
A command line client for Substack, built so your coding agent can run your newsletter for you. Push, pull, schedule, and publish without opening the web editor.
Quickstart · Agents · What it does · Commands · Docs · Why
Your markdown file is the source of truth. Substack is a render target.
pipx install git+https://github.com/HighnessAtharva/substack-cli
substack init
substack push my-post.mdNote
Setup asks for two things. Your publication URL, and the connect.sid cookie from a
browser where you are already logged in. The publication id and the user id get read off
the API and cached for you. docs/authentication.md shows exactly
where that cookie lives.
A post is a markdown file with frontmatter on top.
---
title: How I Publish From The Terminal
subtitle: One command, no web editor
slug: how-i-publish-from-the-terminal
cover: cover.png
---
Your article body, in plain markdown.One command ships it, images and cover included.
substack push my-post.mdThe draft id lands back in your frontmatter. Your next push updates that same draft instead of creating a second one. When the draft reads right, take it live.
substack publish 209491778 --yes --no-emailI did not build this to type commands. I built it so Claude Code could run my Substack while I wrote. Three lines and yours can too.
pipx install git+https://github.com/HighnessAtharva/substack-cli
substack init # paste your publication URL and one cookie
substack agent install # your agent learns the toolThen talk to it in English.
"what Substack drafts do I have?"
"push posts/how-i-publish.md as a draft and give me the link"
"fix the typo in paragraph three of my last post and update it live"
"back up my whole Substack archive into ./archive and commit it"
"schedule Tuesday's draft for 9am, no email"
agent install writes the instruction file your agent already reads. It detects which agent
you use, and it never clobbers rules you already have.
| Agent | File it writes |
|---|---|
| Claude Code, Claude Desktop | .claude/skills/substack-cli/SKILL.md |
| Cursor | .cursor/rules/substack-cli.mdc |
| Codex, Gemini CLI, Aider, Cline | AGENTS.md |
That file is the part that matters. It carries the operating knowledge that stops an agent
doing damage. Seven hard rules, the safe edit loop, the exit codes, and six worked recipes. It
also documents the two-bodies trap, where a push to a live post silently does nothing. Read the
file with substack agent print, or in docs/agents.md.
Tip
Every write gates on one machine-readable check. substack audit post.md --json returns
{"clean": true, "destroyed": {}, ...}, and clean is the whole decision. An agent that
respects that field cannot delete your work.
Warning
Scheduling a post is real and server-side. It needs a second cookie, saved once with
substack init --hub-token. Scheduling a Note is impossible, because Substack has no
endpoint for it. Your agent will offer to schedule the command instead. That means a Claude
Code routine, a cron entry, or a Task Scheduler job that runs substack note on time.
|
Push A markdown file becomes a Substack draft. Headings, bold, links, code blocks, lists, quotes and captions all convert. |
Pull A live post comes back down as markdown. Every image downloads next to it. |
Update You rewrite an already published post in place. No email, no feed bump, same URL. |
|
Audit You see exactly what an update would destroy, before it destroys anything. |
Publish You go live now, with or without emailing your subscribers. |
Schedule You set a real server-side release time, and you cancel it the same way. |
|
Notes You post a Substack Note from a file or a string, with image attachments. |
Covers The hero image uploads itself. |
Slugs Your file owns the public URL. Substack never gets to invent a truncated one. |
|
Tables A markdown table renders to a PNG, because the Substack editor has no table support. |
Templates You list the saved post templates on your account and push a file against one. |
Sitemap You build a local index of every live post, ready to commit beside your drafts. |
It runs on the Python standard library alone. No dependencies to install, no API key to request, no browser to automate, and no Node runtime anywhere. Pillow is the one optional extra, and only if you want tables rendered.
Caution
update regenerates a live post's body from your markdown. Anything on the page that your
markdown does not mention is gone, with no undo and no warning in the output. On my own
publication that would have silently deleted 125 images, 24 captions, 9 embeds, 2 videos
and 2 pullquotes across 33 posts.
So audit runs first. It compares the live page against what your file would produce. It exits
non-zero when the update would lose something.
Some blocks live only in the editor and markdown cannot express them. That covers YouTube
embeds, uploaded video, Twitter embeds, callouts and pullquotes. update extracts them from
the live body and re-anchors each one after the same paragraph it followed. Rewriting the text
around them does not lose them.
All 22 commands, with what each one does
| Command | What it does |
|---|---|
substack init |
Save credentials and verify them. |
substack agent install |
Teach Claude Code, Cursor, or any agent to drive this. |
substack doctor |
Check auth and print the resolved configuration. |
substack list [--published] |
List drafts, or live posts. |
substack get <id|slug> |
Print one post's metadata. |
substack push <file> |
Create or update a draft from markdown. |
substack update <file> --yes |
Rewrite a live post. No email, no feed bump. |
substack audit <file> [--json] |
Report what an update would destroy. |
substack cover <id> --image X |
Swap a post's hero image, body untouched. |
substack pull <id|slug> |
Download a live post as markdown plus images. |
substack pull --published |
Download the entire archive. |
substack render <file> |
Convert to ProseMirror JSON offline. Sends nothing. |
substack publish <id> --yes |
Publish now. --no-email puts it on the web quietly. |
substack unpublish <id> --yes |
Take a live post back to draft. |
substack schedule <id> --at "..." |
Real server-side scheduled release. |
substack unschedule <id> |
Cancel it. |
substack set <id> --title --subtitle --slug |
Change metadata in place. |
substack delete <id> |
Delete a draft. Refuses published posts. |
substack note "text" |
Post a Substack Note. |
substack note-delete <id> |
Delete one of your Notes. |
substack templates |
List the saved post templates on your account. |
substack sitemap |
Build a local index of every live post. |
Full reference with every flag: docs/commands.md.
No public API, no official CLI, and no git push for a newsletter. Everything runs through a
web editor that owns your content, your formatting, and your workflow. You cannot draft in
your own editor, keep posts in version control, run a linter over them, or script a publish.
| The web editor | substack-cli |
|---|---|
| You draft in a browser text box | You draft in your own editor |
| Substack picks the URL slug | Your frontmatter picks the slug |
| Version history you cannot read | Every change is a git commit |
| Publishing is a click, by hand | Publishing is a command, in CI |
| Your archive sits on their server | pull writes the archive to your disk |
| Nothing can lint a draft first | Any script runs before the push |
The tools that do exist read Substack. They fetch feeds and scrape archives. Almost none of
them write, and the ones that try break the moment Cloudflare sees a curl User-Agent.
This one writes. It has published 79 posts to a real newsletter since July 2024. Every rule it enforces exists because something went wrong on a live page first.
An agent runs the whole pipeline. You write the article, then your agent converts it, uploads the images, audits the live page, and hands back a link. See docs/agents.md.
Your newsletter lives in version control. Keep every post in a git repo, review changes in a pull request, and push the merged file to Substack.
Everything gets backed up. substack pull --published -o ./archive writes every live post
to markdown with its images alongside. Run it on a cron and you own a real copy.
A whole blog migrates in. Point push at the markdown you already keep in Hugo, Jekyll,
Obsidian, or a Notion export. The archive moves across without you touching the editor.
CI drives the pipeline. Lint, spell-check, score, or run a model pass over a file, then push and schedule it. Every command is scriptable and exits non-zero on failure.
publish,update, andunpublishall refuse to run without--yes, and each one prints what it is about to do first.deleterefuses published posts outright and tells you tounpublishfirst.updateprints its destroy list before it touches anything, andauditexits 1 when the local file is not a superset of the live page.renderconverts offline and sends nothing, so an agent checks its own work before any write reaches the network.- Your cookies live in a
0600config file or in environment variables, and nothing ever prints them back.
Ten guides, one per job
| Guide | What it covers |
|---|---|
| agents.md | Let Claude Code, Cursor, or Codex run your Substack. |
| installation.md | Every install path, Windows included. |
| authentication.md | Where the two cookies live and how long they last. |
| commands.md | Complete reference, every flag, every exit code. |
| frontmatter.md | Every field the tool reads and writes. |
| markdown.md | What converts, what does not, and why. |
| workflows.md | Git-backed publishing, backups, migrations, CI. |
| api-notes.md | The undocumented Substack API, written down. |
| troubleshooting.md | Every error message and its fix. |
| faq.md | Is this allowed, will it break, what about paid posts. |
Issues and pull requests are welcome. The test suite is 122 offline checks that run in under a second, and every one of them pins a bug that reached a live newsletter.
git clone https://github.com/HighnessAtharva/substack-cli
cd substack-cli
pip install -e ".[dev]"
pytest -q && ruff check .Read CONTRIBUTING.md first.
Atharva Shah, who publishes at blog.atharvashah.com and uses this to do it.
If this saved you an afternoon, a star helps other writers find it.
MIT licensed. Not affiliated with, endorsed by, or supported by Substack Inc. It drives the same private endpoints your browser does, using your own session cookie, against your own publication. Read docs/faq.md before you build a business on it.
Keywords: substack api · substack cli · publish to substack from markdown · substack markdown import · substack automation · substack python client · newsletter as code · substack backup · export substack posts · substack scheduler · claude code substack skill · agent publishing tools · AGENTS.md
