Skip to content

Build brief: substack-cli — agent-first management of a Substack publication and account #1

Description

@OriNachum

Welcome to the AgentCulture mesh. This is your onboarding brief and the source
of truth for what substack-cli is for. It is self-contained: read it, then
run /init to expand the CLAUDE.md seed into a real runtime prompt, and build
from here.

1. What this repo is

substack-cli lets agents manage a Substack publication and account
publish and schedule posts, read posts and comments, run audience and post
statistics, and manage subscribers — from the command line, agent-first.

Name
Repo / agent suffix substack-cli
Console command substack
Import package substack_cli
PyPI distribution substack-cli (already live — genesis 0.9.0 published)

The import package is deliberately decoupled from the command: a top-level
substack import would shadow the existing third-party clients on PyPI
(python-substack imports as substack). Keep it substack_cli.

This is an unofficial community tool, not affiliated with or endorsed by
Substack.
Carry that disclaimer in the README, learn output and the PyPI
description (the qodo-cli precedent).

2. The fact that shapes everything: three access tiers, one of them unofficial

Substack does have an official
Developer API
(introduced 2026, governed by its own Terms of Use),
but it is narrow: its documented surface is public-profile lookup for
creators, gated by a ToS agreement and a manual approval (7–10 business days).
It is not a publishing, stats or subscriber-management API.

So the verbs the operator asked for sit on three different tiers:

Tier What it covers Stability
Official Developer API public creator-profile data documented, ToS-gated
Public surfaces a publication's RSS feed and public archive/post pages stable-ish, no auth
Internal web-app endpoints drafts, publish, schedule, comments, dashboard stats, subscribers undocumented — can change without notice

Requirements that follow:

  1. Every verb declares its tier — in explain, in --json output, and in
    its docs. An agent must be able to tell whether the thing it just called
    rests on a documented contract or on a reverse-engineered one.
  2. All internal-endpoint calls live behind one adapter module, with
    recorded response fixtures. When Substack changes a shape, one module and
    its fixtures change — not every verb. Detect a shape change as a typed
    error ("Substack's internal API response changed"), never a KeyError
    traceback.
  3. The ToS question is a parked operator decision, not yours to settle
    silently
    (see §6). Build so the internal tier can be disabled wholesale.

Prior art worth reading before designing your own client (cite, don't import):
python-substack (Markdown →
drafts, with its own CLI + MCP) and
substack_api (read-only unofficial
wrapper). Evaluate them; don't take a hard runtime dependency on an
unofficial wrapper whose breakage you can't fix.

3. Design constraints (the real content of this brief)

3.1 Sending an email cannot be undone — separate it from publishing

The AgentCulture rule is that every write verb defaults to dry-run and
--apply commits. For Substack that isn't enough on its own. Publishing a
post can also email every subscriber, and an email cannot be recalled.

  • Keep "publish to the web" and "send to subscribers' inboxes" as separate,
    explicit decisions
    . Emailing needs its own flag (e.g. --send-email) in
    addition to
    --apply. It never happens as a side effect of publishing.
  • The dry-run of a send must show what will go out and to how many
    recipients
    (and which audience: all / paid / free / a section).
  • Scheduling is a write, too. Unscheduling and deleting are writes. All of them
    default to dry-run.
  • Agents call CLIs in loops, so make sends idempotent: re-running the same
    publish must not email the list twice.

3.2 Auth: a session, never a password on disk

The internal tier authenticates with a web session (a cookie), not an API key.

  • Never store the account password. Read the session from the environment or
    from grant, the mesh's
    agent-first secrets manager, rather than inventing a config file.
  • whoami is the smallest auth probe: which account, which publications, and
    whether the session is valid. An expired or missing session is a
    first-class typed error with a remediation hint, not a bare 401/403.
  • Accounts can own several publications. Every publication-scoped verb
    takes an explicit --publication. Never default to "the first one" for a
    write.
  • If headless email login turns out to be gated (captcha, magic link),
    don't build browser automation here. Open a seam issue on
    webglass-cli, which owns
    agent-first browsing.

3.3 Subscriber data is personal data

Subscriber lists are other people's email addresses.

  • Default output is counts and aggregates, not addresses.
  • Individual addresses appear only on an explicit verb that writes to an
    explicit --output PATH. Never write them to stdout by default, never log
    them, never commit them. Fixtures must use synthetic addresses.
  • Adding, comping or removing subscribers is a write verb (dry-run default),
    and bulk imports show a count and a sample before --apply.

3.4 Content is a document tree, not a string

Substack's editor stores post bodies as a structured document (verify the
current schema against the live editor; it has historically been a
ProseMirror-style JSON tree), not as Markdown or HTML.

  • Make Markdown ⇄ Substack body a pure function, separate from HTTP, with
    round-trip tests: md → body → md must be stable on a fixture corpus.
  • Document which Markdown constructs are lossy (embeds, footnotes, buttons,
    paywall markers, subscribe widgets) instead of silently dropping them.
  • Images need uploading through Substack before a body can reference them.
    Keep that a separate step so a dry-run can show "3 images would be
    uploaded".

3.5 Statistics: report Substack's numbers with their definitions, don't invent your own

  • Every metric carries its source (which dashboard figure or endpoint),
    its time window and its as-of timestamp.
  • "Not available" is not zero. Stats that the account's plan or role can't
    see must say so.
  • Open rates are inflated by mail-client privacy features that pre-fetch
    images (Apple Mail Privacy Protection being the famous one). Label opens
    accordingly rather than presenting them as reads.
  • Any derived metric you compute (growth rate, conversion, cohort retention)
    is labelled as derived, with its formula available via explain.

3.6 Reading other people's publications: public data only

  • Reading your own publication's content, drafts and comments is in scope.
  • Reading other publications is limited to what they publish publicly (feed,
    public posts). No bulk archive scraping, no paywall circumvention. Paid
    content is readable only when the authenticated account is entitled to it.
  • Throttle: low default concurrency, back off on 429/5xx, and handle
    pagination inside the client so no verb has to remember it.

3.7 Tests are network-free

The unit suite must run with no Substack account and no network: recorded
fixtures behind the adapter, with the HTTP layer injectable. Any live smoke
tests are opt-in, marked, and skipped in CI.

4. A starting verb surface (a sketch — you own the final shape)

  • Read: whoami, publication list|show, post list|show,
    comments list, feed read <publication> (public tier)
  • Write: draft create|update --from FILE.md, draft preview,
    post publish [--send-email], post schedule --at, post unschedule
    (all dry-run by default)
  • Stats: stats posts, stats audience, stats growth
  • Subscribers: subscribers count, subscribers export --output PATH,
    subscribers add|remove (write, dry-run default)
  • Plus the template's agent-affordance verbs (learn, explain) with every
    verb documented in the explain catalog, including its tier from §2.

Suggested order: M1 read-only (whoami, public feed, own posts, stats read),
then M2 drafts (the Markdown ⇄ body function plus create/update), then
M3 publish/schedule with the §3.1 email guard, then M4 subscribers.
Shipping read-only first gives the mesh value before any irreversible verb
exists.

5. Lanes and seams (open issues rather than absorbing)

  • notion-agent: the closest sibling in shape (a SaaS control CLI with a
    token, pagination and rate limits, and a document-tree body format). Read its
    brief and design before designing yours. Converge on shared patterns rather
    than diverging accidentally.
  • storybook-cli: produces shareable write-ups of agent work. A natural
    consumer of substack draft create. Offer it a seam; don't render sites
    here.
  • grant: where the session secret should live (§3.2).
  • webglass-cli: any browser-driven login or scraping fallback (§3.2).

6. Parked unknowns (settle these with the operator, don't guess)

  1. ToS stance on the internal tier. Automating Substack's undocumented
    web-app endpoints may conflict with Substack's terms. Whether this tool
    ships the internal tier on by default, behind an opt-in, or not at all is
    the operator's call. Surface it before M2.
  2. Which account and publication(s) this will run against. Unknown today.
    Nothing in the repo may hardcode one.
  3. Official Developer API access: whether to apply for it (the approval is
    manual and slow) and whether its profile surface earns a verb at all.
  4. Notes and Chat (Substack's short-form and community surfaces): in or
    out of scope? Parked. Don't build them in M1–M4.
  5. Own client vs. building on prior art (§2): your evaluation, reported
    back as a decision with reasons.

7. Housekeeping owed

  • Run /init to expand the CLAUDE.md seed into a full runtime prompt.

  • Keep culture.yaml (backend: claude) and the prompt file consistent.

  • Add the unofficial-tool / trademark disclaimer (§1).

  • The canonical skill kit is already vendored from the template, and you're
    registered in guildmaster's docs/skill-sources.md ledger. Skill updates
    will arrive as guild teach issues.

  • guildmaster (Claude)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions