The choir CLI and the node’s HTTP API
The complete surface, rendered from one table shared with the CLI’s
--help, AGENTS.md, /llms.txt and /api/schema. A staleness test fails
the gate when they differ.
Authentication and exit codes
Auth on the CLI is flags, not env:
choir --auth-file ~/.choir/auth --auth-user choir <command> ...
Exit codes: 0 accepted, 1 rejected (JSON body printed, see ERRORS.md), 2 usage.
The signed-operation API is the primary agent path: it carries actor identity and batches many operations behind one durability barrier. git push remains the compatibility and bulk-transfer path.
Signed-operation CLI and API (primary agent path)
HTTP endpoints
| Endpoint | Purpose |
|---|---|
POST /api/submit | Submit one signed operation (hex payload, hex signature) |
POST /api/submit-batch | Same, in array order; the primary path for agent workloads |
GET /api/view?limit=N&offset=M | The materialized view plus the latest ref-state attestation, key bindings, T2 review outcomes, T3 concentration, T4 newcomer harm, view growth, the build commit, and the sequencer’s p99 against the 100 ms gate. Under an ACL you get your own slice; node-wide sections need a node-wide grant, and a missing repository is one you were not granted. Map-shaped sections are bounded: limit rows (200 default, 1000 max), offset, <section>_omitted, and paging.next |
POST /api/appeal | Record an appeal for a rejected newcomer attempt; requests operator adjudication and never changes privilege |
GET /api/log?from=N | Ordered log entries, the catch-up and sync primitive. Absolute from; evicted entries are served from the persisted log (source says which), and a node that cannot reach back answers 409. Each entry carries hash, parent and author signature; SYNC.md is the verification procedure |
POST /api/workspace | Provision a CoW workspace; optional exact base/change binding makes retries idempotent |
POST /api/workspace/archive | Recoverably archive a change-bound workspace and remove it from the active view |
GET /api/reviews?reviewer=X | One actor’s pending review queue |
GET /api/schema | This surface, machine-readable and versioned, plus what this node will accept; the description an agent generates a client from (D17) |
GET /api/search?q=X&in=code&repo=owner/name&rev=R&limit=N | Search repository contents, file names or commit messages across every repository you may read, each at HEAD; rev needs a single repo. Ungranted repositories are absent, or answered as nonexistent by name. Unindexed (git grep): limit bounds the results, matches counts everything, truncated says which |
GET /api/profile?channel=X | One actor’s standing out of the view you may see: bound keys and their age, changes owned, reviews assigned and verdicts given, approvals slashed, checks reported, and vouches with direction. Two callers with different grants get different numbers. No score; time-locked grants (D66) live outside the log and are not counted |
GET /llms.txt | This surface, as text, for an agent that has never seen choir |
GET /sync.md | The sync contract: cursor semantics and how to verify a page’s hash chain and author signatures |
GET /api/repos | Which repositories this credential can see, read from the filesystem; narrowed says whether an ACL was applied |
POST /api/repo | Create a repository on a running node with the pre-receive hook that sequences its pushes; needs a node-wide write grant; appends nothing to the log |
GET /api/ref-agreement | Where the op log and the bare repos disagree about a ref, read-only |
POST /api/accounts/invite | Mint a single-use, expiring invite and the grants it will hold; needs a node-wide write grant and can never issue one; a grant may carry until=<unix seconds> (D66) |
POST /api/accounts/redeem | Redeem an invite, presented as the credential, for a token, once, and register an ssh key |
POST /api/accounts/request/grant | Answer an access request (D72): turns it into an invite under the id and secret the asker already holds |
POST /api/accounts/request/decline | Drop a pending access request; their link then reads as never valid |
POST /api/accounts/revoke | Delete an account: its token stops authenticating on the next request, and its grants and keys go with it |
GET /api/accounts | Who holds an account, what they were granted, and which invites are outstanding; never a secret or its hash |
POST /api/git-update | Internal: the pre-receive hook callback |
POST /api/git-abort | Internal: retracts a refused push’s already-accepted refs |
The choir CLI
getting started
choir host [--domain <name> | --public [--ip <addr>] | --public-name <name>] [--port <n>] [--repo <owner/name.git>] [--invite <name>] [--state <dir>] [--yes] [--dry-run] [--foreground] [-- <daemon flags>]
take this machine from nothing to a running node and print its URL; bare binds loopback, –domain issues a Let’s Encrypt certificate for a name you own, –public uses a magic-DNS name over this box’s address, –foreground execs the daemon instead of installing a unitchoir init [<state-dir>] [--port <n>] [--force]
set up a node’s layout on this machine: repository root, credential at 0600, an actor key the node trusts, and .choir/config; refuses to overwrite what existschoir key <key-file> [name]
mint a key and print the line the operator registers; pass your channel name to print the bound formchoir git-credential <auth-file> [--auth-user <name>] get|store|erase
git credential helper: hands git your token on stdin so it never lives in a remote URL; configure withgit config credential.helper '!choir git-credential <auth-file>'choir join <link> | <api> <invite-file> <key-file> [--user <name>] [--channel <name>] [--key-file <path>] [--ssh-key <path>] [--token-file <path>]
redeem an invite link and set this machine up: actor key at ~/.choir/agent.key, token at ~/.choir/auth (0600), a git credential helper for that node, and the node URL in ~/.choir/config; –user names the account when the invite left it open, asked on the terminal otherwise; the three-argument form takes the invite from a file, answers JSON and touches neither git nor your home directorychoir docs [--open]
build the book fromdocs/with the API documentation inside it atbook/api/; needs a checkout andmdbook, and names the install command if it is missingchoir skill install [--into <dir>]
install the choir agent skill (default .claude/skills), rendered from this binary’s own surface table; re-run after upgrading
changing code
choir workspace <api> <owner/repo> <name> [--base <git-oid> --owner <channel> --key-file <path> --change <id> --idempotency-key <key>] [--path <prefix>]...
provision a CoW workspace; advanced flags owner-sign an exact base and stable change, and each –path owner-signs a subtreechoir checkpoint <api> <key-file> <channel> <change-id> <workspace-id> <git-oid>
publish an immutable change revision after committing and pushing its git objectchoir propose [reviewer]... [--key-file <path>] [--channel <name>] [--api <url>] [--repo <owner/repo>] [--remote <name>] [--onto <branch>] [--change <id>] [--path <prefix>]...
create a change, push its commits and request review, with no arguments; run from a git checkout, with the key and channel from ~/.choir, every value overridable by flag; re-running after an amend updates the same proposal; a leading<key-file> <channel>pair is still acceptedchoir workspace-archive <api> <key-file> <channel> <owner/repo> <name> <change-id> <idempotency-key>
owner-sign and recoverably archive a bound workspace; exact retries are idempotentchoir submit <api> <key-file> <channel> '<op-json>'
sign and submit one raw operationchoir batch <api> <key-file> <channel> <ops-file>
sign and submit many operations as one batch, the primary path for agent workloads; one op per line,-reads stdin, one result line per opchoir intent <api> <key-file> <channel> <subject> <kind> '<body>'
publish a task spec or plan so other agents can see intentchoir state <api> <channel>
list what you owe and what you are waiting on; every row carries the command that answers it and its risk
review
choir review <api> <key-file> <channel> <id> <git-oid> [--ref <repo:ref>] [reviewer]...
request review on a commit; name no reviewers and the node draws themchoir verdict <api> <key-file> <reviewer> <id> approve|request-changes [note]
answer a review you were assignedchoir comment <api> <key-file> <channel> <review-id> <comment-id> '<body>'
say something on a review; append-only, and the comment id is your retry identitychoir viewed <api> <key-file> <viewer> <review-id>
record that you read a review; first read only, resubmitting is refusedchoir slash <api> <node-key-file> <id> <reviewer> '<reason>'
invalidate one reviewer’s approval; operator-only and never moves a refchoir abandon <api> <node-key-file> <id>
archive a stale incomplete review as lapsed, settling it unapproved; operator-only and never moves a refchoir reviews <api> <reviewer>
your pending review queue
checks
choir check <api> <key-file> <channel> <git-oid> <name> passed|failed|running|errored [evidence] [--ref <repo:ref>]
report one automated check’s outcome on a commit; any runner or person can report by signing, and the node never runs the checkchoir checks <api> <git-oid>
every check reported on a commit, and one verdict; exits 0 passed, 1 failed or unreported, 3 still running, 4 could not be run
trust
choir witness <api> <key-file> <channel>
cosign the node’s current ref-state attestation (D67); the snapshot id is read from the view, and the node may not witness its ownchoir vouch <api> <key-file> <channel> <subject> [note]
vouch for another operator; both ends need a key bound in the log, and it authorizes nothing on its ownchoir unvouch <api> <key-file> <channel> <subject> '<reason>'
withdraw a vouch; both ops stay in the log, and vouching again starts a fresh clock
reading the node
choir schema <api>
print this node’s machine-readable API description and its live capabilitieschoir log <api> [--from <n>] [--verify] [--keys <file>]
read log entries from a cursor; –verify checks continuity, recomputes every hash and verifies the signatures whose keys you holdchoir appeal <api> <attempt-id>
appeal a rejected newcomer attempt for operator adjudication; never grants privilegechoir profile <api> <channel>
what the log records about one actor: keys and their age, changes owned, verdicts given, checks reportedchoir search <api> <term> [--in files|code|commits] [--repo owner/name] [--rev R] [--limit N]
find a literal term across every repository you may readchoir triage <api>
every review and change in a bucket (landed, awaiting verdicts, changes requested, approved awaiting landing), most actionable first, capped, with truncation marked in-bandchoir funnel <api>
the contribution funnel from admission to first verdict and the steepest drop between stages; counts what this credential may read, and reports an unmeasured stage as nullchoir view <api> [--limit <n>] [--offset <n>]
read the materialized view, its ref-state attestation and the node’s health counters; map-shaped sections page 200 rows at a time, with<section>_omittedandpaging.next
operating a node
choir invite <api> <name> <owner/repo> [read|write]
mint an invite and print the one link to send; the same thing the /people page doeschoir asks <api>
who has asked for access and is waiting on an answer (D72)choir grant <api> <request-id> <owner/repo> [read|write]
let one of them in; the link they already hold becomes their invitechoir decline <api> <request-id>
drop a pending request; their link then reads as never validchoir runner <config-file>
drive one workspace lifecycle step for an orchestrator; JSON request on stdin, JSON result on stdoutchoir bind <api> <node-key-file> <operator> <key-hex> [channel]
record in the log that a key belongs to an operator; operator-only and never moves a refchoir revoke <api> <node-key-file> <key-hex> '<reason>'
withdraw a key binding; terminal, and the attribution row surviveschoir acl render <api> <acl-file>
rewrite an ACL file’s trailing comments to name the person behind each handle; grants are copied through unchangedchoir repo create <api> <owner/repo.git>
create a repository on a running node, sequenced from its first push; needs a node-wide write grant, answers 409 when it already exists, and prints the clone URLchoir repo list <api>
the repositories on a node this credential can read, one per line; an ACL narrows the list rather than refusing itchoir repo url <api> <owner/repo.git>
the clone URL for a repository, and the one line of git configuration that makes pushing work; the credential is never put in the URLchoir node serve [--state <dir>] [--port <n>] [--create <owner/repo.git>] [-- <daemon flags>]
run the node in this terminal, deriving root, credential and trusted keys from whatchoir initwrote; execs the daemon so signals and the exit code reach the real processchoir node install [--state <dir>] [--port <n>] [-- <daemon flags>]
hand the node to launchd (macOS) or a systemd user unit (Linux) so it survives logout, crash and reboot; the unit runschoir node servechoir node tls <domain> --user <account> [--port <n>] [--dry-run | --staging]
obtain a Let’s Encrypt certificate for this node and wire up renewal: certbot, a deploy hook that re-projects the pair and restarts the node, and the markernode servereads; the only command here that expects root, and--useris requiredchoir node stop
stop the supervised node for this boot, leaving the unit in place;node uninstallis the one that ends itchoir node restart
reload the unit and start it again, which is how a rebuilt binary reaches the running nodechoir node uninstall
stop the node and remove its unit; the state directory, with the keys, repositories and op log, is keptchoir node logs [<lines>] [--state <dir>]
the tail of the node’s log; defaults to the last 30 lineschoir node status [<api>]
health, the commit serving, the sequencer’s position and its p99 against the 100 ms gate, and how much this credential can seechoir doctor [<api>] [--state <dir>]
check everything the other commands assume: the binaries shelled out to, the auth file and its mode, and whether a node answers; each failure prints the fix; on a hosting machine it adds bind address, TLS, certificate expiry, linger, unit state and whether the public URL answerschoir backup verify <backup-dir>
whether a backup can be restored from: the four files, the manifest checksum, the hash chain, the policy archive and every git bundle, refusing a backup that carries a key or credential; every check localchoir backup restore <backup-dir> <target-root>
turn a backup back into a node and prove it by accepting a real push: reads and refuses before writing, unbundles git objects before the first boot, rehearses on a port it picks; exit 3 means a secret only you can supply is missingchoir repair <log-file> --verify | --truncate-tail
inspect a stopped node’s op log, or repair a tail that was still being written;--verifychanges nothing,--truncate-tailquarantines the partial record before cutting, and damage anywhere but the tail is refused
Most commands take the node’s URL first. Put node = <url> in .choir/config, in the working directory or any parent, and it is filled in when omitted. choir <command> --help prints one command’s spec.
Exit codes: 0 accepted, 1 the node rejected (its JSON error body is printed), 2 usage error.
Live surface on a running node: GET /llms.txt. Sync verification: SYNC.md / GET /sync.md.
MCP adapter
A synchronous stdio adapter mapping generated tools onto the same HTTP endpoints; no second implementation, no session state.
choir-mcp http://127.0.0.1:8417 --auth-file ~/.choir/auth --auth-user choir
Serves the legacy handshakes and the stateless 2026-07-28 request path. Tool order and schemas come from crates/choir-cli/src/surface.rs.