Documentation
Substack CLI
substack-pp-cli
Install
npx -y @mvanhorn/printing-press-library install substackInstalls the CLI binary and the agent skill for Claude Code, Codex, Cursor, Gemini CLI, Copilot, and more. Add --cli-only or --skill-only for one half. Requires Node.
Documentation
Run your Substack growth and authoring loop from the command line — publish rich drafts, manage a multi-publication portfolio, and measure what drives growth.
Substack has no public API and the closed-source tools that work around it (WriteStack, StackSweller) stop at Notes scheduling and a heatmap. This CLI covers the read endpoints the community has reverse-engineered across the 8 wrappers we studied, plus rich authoring (30+ flags on drafts create/update, Markdown→ProseMirror conversion), a multi-publication portfolio layer (portfolio sync → portfolio, posts best, grep, schedule board, subs churn, subs cross-sell), and local-SQLite analytics. Every command is MCP-callable so an agent can drive the full publish → engage → measure → swap loop.
Created by @chirantan (Chirantan Rajhans). Contributors: @JPresting (JimPresting), @tmchow (Trevin Chow).
Authentication
Substack uses a session cookie (substack.sid). The only path today is auth login --chrome (also accepts --browser as an alias) — it reads the cookie from your logged-in Chrome via pycookiecheat / cookies / cookie-scoop-cli and stores it in the OS keyring. There is no password login and no manual cookie-paste subcommand. If your cookie expires, re-run auth login --chrome.
Quick Start
# Health check — confirms your stored Substack session is valid. Capture it first with `substack-pp-cli auth login --chrome`.
substack-pp-cli doctor --dry-run
# Probes all three Substack bases plus the RSS path to surface auth or Cloudflare issues early.
substack-pp-cli doctor
# Pulls posts, drafts, your Notes, comments, profiles, and subscriber-count snapshots into the local store.
substack-pp-cli sync --since 30d
# Bootstrap the portfolio analytics store (run once after login).
export SUBSTACK_PUBLICATION=mypub
substack-pp-cli portfolio sync --json
# One-screen status of every publication you own.
substack-pp-cli portfolio --json
# Dry-run prints the request without firing; drop --dry-run to publish.
substack-pp-cli notes new --body "Stop refreshing the feed. Spend 15 minutes in your inbox replying to commenters and you'll outgrow 90% of writers who don't." --dry-run
# Create a rich draft with Markdown body, paid audience, SEO metadata, and cover image.
# (SUBSTACK_PUBLICATION must already be set)
substack-pp-cli drafts create \
--title "Why X matters" --body-file ./post.md \
--audience only_paid --seo-title "X explained" \
--cover-image https://substackcdn.com/.../cover.jpg --json
# Surfaces which of your last 30 days of Notes brought subs.
substack-pp-cli growth attribution --days 30 --agent --select rank,note_excerpt,subs_acquired
# Ranks candidate publications for a recommendation swap by audience overlap.
substack-pp-cli recs find-partners --my-pub on --top 10 --json
Unique Features
These capabilities aren't available in any other tool for this API.
Local state that compounds
-
growth attribution— Connect every Note you posted to the paid and free subscribers that actually arrived in the 24-hour window after, so you stop guessing which content drove growth.Pick this over a generic stats call when an agent needs to decide which Note formats to repeat next week.
substack-pp-cli growth attribution --days 30 --json --select rank,note_id,note_excerpt,subs_acquired,paid_subs_acquired -
engage reciprocity— See net-give/net-take per writer you engage with — who reciprocates your restacks/comments, who quietly free-rides on yours.Use when an agent is deciding whether to keep investing in a swap partner; surfaces relationships before they go stale.
substack-pp-cli engage reciprocity --days 30 --agent --select handle,outgoing,incoming,net,drift
Algorithm-aware automation
-
notes schedule --guard— Refuse to fire (or queue) a Note that lands less than 30 minutes after your last own-Note or violates your time-of-day rotation. Returns typed exit 2 with a JSON diagnosis.Stops an agent from accidentally torching its own reach by dumping a queue all at once.
substack-pp-cli notes schedule --at 2026-05-10T13:00:00Z --body "hook line\n\nbody" --guard --json -
growth best-time— Top day-of-week × hour cells ranked for whichever growth signal you pick (paid subs, likes, restacks, or comments) — not a single average.An agent picking when to schedule tomorrow's Notes can ask for the goal it's optimizing instead of guessing.
substack-pp-cli growth best-time --days 90 --for-goal subs --json --select day_of_week,hour,rate,sample_size
Pattern intelligence
-
discover patterns— Mechanically extracts which hook patterns (curiosity-gap colon, 3-sentence formula, em-dash reframe, question opener) actually rank in a niche, with restack/comment ratios.An agent drafting Notes can ask which hook shape currently outperforms in this niche before generating.
substack-pp-cli discover patterns --niche productivity --sort restacks --since 14d --agent --select pattern,sample_count,avg_restacks,avg_comments,top_example -
voice fingerprint— Measurable voice profile — sentence length, em-dash rate, colon-hook rate, hook-line ratios, vocabulary uniqueness — for any handle, with --diff to compare against another writer.An agent drafting Notes for a ghostwriter client can verify the output stays inside the client's voice envelope.
substack-pp-cli voice fingerprint --handle maya --diff devon --json --select metric,self,other,delta
Network leverage
-
recs find-partners— Score candidate publications for a Substack Recommendations swap by mutual-overlap density across followee + recommendation graphs.An agent running a weekly cross-promo pass can rank candidates instead of pitching cold.
substack-pp-cli recs find-partners --my-pub on --top 20 --json --select rank,handle,pub,overlap_score,shared_followees -
growth pod— Given a list of handles, render a member × member engagement matrix — last 30 days of restacks/comments/likes between every pair.An agent organizing a mutual-aid pod can see who's net-positive vs free-riding without a spreadsheet.
substack-pp-cli growth pod --members maya,devon,priya,jordan --days 30 --json
Authoring with rich field control
-
drafts create/drafts update— Full Substack draft API surface: 30+ flags covering title, subtitle, body (Markdown auto-converts to ProseMirror), section-id, post type (newsletter/podcast/video/thread), audience, bylines, SEO metadata, social title, cover image, comment settings, podcast/video URLs, and visibility toggles. The only authoring path that gives agents field-level control without fighting a web editor.Use when an agent is constructing a complete long-form post from structured data — research summary, translated copy, ghostwritten piece — and needs paywall, SEO, and section placement set in one command.
# Markdown body, paid-only, with SEO + cover image # (SUBSTACK_PUBLICATION must already be set to the target publication subdomain) substack-pp-cli drafts create \ --title "Why X matters" --subtitle "A short analysis" \ --body-file ./post.md --audience only_paid \ --seo-title "X explained" --seo-description "How X affects Y" \ --cover-image https://substackcdn.com/.../cover.jpg --json # Update just the title and subtitle of an existing draft substack-pp-cli drafts update 12345 \ --title "New title" --subtitle "New subtitle"
Portfolio & Analytics (local columnar store)
These commands read a local SQLite store populated by portfolio sync. The workflow is:
auth login --chrome → export SUBSTACK_PUBLICATION=<your-pub> → portfolio sync → portfolio / posts best / grep / subs churn / …
Custom-domain publications are supported: auth login --chrome captures the Creator-session cookie from the custom domain automatically.
-
portfolio sync— The data-population command. Discovers every publication you own and writes posts, subscribers, and drafts into the local columnar store. Must be run before the analytics commands below return cross-publication data.export SUBSTACK_PUBLICATION=mypub substack-pp-cli portfolio sync --json -
portfolio— One-screen status of every publication you own: subscriber count, paid count, posts published, drafts pending, next scheduled. No tab-switching, no CSV exports.substack-pp-cli portfolio --json -
posts best— Rank posts by views, likes, comments, or restacks within a window.--cross-pubaggregates across all your publications.substack-pp-cli posts best --by restacks --window 30d --cross-pub --json substack-pp-cli posts best --by views --limit 5 --publication mypub-en -
posts twin <slug> --to <pub>— Duplicate a published post into another publication you own as a draft. Preserves paywall markers, section mapping, and re-uploads images to the target CDN.substack-pp-cli posts twin my-en-slug --to mypub-de --dry-run --json substack-pp-cli posts twin my-en-slug --to mypub-de -
posts pair <en> <de>/posts pairs [--missing]— Record EN↔DE post pairings.--missinglists posts without a recorded twin — feed that output intoposts twinto spin up the missing translations.substack-pp-cli posts pair my-en-slug my-de-slug substack-pp-cli posts pairs --missing --publication mypub-en --json -
grep <query>— FTS5 full-text search across synced posts, notes, and comments, ranked by bm25, returning snippets and source URLs.substack-pp-cli grep "yield curve" --json substack-pp-cli grep "rate hike" --scope posts --publication mypub-en --since 2024-01-01 -
schedule board— ASCII calendar of the next N days showing scheduled posts across every publication you own. Multi-publication editorial overview in one screen.substack-pp-cli schedule board --days 30 --json -
subs churn— Diff subscriber snapshots: who newly subscribed, who unsubscribed, who upgraded free→paid, who downgraded paid→free. Run--snapshotat least once first.substack-pp-cli subs churn --snapshot substack-pp-cli subs churn --since 7d --json --publication mypub-paid -
subs cross-sell— Emails paid on one of your publications but free or absent on the others. Requires 2+ owned publications in the local store. The cross-sell list Substack's UI does not ship.substack-pp-cli subs cross-sell --json --limit 100
Recipes
Daily growth-loop morning ritual
substack-pp-cli growth attribution --days 7 --agent --select rank,note_excerpt,subs_acquired
Surfaces yesterday's Note→sub winners. Pair with substack-pp-cli engage reciprocity --days 7 --agent to see whose engagement reciprocates yours, and substack-pp-cli sync --since 24h ahead of time to keep the local store fresh.
Schedule a Note with the cadence guard
substack-pp-cli notes schedule --at 2030-05-13T09:00:00Z --body 'Tuesday hook line' --guard --json
Queues the Note locally; --guard refuses scheduling if it lands within 30 min of an existing own-Note (typed exit 2 + JSON diagnosis). Drop --guard or add --send to fire immediately.
Find this week's swap partners
substack-pp-cli recs find-partners --my-pub on --top 5 --json --select rank,handle,pub,overlap_score
Ranks candidate publications by audience overlap; pipe to your draft-outreach tool of choice (substack-pp-cli does the ranking; outreach drafting is left to your agent's prompt).
Capture a writer's voice fingerprint as JSON
substack-pp-cli voice fingerprint --handle alice --diff bob --json
Mechanical voice metrics for the named handle, with a delta against another writer when --diff is set. Save the JSON yourself; agent generation prompts can ingest it.
Surface deeply nested Note metadata with --select
substack-pp-cli notes get c-12345 --agent --select id,body,attachments.url,attachments.image_url,attachments.published_bylines.name,attachments.published_bylines.handle,context.users.name
Notes responses are deeply nested (attachments, bylines, contextual users). Dotted --select narrows the payload so an agent doesn't burn context parsing 30KB of JSON it doesn't need.
Bootstrap the portfolio analytics store
substack-pp-cli auth login --chrome
export SUBSTACK_PUBLICATION=mypub
substack-pp-cli portfolio sync --json
substack-pp-cli portfolio --json
Run once after login. Every cross-publication analytics command reads the local store that portfolio sync populates.
Publish a rich draft with SEO and cover image
export SUBSTACK_PUBLICATION=mypub
substack-pp-cli drafts create \
--title "The case for X" \
--subtitle "Three reasons it matters now" \
--body-file ./post.md \
--audience only_paid \
--seo-title "Case for X" \
--seo-description "Why X matters for Y" \
--cover-image https://substackcdn.com/.../cover.jpg \
--json
Twin your best EN post into a DE publication
# Sync first so the local store has current posts
substack-pp-cli portfolio sync --json
# Find top post by restacks
substack-pp-cli posts best --by restacks --limit 1 --publication mypub-en --json
# Preview, then create the draft
substack-pp-cli posts twin my-en-slug --to mypub-de --dry-run --json
substack-pp-cli posts twin my-en-slug --to mypub-de --json
Weekly subscriber churn digest
# Run once to set a baseline, then weekly to see movement
substack-pp-cli subs churn --snapshot
substack-pp-cli subs churn --since 7d --json --publication mypub-paid
Full-text search across all your publications
substack-pp-cli grep "interest rates" --scope posts --json
substack-pp-cli grep "reader question" --scope notes --since 2025-01-01 --limit 20 --json
Usage
Run substack-pp-cli --help for the full command reference and flag list.
Commands
categories
Site-wide Substack category list — culture, technology, food, etc.
substack-pp-cli categories list- List all Substack categoriessubstack-pp-cli categories list-publications- List publications in a category
comments
Long-form post comments (distinct from Notes)
substack-pp-cli comments get- Get a single comment by ID (same shape as a Note — Substack treats them uniformly)substack-pp-cli comments list- List comments on a post
discover
Discovery surfaces — search publications, embed metadata
substack-pp-cli discover- Search Substack publications by query
drafts
Drafts CRUD + publish + schedule
substack-pp-cli drafts create- Create a new draftsubstack-pp-cli drafts delete- Delete a draftsubstack-pp-cli drafts get- Get a draft by IDsubstack-pp-cli drafts list- List draftssubstack-pp-cli drafts prepublish- Validate a draft for publication; returns blockerssubstack-pp-cli drafts publish- Publish a draft nowsubstack-pp-cli drafts schedule- Schedule a draft for future publish (or unschedule with --post-date null)substack-pp-cli drafts update- Update an existing draft
feed
RSS feed for a publication
substack-pp-cli feed- RSS XML feed (returns XML; use--rawto dump)
images
Image upload (data-URI JSON, not multipart)
substack-pp-cli images- Upload an image; returns CDN URL. Body is data-URI JSON.
inbox
Authenticated reader feed (home feed) — Notes + posts surfaced for the current user
substack-pp-cli inbox home- Authenticated home feedsubstack-pp-cli inbox reader-posts- Posts feed for current user
notes
Substack Notes — short-form posts (Substack treats Notes as comments internally)
substack-pp-cli notes create- Post a new Note (POST /comment/feed). Body is ProseMirror JSON.substack-pp-cli notes get- Get a single Note by IDsubstack-pp-cli notes list-by-profile- List Notes by a profile (cursor pagination)substack-pp-cli notes reply- Reply to an existing Note (parent_id + ProseMirror body)
grep
Full-text search across synced posts, notes, and comments
substack-pp-cli grep <query>- FTS5 search ranked by bm25, returning snippets and source URLs. Flags:--scope posts|notes|comments|all,--publication,--since,--limit
portfolio
Multi-publication status dashboard and data-population
substack-pp-cli portfolio- One-screen status of every publication you own (subs, paid, posts, drafts, next scheduled). Runportfolio syncfirst.substack-pp-cli portfolio sync- Discover every publication you own and populate the local columnar store (publications/posts/subscribers/drafts). The prerequisite for all cross-publication analytics commands.
posts
Long-form posts and archives on a specific publication
substack-pp-cli posts archive- Public archive of a publication's postssubstack-pp-cli posts best- Rank cached posts by engagement metric (--by views|likes|comments|restacks,--window,--cross-pub,--limit,--publication)substack-pp-cli posts get-by-slug- Get a published post by URL slugsubstack-pp-cli posts list-published- List published posts on the publication (auth required)substack-pp-cli posts pair <en-slug> <de-slug>- Record an EN↔DE translation pairing in the local tablesubstack-pp-cli posts pairs- List recorded post pairs;--missingshows posts without a twin;--publicationfilters to one pubsubstack-pp-cli posts ranked-authors- Ranked list of authors for a publicationsubstack-pp-cli posts twin <slug> --to <pub>- Duplicate a published post into another publication you own as a draft (re-uploads images, preserves paywall markers)
profiles
Substack profiles — your own and other writers'
substack-pp-cli profiles from-linkedin- Look up a Substack profile from a LinkedIn handlesubstack-pp-cli profiles get-by-handle- Get a public profile by handle (e.g. mvanhorn)substack-pp-cli profiles get-by-id- Get a public profile by numeric user IDsubstack-pp-cli profiles handle-options- Available handle suggestions for the current usersubstack-pp-cli profiles posts- All posts by an author across publicationssubstack-pp-cli profiles self- Get the authenticated user's profile
recommendations
Substack Recommendations — outbound (publications I recommend)
substack-pp-cli recommendations <publication_id>- List the publications a publication recommends
sections
Sections of a publication (newsletters can have multiple)
substack-pp-cli sections- List sections + subscriptions
settings
Account settings + connectivity probe (used by doctor)
substack-pp-cli settings get- Get account settingssubstack-pp-cli settings ping- Connectivity probe (non-destructive PUT used by doctor)
schedule
Cross-publication editorial scheduling
substack-pp-cli schedule board- ASCII calendar of the next N days (--days) of scheduled posts across all owned publications
subs
Subscriber count, churn diff, and cross-sell analytics
substack-pp-cli subs authors- List bylined authors of a publicationsubstack-pp-cli subs churn- Diff subscriber snapshots (new/unsubscribed/upgraded/downgraded). Use--snapshotto create a baseline, then--sinceto diff. Flags:--publication,--since,--snapshotsubstack-pp-cli subs count- Get subscriber count (read off the launch-checklist payload)substack-pp-cli subs cross-sell- Emails paid on one publication but free/absent on others (requires 2+ owned pubs). Flags:--limit
tags
Post tags
substack-pp-cli tags create- Create a new tagsubstack-pp-cli tags list- List all tags for the publication
Output Formats
# Human-readable table (default in terminal, JSON when piped)
substack-pp-cli categories list
# JSON for scripting and agents
substack-pp-cli categories list --json
# Filter to specific fields
substack-pp-cli categories list --json --select id,name,status
# Dry run — show the request without sending
substack-pp-cli categories list --dry-run
# Agent mode — JSON + compact + no prompts in one flag
substack-pp-cli categories list --agent
Agent Usage
This CLI is designed for AI agent consumption:
- Non-interactive - never prompts, every input is a flag
- Pipeable -
--jsonoutput to stdout, errors to stderr - Filterable -
--select id,namereturns only fields you need - Previewable -
--dry-runshows the request without sending - Explicit retries - add
--idempotentto create retries and--ignore-missingto delete retries when a no-op success is acceptable - Confirmable -
--yesfor explicit confirmation of destructive actions - Piped input - write commands can accept structured input when their help lists
--stdin - Offline-friendly - sync/search commands can use the local SQLite store when available
- Agent-safe by default - no colors or formatting unless
--human-friendlyis set
Exit codes: 0 success, 2 usage error, 3 not found, 4 auth error, 5 API error, 7 rate limited, 10 config error.
Freshness
This CLI owns bounded freshness for registered store-backed read command paths. In --data-source auto mode, covered commands check the local SQLite store before serving results; stale or missing resources trigger a bounded refresh, and refresh failures fall back to the existing local data with a warning. --data-source local never refreshes, and --data-source live reads the API without mutating the local store.
Set SUBSTACK_NO_AUTO_REFRESH=1 to disable the pre-read freshness hook while preserving the selected data source.
Covered command paths:
substack-pp-cli categoriessubstack-pp-cli categories getsubstack-pp-cli categories listsubstack-pp-cli categories searchsubstack-pp-cli draftssubstack-pp-cli drafts getsubstack-pp-cli drafts listsubstack-pp-cli drafts searchsubstack-pp-cli inboxsubstack-pp-cli inbox getsubstack-pp-cli inbox listsubstack-pp-cli inbox searchsubstack-pp-cli inbox-postssubstack-pp-cli inbox-posts getsubstack-pp-cli inbox-posts listsubstack-pp-cli inbox-posts searchsubstack-pp-cli postssubstack-pp-cli posts getsubstack-pp-cli posts listsubstack-pp-cli posts searchsubstack-pp-cli posts-publishedsubstack-pp-cli posts-published getsubstack-pp-cli posts-published listsubstack-pp-cli posts-published searchsubstack-pp-cli posts-rankedsubstack-pp-cli posts-ranked getsubstack-pp-cli posts-ranked listsubstack-pp-cli posts-ranked searchsubstack-pp-cli profilessubstack-pp-cli profiles getsubstack-pp-cli profiles listsubstack-pp-cli profiles searchsubstack-pp-cli sectionssubstack-pp-cli sections getsubstack-pp-cli sections listsubstack-pp-cli sections searchsubstack-pp-cli subssubstack-pp-cli subs getsubstack-pp-cli subs listsubstack-pp-cli subs searchsubstack-pp-cli tagssubstack-pp-cli tags getsubstack-pp-cli tags listsubstack-pp-cli tags search
JSON outputs that use the generated provenance envelope include freshness metadata at meta.freshness. This metadata describes the freshness decision for the covered command path; it does not claim full historical backfill or API-specific enrichment.
Runtime Endpoint
This CLI resolves endpoint placeholders at runtime, so one installed binary can target different tenants or API versions without regeneration.
Endpoint environment variables:
SUBSTACK_PUBLICATIONresolves{publication}
Base URL: https://substack.com/api/v1
Health Check
substack-pp-cli doctor
Verifies configuration, credentials, and connectivity to the API.
Configuration
Config file: ~/.config/substack-pp-cli/config.toml
Static request headers can be configured under headers; per-command header overrides take precedence.
Troubleshooting
Authentication errors (exit code 4)
- Run
substack-pp-cli doctorto check credentials Not found errors (exit code 3) - Check the resource ID is correct
- Run the
listcommand to see available items
API-specific
- 401 Unauthorized on any write command — Cookie expired. Run
substack-pp-cli auth login --chrometo re-import (also aliased as--browser). - RSS /
posts feedreturns 403 with 'Just a moment...' HTML — Cloudflare TLS fingerprinting. Runsubstack-pp-cli doctorto confirm; if it reports the RSS leg blocked, retry from a different IP or useposts archive(uses the JSON API which Cloudflare doesn't gate as aggressively). - Notes posted at the same minute fail or get hidden by the algorithm — Re-run with
--guard(default innotes schedule); the cadence guard will reject sub-30-min spacing with exit 2 and a JSON diagnosis explaining the violation. engage like/engage restackprinted a curl-equivalent instead of firing — That's the default — these endpoints aren't in any community wrapper, so the CLI prints the request shape so you can preflight it. Add--sendto actually fire.
Sources & Inspiration
This CLI was built by studying these projects and resources:
- alexferrari88/sbstck-dl — Go (216 stars)
- NHagar/substack_api — Python (194 stars)
- ma2za/python-substack — Python (149 stars)
- jakub-k-slys/substack-api — TypeScript (71 stars)
- jakub-k-slys/n8n-nodes-substack — TypeScript (24 stars)
- ty13r/substack-mcp-plus — Python
- arthurcolle/substack-mcp — Python
- nanameru/substack-mcp — Python
Generated by CLI Printing Press
For agents: pass /documentation/substack/agents.md to install this CLI from a prompt.