Substack CLI

Riga di comando · beta · non ufficiale

La newsletter dal terminale, senza pubblicare per sbaglio.

Crea bozze da Markdown, metti in coda le note e programmale, e se vuoi fatti scrivere una prima stesura da un LLM, anche locale. Pubblicare è sempre un tuo sì esplicito.

Bozza n. 1stdout
$ substack article draft post.md
Bozza creata: id 123456789

$ substack article publish 123456789
Errore: Pubblicazione non interattiva: aggiungi
--yes per confermare esplicitamente

$ substack note add "Una nota breve"
$ substack note schedule a1b2c3d4e5f6 \
    --at 2026-10-09T09:00:00+02:00

$ substack notes run-due   # cron / k3s

Senza --yes il comando si ferma prima di toccare la rete.

§ 1

Il prodotto

Substack CLI è un programma da riga di comando che porta la scrittura di una newsletter fuori dal browser: scrivi in Markdown con il tuo editor, il CLI crea la bozza online, tu la rivedi nell'editor di Substack e la pubblichi quando decidi. Le note brevi vivono in una coda locale e partono a mano o a orario.

Bozze da Markdown

Titoli, grassetto, corsivo, liste annidate, citazioni, codice e link diventano il corpo di una bozza online. Quello che non supporta (HTML grezzo, tabelle) lo rifiuta con un errore, senza perdere testo in silenzio.

Coda di note

Le note vivono in locale come bozze. Le pubblichi a mano o le programmi con un orario: notes run-due pensa al resto. Una nota dall'esito incerto non viene mai ripubblicata.

Generazione opzionale

Anthropic oppure qualunque server compatibile OpenAI (llama.cpp, Ollama, vLLM, LM Studio). Il testo generato diventa solo una bozza o una nota in coda: non pubblica mai nulla.

Su una macchina dedicata

Un'immagine Docker, i manifest per k3s (CronJob, PVC, Secret) e un timer systemd. Tre sole dipendenze di runtime: marked, yaml, zod.

§ 2

Come si usa

  1. Installa e compila

    git clone https://github.com/fullo/substack-cli.git
    cd substack-cli
    npm run setup

    Serve Node 22.18 o successivo. npm run setup installa le dipendenze, compila e lancia npm link, che registra il comando substack come collegamento simbolico al repository: ogni npm run build successivo è subito attivo. Se la cartella globale di npm non è scrivibile dal tuo utente, usa un gestore di versioni come nvm oppure un prefisso nella tua home, non sudo. Senza link il comando è node dist/cli/main.js. Per toglierlo: npm unlink -g substack-cli.

  2. Configura la pubblicazione

    substack config init --publication la-tua-newsletter --lang it

    È la parte prima di .substack.com. Con substack config show vedi la configurazione effettiva. I messaggi sono in inglese di default: --lang it li porta in italiano, comprese le parole di conferma. In alternativa c'è la variabile SUBSTACK_LANG=it, che ha la precedenza sul file di configurazione.

  3. Dai al CLI il cookie di sessione

    substack auth guide    # istruzioni passo passo
    substack auth set      # il valore non viene mostrato
    substack auth check    # verifica che la sessione sia valida

    Substack non ha API key: il CLI usa il cookie substack.sid del tuo account. Trattalo come una password e non incollarlo in chat o in file versionati. In alternativa puoi passarlo con la variabile SUBSTACK_SID.

  4. Scrivi e crea una bozza

    ---
    title: Il titolo dell'articolo
    subtitle: Un sottotitolo, se ti serve
    ---
    
    ## Un titolo di sezione
    
    Testo con **grassetto**, *corsivo* e un [link](https://example.com).
    
    - una lista
    - con due voci
    substack article draft post.md --dry-run   # prova, senza inviare nulla
    substack article draft post.md             # crea la bozza online
    substack article list                      # elenca le bozze

    Il file inizia con un front-matter: title è obbligatorio, subtitle facoltativo. Puoi anche passare il testo da un altro programma con cat post.md | substack article draft -.

  5. Trova e scarica i tuoi articoli

    substack article list --status published                  # id, titolo, link
    substack article list --status published --limit 50 --page 2   # pagine da 10, 25, 50 o 100
    substack article list --search "green coding"             # cerca tra gli articoli pubblicati
    substack article export 123456789 --out post.md           # un articolo come Markdown

    Ogni comando fa una sola richiesta e non scorre le pagine da solo. export scrive Markdown con front-matter; embed e widget senza forma Markdown diventano un commento e un avviso. Un «scarica tutto» non c'è, di proposito..

  6. Correggi una bozza senza aprire il browser

    substack article update 123456789 --title "Un titolo migliore" --dry-run
    substack article update 123456789 --title "Un titolo migliore"
    # Scrivi "aggiorna" per confermare:

    Vengono inviati solo i campi che cambiano, e gli articoli già pubblicati restano intatti. Con --file il file Markdown sostituisce corpo, titolo e sottotitolo: provalo prima su una bozza di prova, perché sovrascrive le modifiche fatte nell'editor web.

  7. Aggiungi immagini e copertina

    substack image upload copertina.png                          # carica e stampa l'URL
    substack article draft post.md --cover copertina.png         # carica, crea la bozza, imposta la copertina
    substack article update 123456789 --cover copertina.png --yes    # imposta la copertina su una bozza esistente
    substack article update 123456789 --cover https://substack-post-media.s3.amazonaws.com/... --yes   # oppure un URL già caricato

    Formati accettati: PNG, JPEG, GIF e WebP, fino a 10 MB. Con article draft --cover l'immagine si carica prima di creare la bozza; se poi l'impostazione della copertina fallisce, il messaggio riporta l'id della bozza e il comando per ripetere solo la copertina. Gli endpoint di caricamento e copertina non sono ancora confermati: dopo l'aggiornamento il CLI rilegge la bozza e, se la copertina non risulta impostata, esce con codice 3.

  8. Pubblica, con una conferma

    substack article publish 123456789
    # Scrivi "pubblica" per confermare:
    
    substack article publish 123456789 --yes                 # da script
    substack article publish 123456789 --yes --send-email    # anche l'email agli iscritti

    Da terminale ti viene chiesta una conferma digitata; da uno script serve --yes, altrimenti il comando si ferma subito. L'email agli iscritti parte solo con --send-email.

    substack article schedule 123456789 --at 2026-10-09T09:00:00+02:00
    substack article schedule 123456789 --at 2026-10-09T09:00:00+02:00 --email only_free
    substack article schedule 123456789 --cancel

    Con la programmazione è diverso: Substack non ha l'opzione «nessuna email», quindi con article schedule l'email parte sempre. Scegli i destinatari con --email (everyone di default, only_free, only_paid o founding); compaiono nel riepilogo prima della conferma.

  9. Note: coda locale, a mano o a orario

    substack note add "Una nota breve con un [link](https://example.com)"
    substack note list --status draft
    substack note schedule a1b2c3d4e5f6 --at 2026-10-09T09:00:00+02:00
    substack note publish a1b2c3d4e5f6        # subito, a mano
    substack note unschedule a1b2c3d4e5f6     # torna in bozza

    Le date vogliono sempre il fuso: +02:00 oppure Z per UTC. A orario le note vengono pubblicate da substack notes run-due, pensato per girare ogni minuto (vedi «Su una macchina sempre accesa»).

  10. Fatti aiutare da un LLM (facoltativo)

    # con un server locale, per esempio llama.cpp
    llama-server -m modello.gguf --port 8080
    substack config init --publication la-tua-newsletter --force \
        --provider openai-compat --llm-base-url http://localhost:8080 --model locale
    
    substack generate article --topic "Cos'è il green coding" --draft
    substack generate note --topic "Un consiglio sul green coding"

    Con --provider anthropic si usa invece l'API di Anthropic con la variabile ANTHROPIC_API_KEY. Il testo viene generato nella lingua del CLI, salvo --lang. L'articolo generato viene salvato in locale e, con --draft, diventa una bozza online; la nota va nella coda. Non viene pubblicato niente.

  11. Su una macchina sempre accesa

    docker build -f deploy/Dockerfile -t substack-cli:0.1.0 .
    docker save substack-cli:0.1.0 | sudo k3s ctr images import -
    
    kubectl apply -f deploy/k3s/namespace.yaml -f deploy/k3s/pvc.yaml
    read -rs SID && kubectl -n substack create secret generic substack-secrets \
        --from-literal=SUBSTACK_SID="$SID"; unset SID
    # in deploy/k3s/configmap.yaml sostituisci SOSTITUISCI-SUBDOMAIN con il tuo
    kubectl apply -f deploy/k3s/configmap.yaml -f deploy/k3s/cronjob.yaml

    Il CronJob esegue notes run-due ogni minuto. In alternativa, su una VM Linux, usa il timer systemd in deploy/systemd. Quando il cookie scade il comando esce con codice 2 e il Job risulta fallito, così te ne accorgi.

Riferimento dei comandi

auth guide · set · check · loginOttieni, salva e verifica il cookie (login apre un browser, con Playwright installato a parte).
config init · showCrea e mostra la configurazione (--lang en|it).
article draft <file|->Crea una bozza da Markdown. --cover, --dry-run.
article listElenca le bozze, o gli articoli pubblicati con --status published (id, titolo, link). --limit 10|25|50|100, --page, --search.
article export <id>Scarica un articolo come Markdown con front-matter. --out, --force.
article update <id>Modifica una bozza non pubblicata. --title, --subtitle, --file, --cover, --yes, --dry-run.
article publish <id>Pubblica una bozza. --yes, --send-email, --dry-run.
article schedule <id>Programma con lo scheduler di Substack (--at) o annulla (--cancel). --email sceglie i destinatari, --yes, --dry-run.
image upload <file>Carica un'immagine (PNG, JPEG, GIF, WebP, max 10 MB) e stampa l'URL.
note add <testo|->Aggiunge una nota alla coda locale.
note list · schedule · unscheduleElenca (--status), programma (--at) o rimette in bozza.
note publish · resolvePubblica subito; resolve --published|--retry chiude una nota rimasta in dubbio.
notes run-duePubblica le note programmate scadute (cron, systemd, k3s).
generate article · noteGenera il testo con un LLM (--topic, --lang, --provider, --draft).

Quasi tutti i comandi accettano --json per un output leggibile da altri programmi.

§ 3

Casi d'uso

Quattro modi di usarlo, dal più semplice al più automatico.

Dall'editor alla bozza

Scrivi l'articolo come file Markdown, magari in un repository Git insieme agli altri. Quando è pronto lo mandi a Substack e lo rivedi nel loro editor.

substack article draft post.md --dry-run
substack article draft post.md
# apri il link, rivedi, poi:
substack article publish 123456789

Scrivere con un assistente

Il testo lo prepara un assistente (per esempio un assistente di codice nel terminale) e il CLI lo trasforma in bozza passandolo da stdin. Tu decidi se e quando pubblicare.

cat bozza-dell-assistente.md | substack article draft -

Note programmate per la settimana

Prepari in una volta le note e fissi gli orari. Un timer sulla tua macchina le pubblica al momento giusto; se qualcosa va storto (cookie scaduto, Substack che non risponde) lo vedi come errore, e una nota dubbia non viene mai ripubblicata.

substack note add "Lunedì: nuova puntata online"
substack note schedule a1b2c3d4e5f6 --at 2026-10-12T09:00:00+02:00
substack notes run-due        # ogni minuto, da cron

Prima stesura con un modello locale

Un modello che gira sul tuo computer (llama.cpp, Ollama…) scrive una prima stesura sul tema che indichi. Il risultato diventa una bozza online da rifinire: niente esce senza la tua revisione.

substack generate article \
    --topic "Cos'è il green coding" --draft
§ 4

Regole di redazione

Il progetto è nato con una domanda: come impedire che uno script pubblichi, o mandi un'email a tutti gli iscritti, quando non doveva? Le risposte:

  • Pubblicare richiede --yes quando non c'è un terminale, oppure una parola digitata a mano. Senza terminale il controllo avviene prima di qualsiasi richiesta di rete.
  • L'email agli iscritti parte solo con --send-email. Di default non si invia nulla.
  • Il cookie di sessione non finisce mai in output, log, file di dati o messaggi d'errore, e viene inviato solo a host substack.com. La chiave Anthropic va solo ad api.anthropic.com.
  • Link e immagini solo con schemi sicuri (http, https, mailto); niente HTML grezzo; caratteri invisibili e di controllo rifiutati.
  • Nessuna doppia pubblicazione: la coda delle note è protetta da un lock, e una nota di cui non si sa se sia uscita resta ferma finché non decidi tu.
§ 5

In numeri

398test: unitari, funzionali (CLI vero contro server finti) e di sicurezza.
96%mutation score su 1523 mutanti.
3dipendenze di runtime, senza dipendenze transitive: marked, yaml, zod.
0vulnerabilità note nelle dipendenze di runtime (npm audit).
§ 6

Documenti