hoangvantuan/substack-cli · Archived

sub-cli

Manage posts on your Substack publication and read other people's newsletters from the command line. Use when creating, listing, updating, scheduling, publishing, or deleting posts; filing posts into sections; or scanning and crawling public Substack content.

First seen Aug 24, 2026

Installation

$ npx skills add hoangvantuan/substack-cli --skill sub-cli

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Also in this package

Other skills from hoangvantuan/substack-cli.

npx skills add hoangvantuan/substack-cli

Browse all from hoangvantuan/substack-cli

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 0
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 7,673 B
  • docs SUMMARY.md 274 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

sub-cli

Requires sub-cli 0.4.0 or later. The skill is installed by hand and not version-locked to the package: if a flag below is rejected as unknown, the installed CLI is older than this skill assumes, fall back to the usage line the command prints and confirm the difference with the human.

Core principles

  1. Optimistic execution: run the command you need directly. Do NOT

pre-check auth with profile check or profile list before every command. The exit code already tells you whether auth failed (code 3). Pre-checking wastes a round trip on every invocation when the cookie is almost always still valid. Run the real command first; handle errors after.

  1. Profile discovery: the first time you need a profile name and do not

know it, run sub-cli profile list once per conversation to learn the available names. Cache the result, do not re-run it.

  1. Always pass --profile <name> explicitly on every authoring command

(post, section). Never rely on the default profile.

  1. Prefer --json for machine-readable output; parse stdout only. Progress,

warnings, and errors go to stderr.

Error handling

Read the exit code, not stderr text, to decide what to do:

Code Meaning Action
0 Success Continue.
1 General failure Read stderr, fix the input or retry later.
2 Usage error You invoked the command wrong. Fix arguments from the printed usage line. Do not retry unchanged.
3 Auth expired Cookie is dead. Ask the human to run sub-cli profile login <name>. Nothing else will fix it, do not retry the command until they confirm login succeeded.
4 Rate limited Back off. Wait a few minutes before retrying, do not hammer.

--no-retry disables the CLI's built-in retry/backoff for transient network errors. Use it when you want fast failure instead of waiting (e.g. in --dry-run or when you will retry at a higher level yourself).

What an agent may do alone

  • Read anything public (no cookie needed): feed scan, feed crawl,

feed crawl-all. These never touch profiles.

  • Create drafts: post create <file> (add --dry-run first to preview the

exact request). Drafts are private and reversible.

  • Adjust drafts: post update, section set, post unschedule. Read the

publication's sections with section list.

  • Delete drafts and scheduled posts: post delete <id> --yes.

What needs human confirmation first

  • Publishing (post publish) is irreversible: it cannot be recalled and

may email subscribers. The tool also demands an explicit --profile and --yes; treat those as a second lock, not permission to skip asking.

  • Deleting a published post (post delete <id> --yes --force-published)

removes it for every subscriber. Always ask.

  • Changing the publication's sections (section add, section remove)

edits the publication's own structure, not one post. section remove also strips the grouping from every post filed under it and cannot be undone; ask before either.

  • Scheduling (post schedule) sends email at the trigger time. Confirm

the time and audience with the human before running it.

When in doubt about audience, timing, or content, ask before writing.

Profiles

A profile pairs a name with one publication URL and one cookie.

sub-cli profile add <name> <publication>   # prompts for the cookie
sub-cli profile check <name>               # proves the cookie is alive
sub-cli profile list                       # show all profiles
sub-cli profile use / remove / login

Cookies expire after one to two weeks; exit code 3 means refresh via profile login.

Reserve profile check for when the human explicitly asks to verify a cookie, or after a login to confirm it worked. It is not a prerequisite for running commands.

Command surface

Reading (no profile, no cookie)

These commands access public RSS/HTML and never need authentication. Do not look up or pass --profile.

sub-cli feed scan <publication> [--limit n] [--all] [--json] [--no-retry]
sub-cli feed crawl <url> [--out dir] [--overwrite] [--no-retry]
sub-cli feed crawl-all <publication> [--limit n] [--all] [--out dir] [--overwrite] [--no-retry]

Authoring (always pass --profile <name>)

sub-cli post create <file> --profile <name> [--dry-run] [--title t] [--subtitle s] [--section name] [--cover url] [--audience a] [--slug slug]
sub-cli post list --profile <name> [--state draft|scheduled|published] [--limit n] [--json] [--no-retry]
sub-cli post update <id> --profile <name> [--section name] [--subtitle s] [--slug slug]
sub-cli post schedule <file> <time> --profile <name> [--audience a]
sub-cli post unschedule <id> --profile <name>
sub-cli post publish <file|--id id> --profile <name> --yes [--no-send] [--audience a]
sub-cli post delete <id> --profile <name> --yes [--force-published]
sub-cli section list --profile <name> [--json]
sub-cli section add <name> <description> --profile <name>
sub-cli section remove <name-or-id> --profile <name> --yes
sub-cli section set <section-name> <id...> --profile <name> [--no-retry]

Self-maintenance

sub-cli update   # installs the latest npm release of the CLI

After real commands the CLI may print a one-line "update available" notice on stderr, at most once a day. Silence it with SUBCLINOUPDATECHECK=1. Neither the notice nor update touches posts or profiles.

Common workflows

Create and publish a post

  1. Write the Markdown file with front matter (title required).
  2. Preview: sub-cli post create <file> --profile p --dry-run
  3. Create draft: sub-cli post create <file> --profile p

(returns the draft ID on stdout)

  1. Confirm with the human, then publish the draft:

sub-cli post publish --id <draft-id> --profile p --yes

Publishing via --id preserves the slug, section, and cover that post create already applied. Publishing directly from a file with post publish <file> only applies title, subtitle, and audience, and warns about the three it drops (slug, section, cover).

Schedule a post

  1. Write the file, confirm time and audience with the human.
  2. sub-cli post schedule <file> <ISO-time> --profile p
  3. To cancel: sub-cli post unschedule <id> --profile p

Crawl a newsletter and remix

  1. sub-cli feed crawl <url> --out ./drafts
  2. Crawled files use extra front matter keys (author, date, source_url,

publication) that post create warns about and ignores, so a crawled piece is a valid starting draft for your own writing.

  1. Edit the file, then post create as above.

Post file format

A post file is Markdown with YAML-ish front matter carrying all metadata: title (required), plus subtitle, section, cover, audience (everyone|onlypaid|onlyfree|founding), and slug. The body is Markdown; unsupported constructs fail loudly instead of being dropped.

cover is the header image and only ever a hosted http(s) URL; it never appears in the body. Body images written as local paths (relative to the Markdown file) are uploaded by every sending command and rewritten to hosted URLs, so keep the image files next to the post. A missing file stops the command instead of producing a post with a broken image, and --dry-run reports what would be uploaded without uploading it.