Guide for using the cupthread CLI command-line tool to manage workspaces, apps, feedback inbox, feature requests, roadmaps, changelogs, and integrations.
Installs into .claude/skills of the current project.
Are you the author of Cupthread Cli?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/cupthread-cupthread-cli)
---
name: cupthread-cli
description: Guide for using the cupthread CLI command-line tool to manage workspaces, apps, feedback inbox, feature requests, roadmaps, changelogs, and integrations.
---
# CupThread CLI Guide
The `cupthread` CLI allows developers and AI agents to manage all resources on [CupThread.com](https://cupthread.com) (workspaces, apps, feedback inbox, feature requests, roadmap columns, versions, changelogs, integrations, billing) directly from the terminal.
---
## 1. Installation
If `cupthread` is not yet installed or not found in `$PATH`:
### Option A: Homebrew Tap (Recommended)
```sh
brew tap CupThread/tap
brew install cupthread
# Or one-liner:
brew install CupThread/tap/cupthread
```
### Option B: Go Install (requires Go 1.25+)
```sh
go install github.com/CupThread/CupThreadAgenticCoding/cmd/cupthread@latest
```
### Option C: Build from Source
If working directly inside the `CupThreadAgenticCoding` repository:
```sh
go build -o bin/cupthread ./cmd/cupthread
```
---
## 2. Authentication
### Method A: Browser / Device OAuth (Interactive)
```sh
# Browser OAuth flow
cupthread auth login
# Headless / SSH device code flow
cupthread auth login --device
```
The browser flow binds a random `127.0.0.1` port and uses `http://127.0.0.1:<port>/cupthread/callback` as its redirect URI — the loopback exception in the server's OAuth redirect-URI scheme policy (SEC-46); every non-loopback redirect URI must be `https:`.
### Method B: Personal Access Token (CI / Agents)
```sh
# Stored token
cupthread auth login --token cpt_...
# Or inject via environment variable (overrides stored credentials)
export CUPTHREAD_TOKEN="cpt_..."
```
Check current authentication status:
```sh
cupthread auth status
```
Remove stored credentials from this machine (local only — revoke tokens
server-side in the Console under Settings → API Tokens):
```sh
cupthread auth logout
```
Logging in against a non-default API endpoint (`--base-url <url>` or `$CUPTHREAD_BASE_URL`) stores that
endpoint in the config until `auth logout`, so later invocations without the flag reach the same server
instead of silently falling back to production. Flags and env still override it per invocation; when a
non-default endpoint is stored, `auth status` shows it as "Credential issued for".
Switching accounts: `cupthread auth logout` clears the credential plus the saved default workspace,
per-workspace app defaults and base URL (back to pristine first-run state); `cupthread auth login`
drops saved defaults the new account cannot see (with a warning) instead of silently targeting the
previous user's workspace. Add `--revoke` to also invalidate the stored OAuth token pair server-side
(best-effort POST of the refresh token to the RFC 7009 `/api/v1/oauth/revoke` endpoint, which
cascades to the paired access token). PATs cannot be revoked from the CLI — the token-management API
requires an interactive Console session — so `--revoke` then prints the Console path
(Settings → API Tokens) instead of sending a request. The interactive OAuth flows store the token
pair as soon as the server issues it; the post-login session check is advisory — if it fails, login
still succeeds with a warning on stderr and saved workspace defaults are cleared because they could
not be verified (`cupthread auth status` confirms the session once the API is reachable again).
---
## 3. Global Flags & Output Options
- `--json`: Shorthand for `-o json` (emits indented machine-readable JSON). **Recommended for AI agents.**
- `-o, --output <table|json|yaml>`: Select output formatting (default `table`).
- `-w, --workspace <id>`: Target workspace ID (overrides default).
- `-a, --app <id>`: Target app ID (overrides default).
- `--base-url <url>`: API endpoint override (default `https://api.cupthread.com`; a non-default login is
remembered until `auth logout`).
- `--no-retry`: Disable automatic retry/backoff on transient failures. By default body-less GET requests
that answer 429/502/503/504 are retried up to 3 times with capped exponential backoff (honoring
`Retry-After` when present), so a mid-batch blip no longer aborts a command; mutations (POST/PUT/
PATCH/DELETE) are always single-shot. Each retry logs one line to stderr (never stdout); `api request
--json` error payloads add `"attempts"` when retries were exhausted. `$CUPTHREAD_NO_RETRY=1` is the
env equivalent. Response bodies are buffered under a cap (10 MiB for API responses, 1 MiB for
OAuth/token responses): a larger response fails immediately with a
`response body exceeds the … response-size limit` error naming the endpoint, and is never retried.
---
## 4. Key CLI Commands
### Workspaces & Context
```sh
cupthread workspaces list # List all available workspaces
cupthread workspaces use <workspace-id> # Set active workspace context
cupthread me # Show current user, workspaces, and roles
cupthread workspaces members list # List workspace members & roles
cupthread workspaces invitations list # List pending invitations
cupthread billing show # Show subscription tier (free | pro), limits, and usage
```
All of the above are `[token-safe]`: any workspace role can run them and `cpt_`
API tokens work.
**Console-only (interactive session required — AUTH-01):** member mutations
(`workspaces members invite/add/set-role/remove`, `workspaces invitations revoke`) and billing
changes (`billing checkout/portal/addons`) are interactive-session-only: every CLI credential — a `cpt_`
personal access token and the browser OAuth login alike — fails with `403 interactive_session_required`;
perform these actions in the Console web UI (see Agent Best Practices #6).
**Destructive commands are confirm-guarded** (`features delete`, `columns delete`, `versions delete`,
`changelog delete`, `workspaces members remove`, `imports cancel`): the server hard-deletes these objects with
no undo, so the CLI refuses them on a non-interactive stdin unless `--yes`/`-y` is passed — the refusal fires
BEFORE any id resolution or HTTP request, so a wrong id costs nothing. On an interactive terminal the command
prints `About to … Continue? [yN]` on stderr instead (an answer other than `y`/`yes` aborts with nothing sent).
Scripts and agents must pass `--yes` explicitly (see Agent Best Practices #7).
### Apps Management
```sh
cupthread apps list # List apps in current workspace
cupthread apps use <app-id> # Set active app context
cupthread apps get <app-id> # Show app details and configuration
cupthread apps create --name "My App" # Create a new app
cupthread apps update <app-id> --icon ./icon.png # Upload an app icon (PNG/JPEG/WebP/GIF, or screened SVG;
# requires workspace admin/owner). A declared type that does not
# match the file content fails with 415 "unsupported image type".
# Size cap (SEC-36): files over 10 MB are rejected client-side
# (nothing is sent); the server answers larger payloads with
# 413 payload_too_large.
# Update order: metadata flags are PUT first, icon uploaded last;
# name/slug/URL/platform values are validated locally first and a
# failed icon upload after an applied PUT reports
# "partially applied" (JSON errors carry applied/failed lists).
cupthread apps public-config <app-key> # Show the public portal config (no login required);
# also accepts --workspace-slug <slug> --app-slug <slug>;
# private apps fail with 404 like unknown keys (fail-closed);
# rate limited per client IP (60/min, 429) before lookup
cupthread apps public-changelog <app-key> # Fetch the public changelog feed (no login required);
# cursor-paginated: follow --cursor <nextCursor> until hasMore=false.
# Anonymous 200s may be up to 30s stale (shared cache); a 429
# is retried with the other body-less GETs.
cupthread apps public-feature-requests <app-key> # Fetch the public feature-request feed (no login required);
# keyset-cursor-paginated (DATA-01): follow --cursor <nextCursor> until
# hasMore=false; --offset is ignored when --cursor is set, and an offset
# above 10000 is clamped to 10000 (still 200, not an error); --q filters.
# Plain listings are rate limited per client IP (60/min, 429 JSON)
```
The management commands above are `[token-safe]`; `apps create`/`update`
need the workspace `app.configure` capability (admin/owner).
### App Settings (anonymous access & SDK appearance)
```sh
cupthread apps settings # Show settings of every app in the workspace
cupthread apps settings show <app-id-or-slug> # Show one app's settings
cupthread apps settings set <app-id-or-slug> --anon-vote=false # Toggle anonymous voting
# (also --anon-roadmap/--anon-feedback/--anon-changelog)
cupthread apps settings set <app-id-or-slug> --input ./settings.json # Raw JSON body, e.g. {"sdk":{"theme":"dark"}}
```
`[token-safe]` but needs `app.configure` (workspace admin/owner). These flags
drive what anonymous visitors can do on the public portal (roadmap view,
voting, feedback, changelog) and the SDK appearance (`sdk.theme`).
### Feedback Inbox Triage
```sh
cupthread inbox list # List recent feedback submissions
cupthread inbox list --triage-status open --json # Filter: open | in_progress | resolved | archived | active
cupthread inbox list --assigned-to unassigned --q "crash" # Assignee filter + title search
# --q is a literal substring: %, _ and \ are escaped
# server-side, so "100%" matches "100%" only (QUAL-05)
cupthread inbox get <feedback-id> # Triage detail: attachments, delivery attempts, activity log
cupthread inbox priority <feedback-id> !!! # Raise priority (! / !! / !!!)
cupthread inbox triage <feedback-id> in_progress # Set triage status: open | in_progress | resolved | archived
cupthread inbox assign <feedback-id> <clerk-user-id> # Assign to a workspace member (omit ID to unassign)
cupthread inbox bulk-triage <id1> <id2> --triage-status resolved # Bulk update 1-50 submissions (also --assignee / --unassign)
cupthread inbox retry <feedback-id> # Retry GitHub forwarding for a failed delivery
cupthread inbox deliveries # Show the GitHub delivery queue (status, attempts, last error)
```
The whole `inbox` group is `[token-safe]` (workspace.read / content.manage).
> Delivery `status` (`received` / `forwarded` / `forward_failed`) is managed by
> the platform and is separate from the triage lifecycle — use `inbox triage`
> (never a `--status` flag) to change triage state.
### Notifications
```sh
cupthread notifications list # List notifications, newest first (table ends with the unread count)
cupthread notifications read <notification-id> # Mark one notification as read
cupthread notifications read-all # Mark every notification as read
cupthread notifications prefs show # Show per-channel (inbox/email) notification preferences
cupthread notifications prefs set --channel inbox --all-events # Enable every event type on a channel
cupthread notifications prefs set --channel email --events "delivery.failed,import.failed" --enable # Fine-grained event mask
```
The whole `notifications` group is `[token-safe]` (workspace.read). Event
types accepted by `--events` are the ones shown by `prefs show`
(`feedback.received`, `feature_request.approved`, `delivery.failed`, …).
### Feature Requests & Roadmap
```sh
cupthread features list # List feature requests
cupthread features list --sort revenue # Sort by user ARR/MRR (Pro plan)
cupthread features get <request-id> # View feature request details (requester info, commenters)
cupthread features create --title "Dark mode" --description "Add dark theme support"
cupthread features update <request-id> --title "New title" --column-slug planned # Edit / move / re-version
# (also --description, --version-id, --approved)
cupthread features approve <request-id> # Approve a pending request
cupthread features forward <request-id> --target issue --labels bug,confirmed # Forward to GitHub
# (--target discussion|issue; repo defaults to the app's GitHub config)
cupthread features delete <request-id> --yes # Permanently delete incl. votes & comments (confirm-guarded)
cupthread columns list # List public roadmap columns
cupthread versions list # List release milestones / versions
```
All of the above are `[token-safe]` (triage / content.manage capabilities).
`columns list` and `versions list` are the authenticated console listings.
The public portal reads (`GET /api/v1/public/columns/{appKey}` and
`GET /api/v1/public/versions/{appKey}`, via `cupthread api request`) are
separate: they share a 60 requests/minute per-IP budget with the public
comment thread and changelog feed, and an anonymous `200` can be up to 30
seconds stale.
`features list` reads the console (workspace-scoped) listing. The ID-taking
commands (`features get/update/approve/delete/forward`) resolve
`<request-id>` within the **resolved app** — the `--app` flag, else the saved
default from `apps use` — so an ID from another app in the same workspace
fails with "not found" instead of being mutated; with no app resolved the
lookup stays workspace-wide. Resolution pages through the whole workspace
listing (200 per page), so a request past the newest page still resolves;
prefix ambiguity is judged across every page, and resolution gives up after
50 pages (≈10k requests) with a clear error. To walk the
**public** feed an end user would see, use
`cupthread apps public-feature-requests <app-key>` — keyset-cursor-paginated
(DATA-01): start without `--cursor`, then echo each page's `nextCursor` back
until the table reports no more pages. `hasMore`/`nextCursor` are always
present in the `--json` output (`nextCursor` is `null` on the last page), and
`--offset` is ignored whenever `--cursor` is set.
### Imports (GitHub / Linear / Notion / Slack)
```sh
cupthread imports create --source github_issues --mode preview --owner acme --repo app # Preview (default);
# --mode commit actually creates the requests
cupthread imports list # List import jobs of the current app
cupthread imports history # List every import job in the workspace
cupthread imports get <job-id> # Poll a job; --json includes preview candidates (.job.candidates)
cupthread imports rerun <job-id> # Re-run a job (--mode preview|commit, --include-duplicates)
cupthread imports cancel <job-id> --yes # Cancel a queued/running job (confirm-guarded; cannot be resumed)
```
The whole `imports` group is `[token-safe]`. Preview jobs complete
synchronously; commit jobs run on the queue — poll with `imports get` until
the status leaves `queued`/`running`. Every import source requires Pro
(`402` `tier_limit_import_pro`). The subscription tier is only `free` or `pro`
— there is no `business` tier.
### Integrations (GitHub / Linear / Notion / Slack)
```sh
cupthread integrations status # Connection status of every integration (account, connected)
cupthread integrations github repos # GitHub repositories accessible to the integration
cupthread integrations github categories --owner acme --repo app # Discussion categories of a repository
cupthread integrations github config <app-id-or-slug> --owner acme --repo app # Per-app repo + sync options
# (also --category-slug, --sync-enabled, --status-sync, --comments-sync)
cupthread integrations linear status # Per-provider status (also notion, slack)
```
The reads and the per-app GitHub config above are `[token-safe]` (workspace
read; `github config` needs `app.configure`, admin/owner).
**Console-only (interactive session required — AUTH-01):**
`integrations github|linear|notion|slack auth-url/connect/disconnect` and
`integrations github sync` reject `cpt_` tokens with
`403 interactive_session_required` — connect or sync integrations from an
interactive login or the Console web UI.
### Comments & @Replies
```sh
cupthread comments list <featureRequestId> # List comments on a feature request
cupthread comments create <featureRequestId> --body "Great idea!" [--reply-to <clerkId>] [--parent-id <commentId>]
```
`comments create --parent-id` must reference a visible comment on the same
feature request (not hidden or deleted). The API rejects anything else with
`400 invalid_parent` — "The comment you are replying to was not found on this
feature request" — and stores nothing; the CLI surfaces that as an explicit
error. Take `--parent-id` from `comments list` on the same request, and don't
bother crafting `--reply-to-author-name`: the server always resolves the
replied-to author from the parent comment itself.
Both list commands walk the thread's keyset pagination (PROD-31: 200
comments per request, `limit`/`cursor`) to the end, so threads longer than
one page still list completely, and the trailing count comes from the
server's authoritative `total`.
`comments list` calls the public thread GET. That route shares a 60
requests/minute per-IP budget with the public changelog feed and the public
columns and versions listings, and answers
`429 {"error": "Too many requests. Please try again shortly."}`. The CLI
retries that `429` on the body-less GET. The thread body is the same for
every caller, so a `200` may come from the 30-second shared cache and a
comment posted a moment ago can be missing.
`comments moderation list` is the console route and is not this cache.
### Comment Moderation (workspace)
```sh
cupthread comments moderation list <featureRequestId> # All comments incl. hidden ones (404 if not in workspace)
cupthread comments moderation hide <commentId> # Hide a comment from public portals
cupthread comments moderation unhide <commentId> # Restore a hidden comment
cupthread comments moderation delete <commentId> # Permanently delete a comment (404 if not in workspace)
```
The moderation group is `[token-safe]` (content.manage) and works from any
workspace role whose capabilities include it.
### User Profiles
```sh
cupthread users profile <userId> # Look up a public developer profile, apps, and comments
cupthread users profile u_9f2c… --app-key key_live_… # App-scoped u_* ids from board/comment payloads need --app-key
```
User ids on public boards and comments are app-scoped pseudonyms (`u_<32 hex>`); they only resolve within their app, so pass the app's key with `--app-key`. Legacy `user_*` ids still work without it. Avatar fields in profile and board payloads are always `null` or a managed `https:` URL (PRIV-19) — the CLI prints `—` for `null`; when consuming the `--json` output, render a placeholder rather than assuming the value's host.
### Changelog & Releases
```sh
cupthread changelog list # List published and draft changelogs
cupthread changelog list --limit 50 --offset 100 # Page through large changelogs (server default: 100/page)
cupthread changelog create --title "v1.2.0" --body-file ./release-notes.md --publish-now
cupthread changelog update <entry-id> --title "v1.2.0" # Edit a draft (also --body-file, --version-id,
# --version-label, --link-request-ids, --schedule-at)
cupthread changelog unpublish <entry-id> # Revert a published entry to draft
cupthread changelog delete <entry-id> --yes # Permanently delete a draft (confirm-guarded)
```
Draft editing, `unpublish`, and `delete` are `[token-safe]` (content.manage;
clearing a schedule with `--schedule-at ""` stays token-safe too).
`changelog list` (console) is offset/limit-paginated and reports `total` +
`hasMore`; the table view prints the next `--offset` when more pages remain.
The public feed (`apps public-changelog`) instead uses opaque keyset cursors.
Drafts (create/edit/delete without publishing) work for every workspace role
and with `cpt_` API tokens. Publishing, `--publish-now`, and setting a
schedule with `--schedule-at <datetime>` require the admin/owner
`changelog.publish` capability (SEC-40): every CLI credential — a `cpt_`
API token and the browser OAuth login alike — fails with `403
interactive_session_required` (publish in the Console web UI instead), and
for member-role callers with `403 capability_required` — the CLI appends
both hints to the error. Clearing a schedule is not gated:
`changelog update <id> --schedule-at ""` sends `scheduledAt: null` and only
needs plain `content.manage` (an empty string would 400 on the datetime
schema, so the CLI converts it to null).
### Search
```sh
cupthread search "crash on login" # Global fuzzy search across apps, feedback, and roadmap
```
`search` covers workspaces, apps, feature requests, roadmap versions, changelog entries, and **feedback submissions** (QUAL-06): a `feedback_submission` result matches the submission title/description/reporter name and carries its triage status — `--json` output passes the type through verbatim, so parse `type` as an open string.
### Raw API Passthrough
Agents can invoke any API endpoint directly:
```sh
cupthread api request GET /api/v1/console/me --json
```
Every CLI request carries an `X-Request-Id` correlation header (`cli-<uuid>`; the API echoes it on every response). CLI errors quote the server-echoed value as `request-id=…`, and `api request` prints it on success lines — include that value verbatim in bug reports and support requests so the exact request can be found server-side.
Validation failures (HTTP 400) carry the server's field-level `details` (zod flatten: `{formErrors: [...], fieldErrors: {field: [reasons]}}`). Every command's error line names the offending fields and reasons (e.g. `Validation failed (HTTP 400): title: Title must be at least 3 characters`), and in `--json` mode `api request` includes the raw `details` object in the error payload — read it instead of guessing which input to fix.
`--input @file` (or `"-"`/`"@"` for stdin) sends the body as JSON and is strict: the file must contain exactly one JSON value. A second value or stray text after it fails the command with `parse input JSON: unexpected trailing data` before anything is sent — fix the file rather than retrying. Request bodies are size-capped server-side (SEC-36): 1 MB on console routes, 256 KB on public routes — over-limit bodies answer `413 {"error": "Payload exceeds size limit", "code": "payload_too_large"}`. The CLI also enforces those caps locally before sending: every file/stdin body (`api request --input`, `sign-user-attrs --input`, `apps settings set --input`, `imports run --options`, `changelog create/update --body-file`, plus piped `--secret`/`--token` values at 64 KB) is read through a bounded reader, and input over the route's cap fails in-process with `input exceeds the <n> request-body limit` — nothing reaches the network. In `--json`/`-o yaml` mode the command emits one structured `{error, code: "input_too_large", limit}` document on stdout and still exits 1 with the `Error: …` line on stderr.
Exit codes match the typed commands: on any API error (4xx/5xx) the `--json`/`-o yaml` payload (`{error, code, status, hint}`) still reaches stdout, but the command exits 1 and prints the `Error: …` line on stderr — branch on `$?` first, then parse the payload.
### Repository & Skills Management
```sh
# Inspect git status of local CupThread repositories
bin/cupthread status --json
# Symlink skills into target project (.agents, .claude, .zcode)
# Works from any installed binary: a verified CupThreadAgenticCoding checkout
# is symlinked; otherwise the skills embedded in the binary are copied.
bin/cupthread skills list
bin/cupthread skills link /path/to/target/project
```
### SDK Payment-Attribute Signing Helper
Bodies sent to `PUT /api/v1/public/apps/{appKey}/user` that report payment attributes (`isPaying`, `mrr`, `plan`, `currency` — an explicit `null` counts) must carry an HMAC-SHA256 `signature` + `timestamp` (contract: API skill, "SDK Payment-Attribute Signing (DATA-03)"; since DATA-07 `currency` is signed too, and a signed update that omits `currency` preserves the stored value instead of defaulting to `USD`). The CLI computes reference signatures so coding agents can cross-check platform implementations:
```sh
# Recommended: keep the signing secret off the command line (shell history / ps)
printf %s "$CUP_SDK_SECRET" | cupthread api sign-user-attrs --app-key app_demo12345 \
--user-token 3fa85f64-5717-4562-b3fc-2c963f66afa6 --secret - --input ./user-attrs.json
# or export CUPTHREAD_SDK_SIGNING_SECRET=cpt_sk_... once, then omit --secret entirely
```
`--secret` takes the SDK signing secret inline, as `-`/`@` (read from stdin, trailing whitespace trimmed), or falls back to `$CUPTHREAD_SDK_SIGNING_SECRET` (also trimmed) when omitted; precedence is flag > env, and the no-source error names all three forms. Prefer stdin/env — inline values land in shell history and are visible via `ps`. `--secret` and `--input` cannot both read stdin in one invocation. `--input` takes the **exact JSON body you plan to send** (`"-"` or `"@"` reads stdin); `--user-token` is the `X-User-Token` header value used only when the body carries no `userToken`. Output: the canonical string, the lowercase-hex signature, and the epoch timestamp (pin with `--timestamp <epoch>` for reproducible vectors; `--json` emits `{appKey, userToken, timestamp, canonical, signature}`). Add the returned `signature` and `timestamp` fields to the body without changing the signed values, and send within ±300 seconds of the timestamp. A body with no payment attributes prints a note that it may be sent unsigned.
---
## 5. Agent Best Practices
1. **Always use `--json`**: When calling CLI commands from automated tools, subagents, or scripts, append `--json` for predictable, parseable output.
2. **Set context once**: Use `cupthread workspaces use <id>` and `cupthread apps use <id>` to avoid repeating `-w` and `-a` on every command.
3. **Use `$CUPTHREAD_TOKEN` in CI**: Inject credentials via environment variable rather than storing them in config files.
4. **Handle `402 Payment Required`**: Writes are rejected by two kinds of quotas. Submission endpoints (`features create`, `inbox`-fed feedback) reject when the workspace hits its plan limits (`tier_limit_submissions` → upgrade the plan in Console → Billing; `subscription_inactive` → renew the subscription). `cupthread workspaces create` rejects with `workspace_limit_reached` when the developer account already owns the maximum number of workspaces (see `maxWorkspaces` on `cupthread me`; only owner-role memberships count) — delete or transfer ownership of one you own, then retry. In `--json` mode, the `api request` escape hatch returns the same guidance as `{error, code, status, hint}`. Treat 402 as a deterministic business rule — do not retry automatically.
5. **Handle `429 Too Many Requests`**: Public write endpoints are rate limited per client IP (changelog subscribe/confirm and GETs: 10 req/min; token-bearing one-click unsubscribe POSTs: 300 req/min on a dedicated budget, PRIV-08; `PUT /user` attribute upsert: 60 req/min) and respond with `{"error": "Too many requests. Please try again shortly."}`. Public showcase, board listing, and public config GETs use the same generic body at 60 requests/minute. The public comment-thread, changelog-feed, columns, and versions GETs share the showcase's per-IP bucket at 60 requests/minute, with the same generic body. The OAuth consent family (`GET`/`POST /oauth/authorize`, `GET /oauth/consent-info`, device lookup/decide) shares a separate 60 req/min per-IP budget and answers `429` with an RFC 6749 §5.2 `invalid_request` body on authorize and a plain `{error}` body on the helpers — transient within the per-minute window; an interactive login spends ~3 requests of it, so only scripted hammering hits it. Unlike 402, a 429 is transient: wait and retry with exponential backoff. Body-less GETs, including `comments list` and `apps public-changelog`, are retried automatically. An anonymous `200` from those four routes can lag a write by up to 30 seconds, so refetching immediately will not show a comment or changelog entry that was just saved. The CLI renders the guidance as `rate limited: Too many requests. Please try again shortly. (HTTP 429) — <hint>` and, in `--json` mode, as `{error, status, hint}`.
6. **Handle `403 Forbidden` (AUTH-01 workspace RBAC)**: Every console workspace route declares a capability checked against the caller's workspace role, and the high-impact ones (`members.manage`, `billing.manage`, `integration.manage`) additionally reject `cpt_` API tokens regardless of role. Two structured codes come back with HTTP 403: `capability_required` — the role lacks the capability; ask a workspace admin/owner to perform the action or have an owner upgrade the role (Console → Members) — and `interactive_session_required` — no CLI credential can do this: the OAuth login also issues a `cpt_` token, so perform the action in the Console web UI. Affected commands: `workspaces members invite/add/set-role/remove`, `workspaces invitations revoke`, `billing checkout/portal/addons`, and integration auth-url/connect/disconnect/sync — reads like `workspaces members list`, `billing show`, and `integrations status` are unaffected. The checks are ordered role-first-then-token-type, so a member-role token on a members route reports `capability_required` while an admin/owner token reports `interactive_session_required`. The CLI renders the guidance as `forbidden: <error> (HTTP 403, code=…) — <hint>` and, in `--json` mode via `api request`, as `{error, code, status, hint}`.
7. **Pass `--yes` to destructive commands after checking the target**: `features delete`, `columns delete`, `versions delete`, `changelog delete`, `workspaces members remove`, and `imports cancel` are hard, server-side, unrestorable deletes. On a non-interactive stdin they refuse with `… re-run with --yes to confirm` BEFORE resolving ids or sending any request (also in `--json` mode); on an interactive terminal they prompt `Continue? [yN]` on stderr. When automating, resolve the id first (`features get`, `changelog list`, …), verify it is the record you mean, and only then pass `--yes` — never loop these commands over an unverified generated id list.
8. **Keep provider connection tokens off the command line**: `integrations github|linear|notion|slack connect --token <value>` puts a GitHub PAT / Linear / Notion / Slack API token into shell history, `ps` output, and CI logs. Pass `--token -` (or `@`) to read the token from stdin (`printf %s "$GITHUB_PAT" | cupthread integrations github connect --token -`, trailing whitespace trimmed) or export the per-provider variable `CUPTHREAD_GITHUB_TOKEN` / `CUPTHREAD_LINEAR_TOKEN` / `CUPTHREAD_NOTION_TOKEN` / `CUPTHREAD_SLACK_TOKEN` and omit the flag entirely. Precedence is flag > env, and the no-source error names all three forms. The inline value still works but leaks the secret.
9. **Read `details` on `400 Validation failed` before retrying**: every zod rejection carries field-level reasons, and the CLI surfaces them — the error line names each offending field (`Validation failed (HTTP 400): title: Title must be at least 3 characters; versionId: Invalid version`), and `api request --json` includes the raw `details` object (`{formErrors, fieldErrors}`) in the error payload. Fix the named inputs and retry once; do not brute-force varying inputs against a 400.