donegate — the thin client
The human leg of the gate, from your own shell.
donegate is a thin client: one verb per act, one act per API call, and no
method logic on this side of the wire. It does not know what your board's
columns are, which transitions are legal, or what you should do next — it asks
the server and prints the answer. That is deliberate (canon P4), and it is why
this client keeps working when the method changes.
npm install -g @donegate/cli
donegate login --server https://app.donegate.io
donegate next
Install
The client is published to the npm registry as @donegate/cli (CS-76,
ADR-013 addendum). Install it once, globally, and every command is donegate <verb>:
npm install -g @donegate/cli
donegate whoami
The registry verifies integrity for you: npm install checks every downloaded
tarball against the sha512 the registry holds for that exact version, and the
registry is a trust root the authority host does not control — there is no
manual checksum step, and there is no way for the host you talk to to hand you
different bytes than everyone else gets. Each published version is immutable.
The scope is not optional. Always @donegate/cli. The unscoped name on the
registry belongs to an unrelated third party; installing or running it would
run their software, not this client. A test in this repository refuses any
shipped document or message that names the bare package.
No-install form. npx -y @donegate/cli <command> runs the client without a
global install, resolving and caching it by version through the same registry
integrity check. It is what the harness wiring lines below use, because it
survives a machine where the global install was skipped.
Wiring a harness
# Claude Code
claude mcp add donegate -- npx -y @donegate/cli mcp --harness claude-code
# Codex — ~/.codex/config.toml
[mcp_servers.donegate]
command = "npx"
args = ["-y", "@donegate/cli", "mcp", "--harness", "codex"]
With a global install, donegate mcp --harness <label> is the equivalent
command (claude mcp add donegate -- donegate mcp --harness claude-code;
Codex command = "donegate", args = ["mcp", "--harness", "codex"]). The
npx -y form is the one documented because it does not depend on the
harness's PATH containing npm's global bin directory.
--harness <label> is the agent's own seat label; it never changes what the
server decides (canon P5).
PowerShell
Install globally and call donegate directly — it is a .cmd shim on Windows
and works as-is. If you wrap the no-install form in a PowerShell function, call
npx.cmd rather than npx, or the arguments after the package name are
dropped:
function dg { npx.cmd -y @donegate/cli @args }
Getting started
donegate login --server <url> [--project <id>] # paste your token once
donegate whoami # confirm who you are
donegate next # what act is available to you
login asks for your token at a hidden prompt — it is never a command-line
argument, because arguments land in shell history, in ps, and in transcripts.
Paste it once; you never handle it again. logout forgets it.
Commands
Every command accepts --json, --project <id>, --server <url> and --help.
Session
| Command | What it does |
|---|---|
join --server <url> |
Redeem an invitation. Asks for the code and the PIN at hidden prompts, mints your credential, stores it, and prints whoami. |
login --server <url> [--project <id>] |
Sign in; verifies the token before storing it. |
logout [--revoke] |
Forget the stored credential. With --revoke, revoke it on the server first. |
whoami |
The identity, org role and project seats behind your credential. |
join is the second person's first command, and the only one that sends no
credential at all: the emailed code and the PIN you were given out of
band are the credential. You never need a token to hand-carry, and never a
step on the server host. The seat you get is the one the invitation recorded —
join takes no --project, because a joiner does not choose the seat they were
offered.
Inviting (org owner)
| Command | What it does |
|---|---|
invite --email <address> [--project <id> --role <architect|developer>] [--org-role owner] [--expires <hours>] |
Issue an invitation. Prints the PIN once. |
invites |
Every invitation in this org, and where each one stands. |
The invitation is two factors on two channels. The code goes to the address by email; the PIN is printed to you and to nobody else, and you pass it on yourself — by voice, in person, however you like, as long as it is not the inbox the code just landed in. Forwarding the PIN by email turns a two-factor invitation into a one-factor one.
The PIN is never shown again, is never stored in a form anyone can read back,
and is never in the email. Five wrong attempts burn the invitation; so does
invites plus a revoke from the dashboard. Both surfaces are org-owner only.
Credentials: tokens, revoke, rotate
| Command | What it does |
|---|---|
tokens [--scope self|org] |
Your tokens and your agents' — id, prefix, name, holder, created, last used, expires, status. Every token now has an expires date; none shows - any more. --scope org lists the whole org (owner only). The one this shell is using is marked *. |
revoke <token-id> |
Revoke a token. Immediate: its next request is refused, and so is any dashboard session it signed in. |
rotate [<token-id>] [--print] |
Replace a token with a fresh one and revoke the old, in one server transaction. No argument rotates the token you are using and stores the replacement for you — nothing is printed but the identity. By id, --print is required: the new secret is shown once, for you to hand to the seat it belongs to — including when the id is the token you are using, where it is also stored for you. Either way the replacement is renamed <old name> (rotated <UTC timestamp>) — an existing (rotated …) suffix is replaced, not stacked — so two rotations of one token leave two distinct labels, unless both happen in the same second. |
Who may act on which token is decided by the server, in one place: a token's holder, the human an agent acts for, and an org owner — nobody else, and no hierarchy (an architect has no more say over a developer's token than a developer has over theirs). A token id from another org is a plain 404.
rotate never strands you. The server mints the replacement before it
revokes the old token, and this shell stores the replacement before it
prints anything. If the store itself fails after the server has rotated, the
new secret is printed once with a warning so you can donegate login with it
— a credential you never saw and never stored is the exact outcome this verb
exists to prevent.
rotate --harness <label> is reserved for named agent profiles and is refused
in this version. Rotate an agent's token by id with --print and hand the
secret to the harness.
Every token expires — and rotate is how you renew it
Tokens have a finite lifetime (the figure and its reasoning are in ADR-015).
rotate renews: the replacement gets a full lifetime from the moment you
rotate, so rotating before your token runs out is the whole renewal story, and
it needs nobody's help.
You will be told before it bites. When your credential is close to its date, every verb prints one line to stderr:
Your Donegate token expires in 9 days — run `donegate rotate` to renew it.
That line is on stderr, never stdout — --json output stays byte-for-byte the
server's response, so a script that pipes it is unaffected. whoami shows the
same date as an expires field, and the profile quietly records it beside your
credential (still nothing you ever open or edit). An ordinary command run with a
token supplied through DONEGATE_TOKEN never records that token's expiry there,
because it is not this profile's credential — only login and rotate, which
store a credential, write it (see Credentials).
If you let it lapse, the refusal says so and points at the remedy that actually works:
Your Donegate token expired on 2026-12-01. Ask an owner for a new invitation,
then run `donegate join --server <url>` — or, next time, `donegate rotate`
before it expires.
donegate login is not that remedy: it needs a token to paste, and an expired
holder has none. That is why rotating early is worth the ten seconds.
Working a card
| Command | What it does |
|---|---|
next |
The one act available to you on this project. |
claim <board-item-id> |
Take responsibility for a card. |
release <board-item-id> [--reason <text>] |
Hand a card back. |
get-spec <session-id> [--version <n>] |
Read the approved spec (architects may name a version). |
log <session-id> --command <cmd> --exit-code <n> |
Record evidence: a command and how it exited. |
log <session-id> --note <text> |
Record a note. |
worklog <session-id> |
The session's event stream, in order. |
move <board-item-id> --to <status> [--reason <text>] |
Move a card. The server adjudicates the edge. |
ask <session-id> --question <text> (--blocking | --non-blocking) |
Raise a question. A blocking one parks the card. |
answer <session-id> --seq <n> --answer <text> |
Answer an open question. |
The human leg
| Command | What it does |
|---|---|
review <session-id> --verdict <verdict> [--note <text>] |
Record a review verdict. |
signoff <session-id> |
Print where to sign off. Makes no request. |
These are two acts, by possibly two people, and this client keeps them
separate on purpose. There is no approve that does both: the point of the gate
is that each signature is attributable, and a command that bundles them hides
the second one behind the first.
signoff no longer signs off (CS-85). A bearer token proves custody, not
presence: a process running as you reads the same credentials file you do, so a
CLI that could record a sign-off could not tell you from it (security audit
SA-01). Sign-off now needs a fresh passkey assertion, which a shell cannot
produce — so the act happens on the session page in the dashboard, and this verb
prints the address:
$ donegate signoff ses_01J...
sign off in the dashboard (a passkey is required): https://your-donegate/dashboard/p/prj_.../sessions/ses_01J...
Under --json it emits { "sign_off_url": … }, the same key next carries on
the sign-off step. --note is gone with the wire call: the note is typed on the
page, in front of the human attesting to it.
Ingestion (architect)
| Command | What it does |
|---|---|
session-create --title <t> --type <t> --risk <r> [--link <url>] |
Create a card and its session. |
spec-add <session-id> (--file <path> | --content <text>) [--test-command <cmd>] |
Append the next spec version. |
spec-approve <spec-id> |
Approve a spec version. |
spec-list <session-id> |
List a session's spec versions (metadata only). |
--json
--json prints the server's response body, unaltered — same keys, same
values, same nesting, pretty-printed. Nothing is renamed, dropped, added or
summarised, so it is safe to pipe into jq and safe to depend on from a script.
Human-readable output is the default and is a rendering of that same body;
neither is derived from the other.
Credentials
donegate login stores your token for you, and donegate logout removes it.
That is the whole model: you never manage a credential file. If a command
tells you your credential is not working, the remedy is always another command —
run donegate login again. donegate logout --revoke also revokes the token on
the server before forgetting it, for a shell you are handing on or retiring;
donegate rotate swaps it for a fresh one without a re-login.
Your stored token is sent to exactly one place: the server you logged in to. It
is not attached to any other origin, and a redirect that leaves that origin is
refused rather than followed. Pointing a command at a different server — with
--server or DONEGATE_SERVER — does not carry your stored credential
there; the command refuses and tells you how to sign in to that server instead.
(A token you supply yourself in DONEGATE_TOKEN goes wherever you point it:
that pairing is yours to make.)
At rest the credential is written owner-only (0600 on macOS and Linux). On
Windows the file is protected by your user profile's ACL — the same posture as
gh and npm. An OS keychain is a future hardening option, deliberately not a
dependency today: native modules are a reliability tax on a package people reach
through npx.
Environment overrides
| Variable | Effect |
|---|---|
DONEGATE_TOKEN |
Use this token instead of the stored one. |
DONEGATE_SERVER |
Use this server instead of the stored one. |
DONEGATE_INVITE_CODE |
Feed join's code prompt — for scripted enrolment and the test suite. |
DONEGATE_INVITE_PIN |
Feed join's PIN prompt — same caveat. |
The two invitation variables exist so enrolment can be scripted and so the
integration suite can drive join with no terminal. Prefer the prompts when a
human is present: a PIN in an environment variable is a PIN in a process
listing.
Precedence is environment > flags > stored profile, so CI and agent callers can act as themselves without disturbing a human's stored profile.
Pointing at a different origin means bringing your own token: set
DONEGATE_TOKEN alongside the server override. A stored credential and an
overridden server are never combined.
The expiry a command learns in passing is never cached for a token supplied
through DONEGATE_TOKEN — it is not written to the stored profile, and no
profile is created to hold it where there was none. That token is not the
profile's credential, and an ordinary command run by CI must not leave a trace in
a human's credential file. (Verbs whose whole job is to store a credential still
do: donegate login with DONEGATE_TOKEN is the non-interactive sign-in and
writes the profile, and donegate rotate of the token in use stores its
replacement.)
Exit codes
| Code | Meaning |
|---|---|
0 |
The act succeeded. |
1 |
The act was refused (the server said no), or the server could not be reached. |
2 |
The invocation was wrong — unknown command, missing argument, bad flag. |
Refusals carry their reasons. When the gate refuses a move to done, the unmet
requirements are printed in full — under --json, the refusal envelope comes
through untouched, so a script can branch on details.gate.unmet rather than
parse English.
Requirements
Node 22 or newer (which brings npm and npx). No runtime dependencies.