Discourse.Cli 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global Discourse.Cli --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Discourse.Cli --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Discourse.Cli&version=0.1.0
                    
nuke :add-package Discourse.Cli --version 0.1.0
                    

Discourse CLI (disc)

NuGet Downloads CI License: Apache 2.0 .NET

A user-centric command-line client for any Discourse instance, packaged as a .NET tool. It is read-first: the only write path is draft creation, which always hands off to a browser where a human reviews and clicks to publish.

This tool never publishes directly and cannot. Every mutation passes through a single choke-point (DraftGate) whose whitelist contains only draft routes. There is no code anywhere in this repository that constructs a publish request. The worst an automated caller (or a hijacked credential) can do is stage a draft that a human must still approve in the browser.

It is intended for interactive desktop use and is deliberately unsuitable for headless/CI runtime automation (building/testing/packaging the project in CI is of course fine).

Install

Once published to NuGet.org:

dotnet tool install --global Discourse.Cli

Or build and install from source:

dotnet pack src/disc/disc.csproj -c Release -o src/disc/nupkg
dotnet tool install --global --add-source ./src/disc/nupkg Discourse.Cli

Quick start

disc context add myforum --host forum.example.com   # register a target
disc login                                          # authorize (read scope)
disc list --top --json                              # popular topics
disc search --query "release notes"                 # search
disc read --topic 1234                              # read a topic
disc categories                                     # postable categories

To create drafts you must opt in to the write scope:

disc login --enable-drafts
disc draft --title "Hello" --category general --body "First post body"
# -> stores a draft and opens the browser. You click to publish.

Commands

Command Scope Description
context add\|use\|list none Manage named target forums
login [--enable-drafts] read / read,write User API Key browser flow
logout none Remove the local credential
whoami read Validate the stored credential
status none Show context, auth state, scopes
list [--top] [--category <s>] [--page <n>] read Topic lists
search --query "<q>" [--page <n>] read Keyword search
read (--topic <id> \| --post <id>) [--raw\|--cooked] read Read content
categories [--all] read Postable categories (permission == 1)
draft ... write Create a draft, hand off to browser
skill [--output <path>] none Generate agent-facing SKILL.md

Global options: --forum <host>, --context <name>, --json, --help, --version.

Authentication & storage

  • Authentication uses a User API Key obtained via a browser flow (RSA-2048 keypair generated locally; only the public key leaves the machine; the encrypted callback is decrypted with the private key and the nonce verified).
  • Default scope is read; --enable-drafts requests read,write (Discourse has no draft-only sub-scope, so honesty about the trade-off matters — the safety property comes from the tool's discipline, not from the credential).
  • Credentials are stored only in the OS credential store — never a plaintext file: Windows DPAPI (CurrentUser), macOS keychain (security), Linux libsecret (secret-tool). Keys are never printed or logged.

Exit codes

0 success · 2 usage · 4 auth required · 5 scope insufficient · 6 rate limited · 7 conflict · 8 forbidden · 9 not found · 10 network · 11 API error · 1 general.

Administrator setup guide (forum admin)

The User API Key flow requires a few Discourse site settings on the target forum. Settings are edited in Admin → Settings (search by name). Without the required ones, disc login cannot complete.

Required — the tool will not work without these

Site setting Value to set Why
allowed_user_api_auth_redirects add http://127.0.0.1:* The browser must redirect the encrypted key back to the CLI's loopback listener. If this does not match, Discourse returns 403 (rendered as "Oops! That page is private." / Forbidden) right after you authorize, the browser never returns, and disc login waits forever.
allow_user_api_key_scopes include read (and write for drafts) Bounds which scopes a key may request. disc requests read, or read,write with --enable-drafts.
user_api_key_allowed_groups include the issuing user's group Only members of these groups can generate a User API Key at all.

About the redirect allowlist. Entries are |-separated and matched with Discourse's WildcardUrlChecker, so http://127.0.0.1:* accepts the ephemeral port the CLI binds. This is safe: 127.0.0.1 is loopback — it can only ever point back at the authorizing user's own machine, never at a remote attacker, and the returned key is RSA-encrypted with a public key the CLI generated locally (only the CLI's private key can decrypt it). RFC 8252 endorses exactly this native-app loopback pattern.

If you prefer not to allow a wildcard port, pin the port instead and allow a single exact URL:

disc login --port 8765           # then allow-list exactly:
# allowed_user_api_auth_redirects  ->  http://127.0.0.1:8765/

Do not broaden the allowlist to non-loopback hosts (e.g. http://*, https://*.example.com) — those would be genuine open redirects that leak the key payload to remote servers.

  • Trim requestable scopes to least privilege. allow_user_api_key_scopes defaults to a mobile-app-oriented set (read|write|message_bus|push|notifications|session_info|one_time_password). If disc is your only client, narrowing it to read|write (or just read) reduces what any key — including one a user might be tricked into authorizing — can request. If you keep the broader set, note that one_time_password is the sharpest scope (it can bootstrap a logged-in web session); drop it unless the official Discourse mobile app needs it on this instance.
  • Restrict who can mint keys. Keep user_api_key_allowed_groups limited to a trusted group (e.g. a specific staff/dev group or a trust level). This shrinks the "socially-engineered authorization" surface more than scope trimming does.
  • Expire idle keys sooner. revoke user api keys unused days defaults to 180; 30 (or 7–14) is a reasonable tightening. Only idle keys are culled — an actively used key stays valid — and disc fails gracefully with exit code 4 ("auth required") so the user just re-runs disc login.
  • max_draft_length — the composer data payload must fit within this.
  • max_drafts_per_user — exceeding it makes POST /drafts.json return 403 (surfaced by disc as exit code 8).

Quick verification

disc context add myforum --host forum.example.com
disc login                 # authorize in the browser; should return to the CLI
disc whoami                # confirms the stored credential resolves to your user
disc categories            # confirms read scope + postable categories

If disc login hangs after you authorize and the browser shows "Oops! That page is private." / Forbidden, the allowed_user_api_auth_redirects entry above is missing or does not match — fix it and re-run.

Verification status

Confirmed against a live Discourse forum: the User API Key browser flow (RSA keypair → authorize → PKCS#1 payload decrypt → nonce check), the loopback auth_redirect requirement (a missing allowed_user_api_auth_redirects entry produces the 403 described above), and the read commands.

Still baseline / confirm before relying on (see docs/plan.md §15): the exact data shape for POST /drafts.json, whether a bare /new-topic restores the draft, and the precise meaning of a null category permission. These are implemented to the documented baseline and marked in code.

Build & test

dotnet build src/Discourse.Cli.slnx
dotnet test  src/Discourse.Cli.slnx

Repository layout:

src/
  Discourse.Cli.slnx     # solution
  disc/                  # the tool project (packs as 'Discourse.Cli', command 'disc')
  disc.Tests/            # xUnit suite
docs/plan.md             # design specification
.github/workflows/       # CI (build/test/pack) and Publish (NuGet on release)

Publishing (maintainers)

The project packs as a .NET tool (PackAsTool, command disc). Publishing to NuGet.org is automated by .github/workflows/publish.yml using Trusted Publishing (OIDC) — no long-lived API key is stored. NuGet.org introduced this in September 2025 as the recommended alternative to API keys: the workflow mints a short-lived GitHub OIDC token, the NuGet/login action exchanges it for a temporary (~1 hour) key, and that key is used to push.

One-time setup:

  1. On nuget.org, go to Account → Trusted Publishing and create a policy:
    • Package owner/username: your nuget.org username
    • Repository owner: rkttu · Repository: disc
    • Workflow file: publish.yml
    • Environment: production — the publish job runs in a GitHub environment named production, so the policy's Environment must match (or clear both).
  2. Set the user: input in publish.yml to that same nuget.org username.
  3. Create a GitHub Release with a tag like v0.1.0. The workflow derives the package version from the tag, packs, obtains a temporary key via OIDC, and pushes. (The first successful push claims the Discourse.Cli package id.)

Trusted Publishing is preferred over long-lived API keys. If you must publish manually, you can still dotnet nuget push with a personal API key, but rotate it and avoid storing it in CI.

License

Licensed under the Apache License 2.0. Copyright © 2026 rkttu.

If this tool is useful to you, consider sponsoring the author: github.com/sponsors/rkttu.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.2 139 7/5/2026
0.1.1 108 7/5/2026
0.1.0 130 7/5/2026 0.1.0 is deprecated because it is no longer maintained.