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.
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.
| 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; on a seed, replica says whose copy it is and how far behind. 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 |
GET /api/signers |
This node's public key and every key it trusts, with the channel a key is bound to, versioned: what SYNC.md's authorship check needs, since the log names a key's id and never the key. Behind the same node-wide read grant as /api/log |
GET /api/witness |
On a seed: its signed statements that it folded the home's attestations, the latest and the last 64, versioned. Compare one with the home's snapshot by ancestry in the prev_snapshot chain (SYNC.md, Seeds); neither an ancestor of the other is a fork. 404 on a home |
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 |
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>] [--no-clone]
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, the node URL in ~/.choir/config, and a clone of each repository the invite names in the current directory (--no-clone skips it); the same link run again on this machine clones only what is missing; --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 seed <home-url> [--credential <file>] [--name <principal>] [--port <n> | --archival] [--state <dir>] [--foreground] [-- <daemon flags>]
take this machine from nothing to a running seed of another node's log (D80): mints the seed's node key and prints the three lines the home registers, then, run again with the credential the home issued, starts and supervises the seed; --archival replicates and stores without servingchoir repo follower add <owner/repo.git> <name> <url> [--state <dir>]
on the node host, add a remote the repository's landings are pushed to (D21):git remote addon the bare repository, plus the marker that makesnode servepass --followers; restart the node once and every landing followschoir repo follower list [--state <dir>]
every repository this node holds with the remotes it pushes to; a repository with none is named, since its objects have no second copy yetchoir repo follower push [<owner/repo.git>] [--state <dir>]
push every branch and tag to each remote now, no force and no prune; what the daemon does after each landing, for when it was not running with --followerschoir 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 upgrade --from <node-url> | --source <checkout> [--into <dir>] [--state <dir>] [--dry-run]
newer binaries into the directory thischoirruns from (or --into), a restart of the supervised node, and the build stamp read back from the running daemon; --from runs a node's own installer, --source builds a checkout with cargochoir 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 answers; withseeds =in .choir/config, a fork check per seedchoir 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 take <backup-dir> [--state <dir>]
take a backup of this machine's node into a directory, in the shapebackup verifyreads: the log, checked to extend the copy already there and to verify as a chain, the node fingerprint, the attestation, the policy files as one archive with no secret in it, and one git bundle per repository, kept when its refs have not moved; then verifies what it wrotechoir backup schedule <backup-dir> [--every <seconds>] [--state <dir>]
runbackup takeinto that directory on a timer, launchd or systemd, every hour unless told otherwise;backup unscheduleremoves itchoir backup unschedule
remove the backup timer; the backups already taken are keptchoir 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.
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 choirServes the legacy handshakes and the stateless 2026-07-28 request path. Tool order and schemas come from crates/choir-cli/src/surface.rs.