WarmHub: An epistemic repository platform for versioned, structured knowledge
# MCP Server
> Endpoints, protocol details, and configuration for the WarmHub MCP server.
WarmHub implements the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) over HTTP, exposing core repo read/write operations and discovery tools as typed tools that AI agents can discover and call.
## Protocol
- **MCP version**: 2026-07-28, with older clients still served. See [Version negotiation](#version-negotiation) for what puts a request on the modern path and how rejections are reported.
- **Transport**: JSON-RPC 2.0 over HTTP POST
- **Origin check**: a request carrying an `Origin` header outside the deployment's allowed origins (loopback origins are always allowed) is refused with HTTP 403 and a JSON-RPC error body, on every method; a request with no `Origin` header — the normal case for non-browser clients — is unaffected.
- **Supported methods** depend on which path the request is on. Legacy: `initialize`, `tools/list`, `tools/call`, `ping`. Modern (`2026-07-28`): `server/discover`, `tools/list`, `tools/call` — `ping` is a legacy-only mechanism there, and `server/discover` is not served on the legacy path. `initialize` always selects the legacy path, even when decorated with modern signals. Anything outside a path's set answers `-32601`.
- **Every modern-path result** carries `resultType: "complete"`. Legacy results carry no `resultType`.
- **`tools/list` is cacheable**: results carry `ttlMs` (1 hour by default) and a `cacheScope`.
`/mcp` returns `cacheScope: "public"`. That URL identifies no org or repo, and the catalog is byte-identical for every caller, so shared caches may reuse it for the full TTL window.
Tool *results* carry no cache hints and must not be cached.
## Version negotiation
`initialize` always answers with protocol version `2024-11-05`, whatever version signals the request carries. Everything below applies only to the other methods.
The `initialize` result also carries `instructions` — server-level guidance covering anonymous access and the CLI naming convention, built from the same constants `warmhub_capabilities` returns. It is a standard optional `InitializeResult` field. Client support varies: Claude Code honors it, while Claude.ai connectors currently ignore it, so treat it as a bonus and never as the only place a fact is stated.
A non-`initialize` request goes on the `2026-07-28` path when it carries either signal, and the two are **not** symmetric:
| Signal | Puts the request on the modern path when |
|---|---|
| `MCP-Protocol-Version` header | its value is anything other than a known older revision (`2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`). Those stay legacy-tolerated because conformant older clients echo their own negotiated version on every request; any other value is a version declaration and gets the modern verdict, including an unrecognized one. |
| `_meta["io.modelcontextprotocol/protocolVersion"]` | the key is **present at all**, whatever its value or type — with one exception: the exact string `2024-11-05` is the revision the legacy path serves, so a request declaring it is served as legacy rather than judged against the modern contract. (A `2026-07-28` header alongside it still wins classification, and the two then disagree — see step 2 below.) Other older revisions in `_meta` are not served and get `-32022`. |
A request carrying neither signal is served exactly as before.
Once a request is on the modern path, it is validated in this fixed order. The order is the contract: each step is free to reject before a later step could judge the request against rules a future revision may have changed.
1. **Version is a string.** If `_meta["io.modelcontextprotocol/protocolVersion"]` is present it must be a string.
2. **Version agreement.** If the header and `_meta` both carry a version string, they must be equal. A request that contradicts itself gets no verdict about either value.
3. **Version verdict.** The declared version — the `_meta` value when present, otherwise the header — must be `2026-07-28`.
4. **Required headers.** See [Required headers](#required-headers). Skipped for notifications (requests with no `id`).
5. **`_meta` envelope.** `_meta` exists, is an object, and carries a string `_meta["io.modelcontextprotocol/protocolVersion"]` and an object `_meta["io.modelcontextprotocol/clientCapabilities"]`.
Because the verdict runs ahead of steps 4 and 5, a request that names an unrecognized version fails with `-32022` whatever else is wrong with it — a missing `clientCapabilities` or a contradicting `Mcp-Method` never changes that answer.
### Required headers
Every modern-era request that expects a response must carry:
| Header | Rule |
|---|---|
| `MCP-Protocol-Version` | present |
| `Mcp-Method` | present and exactly equal to the request's `method` |
| `Mcp-Name` | present on `tools/call`, `resources/read`, and `prompts/get`, and exactly equal to `params.name` (`params.uri` for `resources/read`). On any other method there is no target to name, so the header is ignored rather than rejected. |
Header names are case-insensitive; values are compared case-sensitively. A value outside the RFC 9110 field-value range is rejected. A `Mcp-Name` may be sent in the `=?base64??=` sentinel form — the markers are lowercase and case-sensitive — and is decoded before the comparison.
These rules bind only clients that declare `2026-07-28`. A legacy-path request needs no protocol headers at all.
WarmHub does not serve `resources/read` or `prompts/get` — both answer `-32601` with HTTP 404. They still appear in the table because header validation runs **before** dispatch: a request for either that omits or mismatches `Mcp-Name` gets `-32020` with HTTP 400 first, and never reaches the 404.
### Status and code by outcome
| Outcome | JSON-RPC code | HTTP |
|---|---|---|
| Header and `_meta` versions both present as strings and disagree | `-32020` | **400** |
| Version value is a string but not `2026-07-28` (error data carries `supported` and `requested`) | `-32022` | **400** |
| A required header is missing, mismatched, or malformed | `-32020` | **400** |
| Malformed `_meta` (missing, not an object, missing or non-string version, missing or non-object `clientCapabilities`) | `-32602` | **400** |
| Method not served on the modern path, `ping` and unknown methods included | `-32601` | **404** |
| Unknown tool name on `tools/call` | `-32602` | 200 |
| Tool execution error | `-32000` (in an `isError` result) | 200 |
| Notification (no `id`) accepted | — (no body) | **202** |
Legacy-path responses are unaffected: every legacy JSON-RPC error still rides out on HTTP 200, and a legacy notification also answers 202. A non-string `_meta` version value classifies the request as modern and fails at step 1 — it does not fall back to the legacy path.
Modern-era responses carry server identity, but its location depends on whether the response succeeded: success responses put it at `result._meta["io.modelcontextprotocol/serverInfo"]`, and error responses at `error.data._meta["io.modelcontextprotocol/serverInfo"]`. JSON-RPC forbids extra envelope members, so `error.data` is the only legal slot on a rejection. Legacy-era responses carry no server identity at all.
The following minimal example shows a `tools/list` request on the `2026-07-28` path. Both `_meta` members — `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` — are required. Sending the `MCP-Protocol-Version: 2026-07-28` header without these `_meta` fields fails the envelope step and returns `-32602` with HTTP 400:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
```
That request must also carry the headers `MCP-Protocol-Version: 2026-07-28` and `Mcp-Method: tools/list` — see [Required headers](#required-headers). The `_meta` members are required whether or not the header is present, and vice versa. A successful response will include `result._meta["io.modelcontextprotocol/serverInfo"]` with server identity.
### server/discover
`server/discover` is the modern path's handshake-free orientation call. It takes no arguments beyond the `_meta` members every modern request carries, needs no Bearer token, and answers with:
| Field | Meaning |
|---|---|
| `supportedVersions` | Every protocol revision this server serves, newest first: `["2026-07-28", "2024-11-05"]`. This is the union across both paths — `initialize` still counter-offers only `2024-11-05`. |
| `capabilities` | The same capability object `initialize` returns (`{ "tools": { "listChanged": false } }`). |
| `instructions` | The same server-level guidance the `initialize` result carries. |
| `ttlMs` / `cacheScope` | Cache hints matching `tools/list`: 1 hour by default, `"public"` — the result names no org, repo, or caller. |
It is served on the modern path only. A legacy-path `server/discover` answers
`-32601`, and so does a modern-path `ping`. `initialize` always selects the
legacy path, even when decorated with modern signals.
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/mcp` | Global MCP endpoint |
| `GET` | `/mcp` | Returns 405 Method Not Allowed with RFC 9728 discovery challenge. A disallowed `Origin` is refused with 403 first — see [Authentication](#authentication) |
| `GET` | `/.well-known/oauth-protected-resource` | OAuth 2.0 Protected Resource Metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) |
| `GET` | `/.well-known/oauth-protected-resource/mcp` | Same protected-resource metadata, at the MCP-suffixed discovery path some clients probe |
| `GET` | `/mcp/.well-known/oauth-protected-resource` | Same protected-resource metadata, at the MCP-prefixed discovery path |
| `GET` | `/.well-known/oauth-authorization-server` | OAuth 2.0 Authorization Server Metadata ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) |
## One endpoint, one catalog
`POST /mcp` serves the full MCP tool catalog. Tools that act on a repository take
`orgName` and `repoName` as arguments; org-level tools such as
`warmhub_org_list`, `warmhub_org_get`, `warmhub_repo_list`, and
`warmhub_repo_create` take whatever locator they need the same way.
Organization membership can be read via `warmhub_org_member_list` (takes `orgName`
and an optional `pending` filter). Changing membership — adding, removing, or
assigning roles to members — is not on the MCP surface, nor are other org and repo admin
operations such as renaming, editing org or repo descriptions, archiving, unarchiving,
changing repo visibility, or deleting a repo; use the CLI or SDK for those operations. Meta
tools — `warmhub_doctor` and `warmhub_capabilities` — require no locator at
all.
Call `warmhub_capabilities` for the catalog, then `warmhub_repo_describe` with a
repo locator for that repo's schema and write contract.
## Authentication
`POST /mcp` takes an optional Bearer token. Every tool carries a reviewed auth
tier, and the token decides which tiers you can call:
- **Anonymous tier** — served without credentials, over public orgs and repos.
- **Builder tier** — requires a valid Bearer token.
`initialize`, `server/discover`, `tools/list`, and the OAuth discovery
endpoints answer pre-auth on the path that serves them — `initialize` on the
legacy path, `server/discover` on the modern one, `tools/list` on both. A token that cannot be verified is treated as anonymous rather than
rejected; a revoked or disabled account still returns its own error.
Calling a builder-tier tool without a token returns HTTP `401` with an RFC 9728
challenge, before the tool runs:
```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.warmhub.ai/.well-known/oauth-protected-resource"
Content-Type: application/json
{ "error": { "code": "UNAUTHENTICATED", "message": "Authentication required for \"warmhub_org_list\"" } }
```
GET requests to `/mcp` return HTTP `405 Method Not Allowed` and include the same RFC 9728 `WWW-Authenticate` challenge header:
```
HTTP/1.1 405 Method Not Allowed
Allow: POST
WWW-Authenticate: Bearer resource_metadata="https://mcp.warmhub.ai/.well-known/oauth-protected-resource"
```
This means a GET probe against `/mcp` can bootstrap OAuth discovery in the same way as an unauthenticated POST — standards-compliant clients can follow the challenge from either response to discover the protected-resource metadata endpoint and acquire a token.
The [Origin check](#protocol) runs ahead of the method check, so this is the response for a request with no `Origin` header — the normal case for a non-browser client — or one whose `Origin` is allowlisted or loopback. A GET carrying a disallowed `Origin` is refused with HTTP `403` and a JSON-RPC `-32600` error instead, and that response carries neither `Allow` nor `WWW-Authenticate`:
```
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Origin not allowed"}}
```
The `resource` field in the protected-resource metadata document and the `resource_metadata` URL in every `WWW-Authenticate` challenge are host-specific: they reflect the MCP hostname the client actually reached, when that host is owned by the deployment. A client reaching `mcp.example.com` sees `resource_metadata` pointing at `https://mcp.example.com/.well-known/oauth-protected-resource`; a client reaching `api.example.com` sees the equivalent URL for that host. MCP/OAuth clients that follow the challenge to the protected-resource document will therefore always receive metadata scoped to the host they contacted, not a single canonical API origin.
Standards-compliant MCP/OAuth clients use the challenge to discover the protected-resource metadata endpoint and bootstrap auth. The tool catalog is the same for every caller: builder-tier tools are listed to anonymous callers too, so a client can discover them and upgrade when one is first called. Changing membership — adding, removing, or assigning roles to members — is not on the MCP surface; use the CLI or SDK for those operations.
## Configuration
### MCP Client Setup
Add a WarmHub entry to the `mcpServers` section of your MCP client's configuration. The examples below use the standard format supported by Claude Desktop, Cursor, and other MCP-compatible clients. The `type` field names the transport; some clients infer it from the URL scheme, but stating it is always safe. On `claude mcp add` the equivalent flag is spelled `--transport`, and the entry it writes uses `type`.
The URL alone gets you the anonymous tier; builder-tier tools need a Bearer token. Standards-compliant MCP/OAuth clients follow the RFC 9728 challenge returned on the first builder-tier call to discover the OAuth flow and acquire a token automatically. Clients without that capability must inject the token themselves; mint a personal access token (see [HTTP Authentication](/http-api/authentication/)) and configure your client to send `Authorization: Bearer ` on every MCP POST. Many clients accept a `headers` map in the `mcpServers` entry for this purpose, e.g.:
```json
{
"mcpServers": {
"warmhub": {
"type": "http",
"url": "https://mcp.warmhub.ai/mcp",
"headers": {
"Authorization": "Bearer ${WH_TOKEN}"
}
}
}
}
```
The MCP endpoint authenticates the bearer token in the `Authorization` header; the example above sources that value from `WH_TOKEN`. See [Environment Variables](/reference/env-vars/) for the variables WarmHub reads and their precedence.
Consult your MCP client's documentation for the exact field name. See [Authentication](#authentication) above for the challenge format and discovery contract.
The example above uses the production MCP URL. For self-hosted deployments, replace `mcp.warmhub.ai` with your deployment URL.
### Updating an older configuration
Point new configurations at `mcp.warmhub.ai`. An existing one on `api.warmhub.ai/mcp` needs no change — the API hostname serves `/mcp` as a compatibility path, and both hostnames reach the same server — so switch it the next time you edit the file.
If your agent calls tools by remembered name, check them against the tables below. There are no aliases, so an unrecognized name answers `Unknown MCP tool` immediately. Calling `warmhub_capabilities` at the start of a session avoids the problem entirely: it returns the current catalog.
| Use this | Instead of |
|----------|------------|
| `warmhub_subscription_log` | `warmhub_action_livefeed` |
| `warmhub_subscription_attempts` | `warmhub_action_attempts` |
| `warmhub_subscription_runs` | `warmhub_action_runs` |
| `warmhub_notifications` | `warmhub_action_notifications` |
| `warmhub_subscription_*` with `scope: "org"` | `warmhub_org_subscription_*` (7 tools) |
| `warmhub_thing_get`, which takes `wrefs` at every cardinality | `warmhub_thing_get_many` |
| `warmhub_commit_submit` | `warmhub_commit_apply` |
| `warmhub_repo_content_get` / `warmhub_repo_content_set`, with `kind` | `warmhub_repo_readme_get` / `_set`, `warmhub_repo_agents_get` / `_set`, `warmhub_repo_llms_txt_get` |
Some tools have no replacement on the MCP surface. Reach for these instead:
| Instead of | Do this |
|------------|---------|
| `warmhub_wref_resolve` | Call `warmhub_thing_get` with `includeRetracted: true` — a superset of the same identity fields |
| `warmhub_repo_content_generate`, `warmhub_repo_readme_generate`, `warmhub_repo_agents_generate` | Draft with `wh repo content prompt`, then persist with `warmhub_repo_content_set` |
| `warmhub_org_set_display_name`, `warmhub_repo_set_display_name` | `wh org rename` or `wh repo rename`, or the web app |
| `warmhub_org_set_description`, `warmhub_repo_set_description` | `wh org update` or `wh repo update`, or the web app |
`warmhub_use` and `warmhub_channel` have no equivalent. Both were local client conveniences rather than repo operations, so there is nothing to call in their place.
## Tool Responses
Tool responses include both human-readable and structured content:
```json
{
"resultType": "complete",
"content": [{ "type": "text", "text": "..." }],
"structuredContent": { ... },
"isError": false
}
```
- `resultType` — always `"complete"`. Present on every modern-path result, not just tool calls; legacy-path results omit it.
- `content` — tool result as text for the model-readable channel. Typically JSON.
- `structuredContent` — typed object for programmatic access. Includes an `auth` field on every response (see below).
- `isError` — `true` if the tool call failed
Every tool call still returns `structuredContent` for programmatic access. WarmHub keeps reviewed output schemas internally for contract tests and schema-clarity audits covering successful structured results (including `auth`) and structured tool-error payloads (including `error` and `auth`), but does not repeat those schemas in `tools/list`. Each listed tool includes its input schema and an `annotations` block with explicit safety hints:
- `readOnlyHint` — `true` only for tools that do not change state.
- `openWorldHint` — `true` when a write can affect public WarmHub data, another user, or a third-party webhook; always `false` for read-only tools.
- `destructiveHint` — `true` when a write can delete, overwrite, retract, revoke access, or cause a comparable irreversible effect.
- `category` — one of `org`, `repo`, `shape`, `thing-read`, `collection`, `commit`, `subscription`, `action`, `meta`.
- Clients can use `category` to group or filter tools in discovery UIs.
- `warmhub_capabilities` uses the same enum, but groups tools under each category rather than repeating the value per tool. Each summary includes `name`, `title`, `description`, `tier`, `readOnly`, `openWorld`, `destructive`, and `annotationJustification`; the category is implicit in the enclosing group. The `annotationJustification` field is a short rationale for why the safety hints were assigned to that tool.
- Every listed tool also carries a top-level `title` — a short display name for tool pickers and permission prompts. Tool names follow `warmhub__` ↔ `wh `; `warmhub_capabilities` returns the two aliases, the eight exceptions, and the two tools with no CLI command under `naming`. See the [CLI name convention](/agent-integration/mcp-tools-reference/#cli-name-convention).
### Authentication Awareness
Tool responses include `structuredContent.auth` with the caller's authentication status. An anonymous call reports `authenticated: false` and carries a hint:
```json
{ "structuredContent": { "items": [...], "auth": { "authenticated": false, "hint": "..." } } }
```
The tier itself is not in the descriptions. Read it from `warmhub_capabilities`, where every tool summary carries `tier: 'anonymous' | 'builder'`; the anonymous-access guidance is stated once there as a usage pattern. Calling a builder-tier tool without a token returns the `401` challenge, which is the signal to authenticate. A miss on a private or missing target from an anonymous caller is reported in `auth.hint` as possibly private; the MCP handler preserves the backend error code (`NOT_FOUND`, `WREF_UNRESOLVABLE`, `FORBIDDEN`, or `UNAUTHENTICATED`) rather than rewriting all such failures to a single code, so branch on the specific `errorCode` in the structured error.
## Error Handling
MCP tool failures arrive in three distinct ways. Inspect the tool response, not just the client-level `try`/`catch`.
**Tool-result errors.** The tool dispatched, but its execution failed. The response carries `isError: true` and a structured error in `content` / `structuredContent`. This covers `VALIDATION_ERROR`, `NOT_FOUND`, `FORBIDDEN`, `RATE_LIMITED` (per-user / per-org tier limits hit during the call), `QUERY_TOO_EXPENSIVE` (typed `where` predicate exceeded the query budget), and `warmhub_commit_submit` ambiguous append failures. Each code is described in the [Tool-error code reference](#tool-error-code-reference) below.
**`warmhub_commit_submit` per-operation failures.** Per-op failures are *not* tool errors. The response is a normal success payload in which one or more `operations[]` rows carry `status: "error"` plus a non-empty `errors[]` array. There is no `partial` flag and no `statusCounts` aggregate to branch on — scan the rows for `status: "error"` yourself. Treat partial results as a routine outcome to inspect. See the [tool reference](/agent-integration/mcp-tools-reference/#warmhub_commit_submit) for the full row contract, including the legacy receipt version that spells this status differently.
**Transport-level errors.** Some failures happen before the tool dispatches and surface as HTTP errors instead of tool results:
- **POST with no `Authorization` header, calling an anonymous-tier tool** → HTTP 200 over public data, with `structuredContent.auth.authenticated: false`.
- **POST with no `Authorization` header, calling a builder-tier tool** → HTTP 401 with the [`WWW-Authenticate` challenge](#authentication), returned before the tool runs; use it to authenticate.
- **POST with no `Authorization` header, once the anonymous rate-limit budget is exhausted** → HTTP 429 (see the anonymous rate-limit bullet below) rather than the `401` challenge. If you are seeing 429 on headerless requests, authenticate rather than only backing off.
- **POST with a malformed or unverified bearer** → treated as anonymous: builder-tier calls get the `401` challenge, and the request draws down the anonymous rate-limit budget. A `429` after presenting a token means the token wasn't accepted — *re-authenticate* with a valid one rather than only backing off.
- **POST with a valid bearer the gate never got to verify** → treated as anonymous, and therefore able to draw a transport-level 429 even though the token itself is good. Verifying an unrecognized bearer is admitted through its own per-IP budget, separate from the anonymous request budget; when that budget is exhausted the token is not checked at all and the request falls back to the anonymous path. A token verified recently enough to still be cached skips this budget entirely, so this affects bursts of first-time tokens from one IP. Back off and retry rather than re-issuing the token. The 429 body is identical to the anonymous case described below.
- **Request body exceeds the MCP size limit** → HTTP 413 with a JSON-RPC error body. The error uses code `-32013`. This check runs before tool dispatch, so no tool result is produced. Reduce the request payload and retry.
- **Anonymous request over the rate limit** → HTTP 429 carrying a JSON-RPC transport error (code `-32029`) whose retry metadata lives at `error.data.warmhub`: `{ code, message, status, hint, retryAfter, details }`. A request whose bearer was never verified reaches this same path — see the verification-budget bullet above. The `details` object has the shape `{ reason: "rate_limit_reset", retryAfterSeconds, resetAt }`. Read `retryAfter` (seconds) to back off; read `details.resetAt` for the precise reset timestamp. This transport-level 429 shape — with retry metadata at `error.data.warmhub.retryAfter` — differs from the tool-result `RATE_LIMITED` error; see the [Tool-error code reference](#tool-error-code-reference) below for the tool-result fields.
Once a bearer is cryptographically verified, MCP traffic bypasses the [anonymous rate limit](/http-api/rate-limiting/) entirely — the pre-authentication gate is skipped rather than passed through, so a verified token never draws a transport-level 429. Per-user and per-org tier limits hit *during* a call surface instead as the `RATE_LIMITED` tool-result error described below. The one way a token-bearing request still sees a transport-level 429 is the verification-budget fallback above, where the token was never verified and the request was handled as anonymous.
**Parsing transport-level 429 responses.** Both origins — an anonymous request, and a bearer the gate never got to verify — produce the same response shape: an HTTP 429 with a JSON-RPC error body where retry metadata lives at `error.data.warmhub`. Do not look for `backendCode` or `isError` on a 429 — those fields belong to tool-result errors and are absent here. Branch first on the HTTP status code: if it is 429, read `error.data.warmhub.retryAfter` and `error.data.warmhub.details.resetAt`; if it is 200 with `isError: true` in the body, parse the tool-result error fields instead.
### Tool-error code reference
- **Validation failures** — include the validation message and, for `warmhub_commit_submit` and `warmhub_commit_validate`, enriched hint data when the failing path is `operations` (see [Validation Hint](#commit-operations-validation-hint) below).
- **`NOT_FOUND`** — includes the error details. When the caller is authenticated, missing targets surface as `NOT_FOUND` regardless of which token was presented. When the caller is anonymous, a miss on a hidden or missing target preserves the backend error code (`NOT_FOUND`, `WREF_UNRESOLVABLE`, `FORBIDDEN`, or `UNAUTHENTICATED`) and appends a hint that the resource may be private.
- **`FORBIDDEN`** — surfaced as `errorCode: "FORBIDDEN"` in the structured error when the presented token is valid but lacks the scope required for the requested resource. Distinct from `NOT_FOUND`; use it to decide whether retrying with a more privileged token is sensible.
- **`RATE_LIMITED`** — surfaced as `errorCode: "RATE_LIMITED"` in `structuredContent.error.data` when a per-user or per-org write limit is hit during a tool call. The same `data` object also carries a `details` field with the shape `{ reason: "rate_limit_reset", retryAfterSeconds, resetAt }`. See [Rate Limiting](/http-api/rate-limiting/) for tier limits and retry guidance. Treat as retryable with a bounded backoff, prefer the `errorCode` over string-matching the message, and read `details.resetAt` for the precise reset time. Note that this tool-result shape (`errorCode` plus `details` under `structuredContent.error.data`) differs from the transport-level 429 shape (JSON-RPC error code `-32029`, with `error.data.warmhub` carrying `code`, `message`, `status`, `hint`, `retryAfter`, and `details`) — on a transport-level 429 no tool ran, so there is no tool result to read. Parse them separately: check for `isError: true` in a tool result to identify a `RATE_LIMITED` tool error; check for an HTTP 429 response with a JSON-RPC error body to identify a transport-level rate limit.
- **`QUERY_TOO_EXPENSIVE`** — surfaced as `errorCode: "QUERY_TOO_EXPENSIVE"` in `structuredContent.error.data` when a typed `where` predicate is rejected as too broad or complex to execute within the query budget. This is distinct from `RATE_LIMITED`: it signals that the query itself needs to be narrowed, not that a throughput limit was hit. Recovery depends on which tool raised the error:
- **Paged `where` queries** (`warmhub_thing_head`, `warmhub_thing_query`, `warmhub_thing_about`): tighten the `where` predicate to reduce the result set, or retry with a shallower page size.
- **Count-mode requests** (`warmhub_thing_head` or `warmhub_thing_query` called with `count: true`): count mode has no page-size argument, so reducing page size is not applicable. Narrow the query with a more selective equality predicate instead.
Backing off without changing the query will not resolve this error in either case.
- **Ambiguous append failures** (`warmhub_commit_submit` only) — a transport-ambiguous failure (network reset, 5xx, timeout) interrupted the stream append. The failed append may have landed server-side.
The tool returns `isError: true`. Do not resubmit blind — resolve the outcome first:
1. Stop sending writes.
2. Read `eventRequestId` from `structuredContent.error.data`.
3. Call `warmhub_commit_receipt_get` with that `eventRequestId`. The receipt is the authoritative answer to whether the append landed.
4. Retry only what the receipt shows did not land, reusing the original `submissionId`.
`structuredContent.error.data` carries the fields you need for that lookup:
- `tool` — `"warmhub_commit_submit"`
- `errorCode` — the error code; parse this field
- `backendCode` — alias for `errorCode`; parse `errorCode` instead
- `submissionId` — the submission identity to reuse on a retry
- `eventRequestId` — pass this to `warmhub_commit_receipt_get`
- `lookupTool` — `"warmhub_commit_receipt_get"`, the tool to call
Ambiguous errors never carry `continuation`. Note: `streamId`, when present, does not provide recovery or idempotency, and `skipExisting: true` is an idempotency convenience for fixed-name adds — a later noop or conflict is not a receipt for the ambiguous attempt, so prefer the receipt lookup over inferring outcome from repository state. See [SDK Streaming Write Failures](/sdk/transient-retry/) for the parallel SDK contract.
Tool-result errors (the `isError: true` responses above) also include `structuredContent.auth` with the caller's authentication status, so agents can decide whether to suggest authenticating and retrying. Transport-level failures do not carry this field.
### Commit operations validation hint
When `warmhub_commit_submit` or `warmhub_commit_validate` rejects a malformed `operations` entry, the tool result's `structuredContent.error.data` lists the full set of valid operation variant signatures so agents can self-correct on the next turn — the `message` is the original validation message.
```json
{
"resultType": "complete",
"isError": true,
"content": [{ "type": "text", "text": "" }],
"structuredContent": {
"error": {
"code": -32602,
"message": "",
"data": {
"tool": "warmhub_commit_submit",
"errorCode": "VALIDATION_ERROR",
"backendCode": "VALIDATION_ERROR",
"expected": "one of the operation variants",
"operations": [
"ADD shape: { operation:'add', kind:'shape', name, data }",
"ADD thing: { operation:'add', kind:'thing', name, data }",
"ADD assertion: { operation:'add', kind:'assertion', name, about, data, affirmedTargets? }",
"ADD collection: { operation:'add', kind:'collection', name, type, members }",
"REVISE shape: { operation:'revise', kind:'shape', name, data }",
"REVISE thing: { operation:'revise', kind:'thing', name, data }",
"REVISE assertion: { operation:'revise', kind:'assertion', name, data, affirmedTargets? }",
"REVISE collection: { operation:'revise', kind:'collection', name, type, members }",
"RETRACT: { operation:'retract', name, reason?, kind?, expectedVersion? }",
"REAFFIRM assertion: { operation:'reaffirm', name, add?, remove?, expectedVersion? }",
"RENAME: { operation:'rename', name, newName, kind? }"
]
}
},
"auth": { "authenticated": true }
}
}
```
The `expected` and `operations` fields are attached for `warmhub_commit_submit` and `warmhub_commit_validate` validation errors when the failing path is `operations`; validation errors from other tools, or from `warmhub_commit_validate` on a path other than `operations`, continue to return `data` with just `tool` and `errorCode`. `backendCode` may also appear as a compatibility alias for `errorCode`; parse `errorCode`. The same variant list is also returned by `warmhub_repo_describe` under `commitContract.operationVariants`.
---
# MCP Tool Walkthrough
> Bootstrap, read, write, and query a WarmHub repo through the MCP tool sequence.
:::note[New to MCP?]
Start with the [Quickstart](/get-started/#connect-via-mcp) to set up your client first. This page assumes you already have an MCP-connected agent pointed at WarmHub.
:::
:::tip[Try it against live data]
The walkthrough below uses a generic game-world example for clarity. To run it against real data, point your authenticated MCP client at the public [`warmhub-data/us.congress.trades`](https://app.warmhub.ai/orgs/warmhub-data/repos/us.congress.trades) repo and walk the same `warmhub_capabilities` → `warmhub_repo_describe` → read-tools sequence against the `CongressTrade` shape. Other public repos in the [warmhub-data org](https://app.warmhub.ai/orgs/warmhub-data) work too — let `warmhub_repo_describe` tell you which shapes each one exposes.
The walkthrough interleaves reads and writes: run steps 1-3 and 5 against `warmhub-data/*` (the discovery + read steps — substitute your target repo's shapes from `warmhub_repo_describe`'s output for the placeholder `Location`/`Observation` payloads), and skip step 4 (the write step) on public datasets — it needs permission on the target repo. To run writes, pass a repo you own as the `orgName`/`repoName` arguments.
This walkthrough assumes a signed-in client because it includes builder-tier steps such as `warmhub_repo_describe` and `warmhub_commit_submit`. Several read tools — including `warmhub_capabilities`, `warmhub_thing_head`, `warmhub_thing_get`, `warmhub_thing_query`, `warmhub_shape_list`, and `warmhub_shape_get` — are available anonymously on public repos, so a token is not required for those calls alone.
:::
Once connected, an agent can interact with WarmHub using the tools below. Here is the recommended sequence for bootstrapping.
## 1. Discover the catalog with `warmhub_capabilities`
Call `warmhub_capabilities` first to see which tools are callable on the current endpoint, grouped by category, plus a short workflow cookbook and the wref syntax reference:
```json
{
"name": "warmhub_capabilities"
}
```
The payload is static (no arguments).
Use this as your catalog index, then follow up with `warmhub_repo_describe` for per-repo specifics.
## 2. Orient to the repo with `warmhub_repo_describe`
Call `warmhub_repo_describe` to get a complete picture of the repository:
```json
{
"name": "warmhub_repo_describe",
"arguments": {
"orgName": "my-org",
"repoName": "my-repo"
}
}
```
This returns:
- **Shapes** defined in the repo, including field types, optional shape-level descriptions, and per-field inline descriptions extracted from [typed field objects](/data-modeling/shapes/#typed-field-objects) (fields with descriptions appear as `{ "type": "number", "description": "Horizontal position" }`)
- **Summary counts** — `shapeCount`, `subscriptionCount`, `totalCount`, plus breakdowns by kind and by shape
- **Sample wrefs** — example references the agent can use immediately
- **Write examples** — ready-to-use `warmhub_commit_submit` operations tailored to the repo's actual shapes
The write examples are generated from the repo's current shape definitions, so the agent gets correct field names and types without guessing.
## 3. Read current state with `warmhub_thing_head`
Get a snapshot of all active things:
```json
{
"name": "warmhub_thing_head",
"arguments": {
"orgName": "my-org",
"repoName": "my-repo"
}
}
```
Filter by shape or kind for targeted results:
```json
{
"name": "warmhub_thing_head",
"arguments": {
"orgName": "my-org",
"repoName": "my-repo",
"shape": "Location",
"limit": 10
}
}
```
## 4. Write data with `warmhub_commit_submit`
Create or update things and assertions with versioned write operations. The `committer`
wref below must already resolve to an existing thing — create the agent
identity in a prior write, or omit `committer` to attribute the write to
the authenticated user.
```json
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "my-org",
"repoName": "my-repo",
"submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"committer": "Agent/claude",
"message": "Add initial game locations",
"operations": [
{
"operation": "add",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 3, "y": 7, "label": "Dark Cave" }
},
{
"operation": "add",
"kind": "assertion",
"name": "Observation/cave-safe",
"about": "Location/cave",
"data": { "safe": true, "confidence": 0.8 }
}
]
}
}
```
`submissionId` is required — supply a **UUID** (e.g. `crypto.randomUUID()` in JavaScript, `uuid.uuid4()` in Python, or any standard UUID v4 generator) that identifies this logical submission. The tool validates this field before attempting the write; omitting it or supplying a non-UUID value will fail before any operations are applied.
**Retries must reuse the same `submissionId`.** Minting a new UUID on a retry changes the request identity and will not be treated as a retry of the original submission. If a call fails with an ambiguous append error, its `structuredContent.error.data` carries an `eventRequestId` alongside `lookupTool: "warmhub_commit_receipt_get"`. Call that tool with the `eventRequestId` to check whether the write landed before deciding whether to retry — `warmhub_commit_receipt_get` takes `eventRequestId`, not `submissionId`, and rejects unknown arguments.
A single MCP write request can carry multiple operations across `add` (shapes, things, assertions, collections), `revise` (shapes, things, assertions, collections), `retract` (any entity kind), and `rename` (any entity kind). The response payload may include per-operation results — inspect each operation's outcome individually, as partial failures are a normal success payload that callers must handle. See the [MCP Tools Reference](/agent-integration/mcp-tools-reference/#warmhub_commit_submit) for the exact response shape and [Writes](/writes/overview/) for the cross-surface contract.
## 5. Query with `warmhub_thing_query`
For targeted retrieval by shape, kind, or about-reference:
```json
{
"name": "warmhub_thing_query",
"arguments": {
"orgName": "my-org",
"repoName": "my-repo",
"shape": "Observation",
"about": "Location/cave"
}
}
```
This returns all active `Observation` assertions about `Location/cave`.
## Key Concepts for Agents
**Discover, then describe.** Start with `warmhub_capabilities` to learn what tools are callable on the current endpoint, then call `warmhub_repo_describe` for schema definitions, sample data references, wref syntax rules, and write contract examples tailored to the repo.
**`warmhub_commit_submit` requires a `submissionId`.** Supply a UUID identifying the logical submission, and reuse it when retrying rather than minting a new one. If a write fails and you cannot tell whether it landed, look up the receipt before retrying — the error tells you which value to look it up by. Partial failures are a normal success payload, so inspect each operation's outcome individually. See [Step 4](#4-write-data-with-warmhub_commit_submit) above and the [MCP Server](/agent-integration/mcp-server/#error-handling) page for the failure taxonomy and payload shapes.
**Wrefs address everything.** Things are referenced by `Shape/name` (local) or `wh:org/repo/Shape/name` (canonical). Assertions use `about` to reference their subject.
**Prefer `warmhub_thing_query` for targeted reads.** Use `warmhub_thing_head` for broad orientation and `warmhub_thing_query` when you know what shape or subject you need.
## Full Tool Reference
WarmHub exposes MCP tools covering organizations, repositories, shapes, things, assertions, commits, subscriptions, actions, and meta (capability discovery). See the [MCP Tools Reference](/agent-integration/mcp-tools-reference/) for the complete list with argument schemas and descriptions.
## Next Steps
- [Core Concepts](/get-started/core-concepts/) — review the data model behind things, assertions, and writes
- [MCP Server](/agent-integration/mcp-server/) — detailed MCP server configuration and protocol details
- [MCP Tools Reference](/agent-integration/mcp-tools-reference/) — full reference for all MCP tools
- [Agent Context (wh prime)](/agent-integration/wh-prime/) — CLI-side bootstrapping for agents with shell access
---
# MCP Tools Reference
> Complete catalog of all MCP tools with parameters and descriptions.
WarmHub exposes a full MCP tool catalog for organizations, repositories, shapes, things, assertions, writes, subscriptions, actions, collections, and meta (capability discovery). Repo-level tools take `orgName` and `repoName` as arguments.
## SDK and MCP Method Map
If you switch between the MCP tools and the [TypeScript SDK](/sdk/overview/), the names line up almost one-to-one. A few common pairs:
| MCP tool | SDK method |
|----------|-----------|
| `warmhub_thing_head` | `client.thing.head(...)` |
| `warmhub_thing_query` | `client.thing.query(...)` |
| `warmhub_thing_get` | `client.thing.get(...)` for one wref, `client.thing.getMany(...)` for several |
| `warmhub_shape_list` | `client.shape.list(...)` |
| `warmhub_subscription_list` | `client.subscription.list(...)` |
| `warmhub_commit_submit` | `client.commit.apply(...)` — note the verb differs |
| `warmhub_commit_validate` | `client.commit.validate(...)` |
| `warmhub_capabilities` | `client.diagnostics.capabilities()` — returns the backend API version, minimum supported SDK, minimum supported clients, write contract version, feature flags, and the client flags honored for your request (not the tool catalog) |
| `warmhub_repo_describe` | `client.repo.describe()` — returns repo metadata, shapes, stats, subscriptions, license, and a page of HEAD records; the MCP tool additionally returns the write contract and generated write examples, which the SDK method omits |
Most tools follow the `warmhub__` ↔ `client..` pattern. `warmhub_thing_get` is one MCP tool over two SDK methods: the MCP tool takes a `wrefs` array at every cardinality, while the SDK keeps single-wref `thing.get` and batch `thing.getMany` separate. The last four rows are the other exceptions — the SDK verb differs (`commit.apply`), returns different data (`diagnostics.capabilities`), or omits the MCP-only write contract and generated examples (`repo.describe`).
## CLI Name Convention
`warmhub__` maps to the CLI command `wh `. Two systematic aliases cover most of the rest:
| MCP name | CLI name |
|----------|----------|
| `_get` | ` view` — `warmhub_repo_get` is `wh repo view` |
| `subscription_*` | `sub *` — `warmhub_subscription_list` is `wh sub list` |
Eight tools diverge from the rule and its aliases. An exception overrides both:
| MCP tool | CLI command | |
|----------|-------------|---|
| `warmhub_thing_head` | `wh thing list` | described |
| `warmhub_component_uninstall` | `wh component teardown` | described |
| `warmhub_commit_receipt_get` | `wh commit receipt` | described |
| `warmhub_commit_validate` | `wh commit submit --dry-run` | described |
| `warmhub_repo_content_get` | `wh repo content get` | |
| `warmhub_subscription_get` | `wh sub view` | |
The four marked *described* also repeat the mapping in their own tool description; the rest are listed only here and in `warmhub_capabilities`, where a correction costs no `tools/list` tokens. `warmhub_subscription_runs` and `warmhub_capabilities` have no CLI command at all and are returned under `noCliEquivalent`.
`warmhub_capabilities` returns this whole mapping under `naming`, so an agent can read it without leaving the protocol.
The SDK is a separate mapping — mostly `client..`, with the exceptions in the [SDK and MCP method map](#sdk-and-mcp-method-map) above. `naming` covers the CLI only.
## Tool Titles
Every tool carries a `title` alongside its `name` — a short display name such as `Submit Commit` or `List Shapes`. MCP clients show it in tool pickers and permission prompts. It is a display name only: hints, CLI mappings, and auth guidance live in the description or in `warmhub_capabilities`, never in the title.
:::note[Auth awareness is a runtime signal]
Every tool carries a reviewed auth tier, and `warmhub_capabilities` reports it per tool as `tier: 'anonymous' | 'builder'`. That field — not `readOnlyHint` — is the authoritative answer to "can I call this without credentials?": several builder-tier tools are read-only, including `warmhub_commit_validate`, `warmhub_org_list`, `warmhub_org_member_list`, `warmhub_repo_describe`, the subscription and action readers, and `warmhub_commit_receipt_get`, each of which reads through an authorization-gated procedure.
Tool descriptions carry no auth note. The signal is on the responses instead: every response carries `structuredContent.auth.authenticated`, and a miss from an anonymous caller carries `auth.hint` saying the resource may be private. `warmhub_capabilities` states the guidance once, as the `Anonymous access` usage pattern.
:::
## Meta Tools
Orientation and capability discovery. The `meta` category covers three tools:
| Tool | Description |
|------|-------------|
| `warmhub_capabilities` | Static, endpoint-scoped overview of the MCP tool catalog: tools grouped by category, a workflow cookbook, [wref](/data-modeling/wrefs/) syntax, and a pointer to the full write operation contract. Read-only; no arguments. |
| `warmhub_repo_describe` | Per-repo live view: schema, shape descriptions, field types, summary stats, wref syntax, operation contract, write examples generated from the repo's own shapes, and the repo's license. Documented under [Repository Tools](#repository-tools). |
| `warmhub_doctor` | Diagnostic tool. Takes no arguments. Returns `{ ok: true, capabilities, hint }`, where `capabilities` is a diagnostics payload containing `apiVersion`, `minSupportedSdk`, `minSupportedClients`, `writeContractVersion`, `features`, and `honoredClientFlags`. The last of those echoes back which *client flags* — opt-in compatibility switches a client declares on its request — were honored for **your** request; it is empty when you declared none. |
Call `warmhub_capabilities` first to orient on what tools exist; then call `warmhub_repo_describe` to learn the repo-specific shapes and write examples.
### warmhub_capabilities
Takes no arguments. Returns a static orientation payload with the following fields:
| Response Field | Type | Description |
|----------------|------|-------------|
| `categories` | object[] | One entry per tool category (`org`, `repo`, `shape`, `thing-read`, `collection`, `commit`, `subscription`, `action`, `meta`), each with `name`, `description`, and the tools (`name`, `title`, `description`, `tier`, `readOnly`, `openWorld`, `destructive`, `annotationJustification`) advertised on the current endpoint. `tier` is `anonymous` or `builder` — the authoritative answer to whether the tool is callable without credentials. |
| `naming` | object | The [CLI name convention](#cli-name-convention) as `{ rule, aliases: [{ mcp, cli }], exceptions: [{ tool, cli }], noCliEquivalent: string[] }`. An `exceptions` entry overrides the rule and any alias. |
| `cookbook` | object[] | Common workflows as `{ task, steps: [{ tool, note }] }` — e.g. discovering shapes, searching by content, writing first data. |
| `usagePatterns` | object[] | Query-discipline guidance — recommended patterns for reading, querying, and writing efficiently. |
| `wrefSyntax` | object | Local and canonical wref forms, version modifiers, path/name constraints, and write-path preview rules. |
| `commitContractRef` | object | Pointer to `warmhub_repo_describe`, which returns the full write operation contract, operation variants, and live write examples scoped to a specific repo. |
`categories` lists every registered tool — there is one endpoint and one catalog.
Call `warmhub_capabilities` first to orient an agent, then call `warmhub_repo_describe` for per-repo schema and write examples.
## Organization Tools
Organization tools take `orgName` as an argument and need no repository.
| Tool | Description |
|------|-------------|
| `warmhub_org_list` | List organizations (archived hidden by default). *(global only)* |
| `warmhub_org_get` | Get an organization by name. *(global only)* |
| `warmhub_org_member_list` | List members of an organization. *(global only)* |
### warmhub_org_list
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `includeArchived` | boolean | no | Include archived organizations in results |
**Response fields:**
| Response Field | Type | Description |
|----------------|------|-------------|
| `repoCount` | integer | Number of repos in the organization readable by the caller. Includes only repos the caller has effective [`repo:read`](/auth/getting-access/#repository-visibility) access to. |
| `errorCount` | integer | Number of repos in the organization that have at least one active subscription whose most recent completed run failed. |
| `lastActivityAt` | integer | Unix timestamp in milliseconds of the most recent activity across the organization's readable repos. Advances on repo creation even before the first write has been submitted to that repo. Omitted when no activity has been recorded. |
### warmhub_org_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
### warmhub_org_member_list
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `pending` | boolean | no | When `true`, returns only pending (invited but not yet accepted) members. When omitted or `false`, returns all members regardless of status. |
## Repository Tools
| Tool | Description |
|------|-------------|
| `warmhub_repo_create` | Create a new repository in an organization. *(global only)* |
| `warmhub_repo_list` | List repositories in an organization (archived hidden by default). *(global only)* |
| `warmhub_repo_get` | Get repository metadata by org/repo. |
| `warmhub_repo_describe` | Describe repository schema, shape descriptions, field types, per-shape `queryHints`, summary stats, and license for agent bootstrapping. |
### warmhub_repo_create
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name (slug) |
| `displayName` | string | no | Human-readable display name for the repo. Must be non-empty and non-whitespace if provided; max 100 characters. Defaults to the repo slug when omitted. |
| `description` | string | no | Repository description. Max 2000 characters. |
| `visibility` | string | no | `"public"` or `"private"`. Defaults to `"private"`. |
Returns the created repo object (same shape as `warmhub_repo_get`). Only available on the global MCP endpoint.
### warmhub_repo_list
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `includeArchived` | boolean | no | Include archived repositories in results |
| `limit` | integer | no | Max repos to return (1–200). Must be paired with `cursor` when paging. |
| `cursor` | string | no | Pagination cursor from a prior response. Must be paired with `limit`. |
### warmhub_repo_describe
The most important tool for agent bootstrapping. Returns:
- Repository metadata
- Shape definitions with field types and optional shape-level `description`
- Per-shape `queryHints` with `queryableFields`, `wrefFields`, and suggested MCP query patterns
- Per-field descriptions inlined into each field entry. Fields without descriptions appear as bare type strings (e.g. `"string"`); fields with descriptions appear as `{ "type": "number", "description": "Horizontal position" }`. Descriptions are extracted from [typed field objects](/data-modeling/shapes/#typed-field-objects).
- Summary counts (`shapeCount`, `subscriptionCount`, `totalCount`)
- Counts by kind and by shape
- Sample wrefs from the repo
- Wref syntax reference
- Operation contract — `operationVariants` (the add/revise/retract/rename/reaffirm forms), `commitRules` (revise-data, retract-only, illegal op sequences, noop, opinion rules), and `aboutSemantics` — plus `writeExamples` generated from the repo's own shapes
- Write examples generated from actual repo shapes
- `license` — the repository's current license metadata, or `null` when the repo has no active, visible, valid license declaration (see the response field description below)
- Indexed field metadata when `includeIndexedFields` is set (see below)
Call this first when connecting to a repo.
**Input parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `includeIndexedFields` | boolean | no | When `true`, the response includes an `indexedFields` block with typed field index state across the repo's shapes. Defaults to `false`. |
**Response fields (selected top-level):**
| Response Field | Type | Description |
|----------------|------|-------------|
| `license` | object \| null | The repository's current license metadata, or `null` when the repo has no active, visible, valid license declaration. Subfields are listed below. |
When `license` is non-null, the object carries these fields:
| Field | Type | Description |
|-------|------|-------------|
| `spdxId` | string | The SPDX license identifier. Always present when `license` is non-null. |
| `licenseWref` | string \| null | Wref of the license declaration thing. |
| `spdxExpression` | string \| null | The full SPDX expression as stored. |
| `appliesTo` | string \| null | Wref of the subject the license applies to. |
| `attributionText` | string \| null | Optional attribution text. |
| `declaredBy` | string \| null | Wref of the declaring entity. |
| `sourceUrl` | string \| null | Link to the license text. |
The response includes an `additionalInformation` array pointing at the three well-known [Content shape](/data-modeling/content-shape/) wrefs:
```json
"additionalInformation": [
{ "name": "Readme", "wref": "Content/Readme", "synthesized": false },
{ "name": "Agents", "wref": "Content/Agents", "synthesized": false },
{ "name": "LlmsTxt", "wref": "Content/LlmsTxt", "synthesized": true }
]
```
When `includeIndexedFields: true` is passed, the response also includes an `indexedFields` block. The block exposes four state buckets — `ready`, `building`, `failed`, and `other` — each containing an array of field entries. Every entry carries the field's `state`, associated timestamps, and backfill counters. Parse all four buckets when acting on index state: a field in `building` is not yet queryable, and a field in `failed` requires attention before structured queries against it will succeed.
## Content Tools
Two tools cover the built-in [Content shape](/data-modeling/content-shape/) — `Readme`, `Agents`, and the synthesized `LlmsTxt` — discriminated by a `kind` argument.
| Tool | Description |
|------|-------------|
| `warmhub_repo_content_get` | Fetch repo Content markdown by `kind`. For `readme`/`agents`, returns a synthesized empty stub when nothing has been written — never null. `kind: llms-txt` always returns a synthesized response with the rendered sitemap; the structured `refs` field is present for authenticated callers only. Read-only. |
| `warmhub_repo_content_set` | Set `Content/Readme` or `Content/Agents` markdown (commits an add or revise operation). Writes to `kind: llms-txt` are rejected — it is synthesized and cannot be stored. Requires `repo:write`. |
WarmHub no longer hosts README/AGENTS generation. To draft content, run the CLI command `wh repo content prompt --kind readme` to get an agent-ready prompt, let your own agent write the markdown, then persist it with `warmhub_repo_content_set`.
### warmhub_repo_content_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
| `kind` | string | yes | One of `readme`, `agents`, or `llms-txt` |
For `kind: readme` and `kind: agents`, returns the stored content thing after the first write. When nothing has been written yet, the response is a synthesized empty stub (`{ synthesized: true, shape: "Content", name: "...", data: { content: "" }, active: true }`).
For `kind: llms-txt`, always returns a synthesized response. `data.content` contains the full rendered markdown. For an authenticated caller the response also carries a `refs` field with partitioned outbound/inbound references, minus any cross-org refs that caller cannot read.
`warmhub_repo_content_get` is an anonymous-tier tool, so an unauthenticated caller gets the basic body — H1, description, license line, and shapes — with no `refs` field and no ref sections in the markdown. See [Content Shape](/data-modeling/content-shape/).
### warmhub_repo_content_set
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
| `kind` | string | yes | One of `readme`, `agents`, or `llms-txt`. `llms-txt` is read-only — `set` attempts are rejected. |
| `content` | string | yes | Markdown content to store |
| `eventRequestId` | UUID | yes | Caller-known idempotency key for this write request. Reuse only for an identical retry. |
Returns the standard single-operation commit result: `operationCount`, one `operations[]` entry (`name`, `operation`, `version`, `dataHash`), plus optional `committer`, `createdByEmail`, and `message` metadata. Unlike `warmhub_commit_submit`, this helper does not return partial-result fields; rejected writes surface as tool errors.
## Thing / Query Tools
| Tool | Description |
|------|-------------|
| `warmhub_thing_head` | List all items at HEAD with optional filters (shape, kind, glob match). Supports incremental reads via `sinceRepoSeq` — see the parameter reference below. Use to enumerate a repo's current state; for fuzzy lookups use `warmhub_thing_search`. |
| `warmhub_thing_get` | Fetch one or more things by wref in one call — pass every wref you need together rather than calling repeatedly. Always returns a `{ requested, items, missing }` envelope; a wref that does not exist or is not visible to you is listed in `missing`, not returned as an error. |
| `warmhub_thing_graph` | Get one thing and its embedded assertion/about/wref graph to a bounded depth. |
| `warmhub_thing_history` | List version history for a thing. Provide `wref` for one thing's history, or `shape`/`about` to survey history across matching things. With `about`, `resolveCollections:true` includes assertions about Arc/Bond/Set/List collections containing the target identity. |
| `warmhub_thing_about` | List assertions whose about target resolves to the supplied target identity. Use `resolveCollections:true` to include assertions about Arc/Bond/Set/List collections containing the target; pinned `@vN` inputs stay version-exact and do not expand collection members. Use `warmhub_thing_refs` with `direction:"inbound"` for broader backlink discovery. |
| `warmhub_thing_query` | Query things by structured filters (shape, kind, about, glob match). Supports incremental reads via `sinceRepoSeq` — see the parameter reference below. Best for exact/structured lookups; for fuzzy or semantic search use `warmhub_thing_search`. |
| `warmhub_thing_search` | Full-text/vector/hybrid search across thing data. Best for fuzzy lookups; for exact field matches use `warmhub_thing_query` with a glob filter. |
| `warmhub_thing_refs` | List inbound or outbound refs for a target wref. Use `direction:"inbound"` to find what references X. Pair with `warmhub_thing_get` to resolve details. |
### warmhub_thing_head
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shape` | string | no | Filter by shape name |
| `kind` | string | no | Filter by kind. One of `shape`, `thing`, `assertion`, or `collection`. |
| `match` | string | no | Glob pattern to filter wrefs (`*` = one segment, `**` = zero or more) |
| `where` | object[] | no | Typed field-value predicates (`{ fieldPath, op, rhs }`), ANDed, max 8. See [Field-Value Predicates](/queries/filtering/#field-value-predicates). |
| `excludeInfraShapes` | boolean | no | Hide internal infra shapes from results |
| `count` | boolean | no | Return count of matching items instead of the full result list |
| `limit` | integer | no | Max items (minimum 1) |
| `cursor` | string | no | Pagination cursor from previous response |
| `sinceRepoSeq` | integer | no | Return only items that changed after this repo sequence number. Pass `-1` to start from the beginning. Reuse the same lower bound across all pages of a single incremental read; only persist the `repoSeq` returned on the exhausted final page or on an exact count result — intermediate pages do not carry `repoSeq`. Requires full-repository read authority. Not available when using glob filters (`match`). |
When an incremental read is complete (final page exhausted, or `count: true` result), the response carries a top-level `repoSeq` field. Use that value as `sinceRepoSeq` on the next poll to receive only subsequent changes.
### warmhub_thing_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wrefs` | string[] | yes | One or more WarmHub references, 1 through 500 entries per call |
| `version` | integer | no | Pin all lookups to this version |
| `includeRetracted` | boolean | no | Return things even when retracted at HEAD or at the requested `version` |
| `dataMode` | string | no | `"auto"` (default) or `"full"`. In `"auto"` mode, collections larger than 100 members return a summary (member count plus a truncated preview) instead of the full body; fixed-arity Arc/Bond collections and collections with 100 or fewer members keep their full body. Use `"full"` to force the complete body for large collections too. |
There is one call shape for every cardinality: pass a single-entry `wrefs` array for one thing and a longer array for many. The response is always the batch envelope `{ requested, items, missing }` — `requested` is the number of wrefs the call processed, `items[]` carries the resolved things, and `missing[]` is a `string[]` of the wrefs that came back empty.
A wref that does not exist, is retracted (without `includeRetracted: true`), or is not visible to the caller is listed in `missing` — it is **not** an error, so a partially-satisfiable batch still returns the things it could read. When a top-level `version` is supplied and the input wref is not already pinned, missing entries are version-qualified (`Shape@vN` or `Shape/name@vN`) so the round-trip is unambiguous; per-wref pins survive intact (no double-pinning). Duplicates in `wrefs` are not deduped — they count toward the 500-entry cap and produce duplicate `items`/`missing` entries.
`orgName`/`repoName` may be omitted when every wref is a [durable id](/data-modeling/wrefs/#durable-ids) or a canonical `wh:org/repo/...` reference — those route themselves to the repo that owns the target.
### warmhub_thing_graph
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | WarmHub reference |
| `version` | integer | no | Specific version number |
| `depth` | integer | no | Embedded traversal depth, 1 through 5 |
| `limit` | integer | no | Max embedded nodes, 1 through 500 |
Returns the root thing with readable assertion `about` links and readable wref-typed fields embedded as objects. Refs the caller cannot read remain string wrefs, with no internal IDs or denial reasons exposed.
`orgName`/`repoName` may be omitted when `wref` is a [durable id](/data-modeling/wrefs/#durable-ids).
### warmhub_thing_history
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | no | Thing wref |
| `shape` | string | no | Filter by shape |
| `about` | string | no | Filter by about target |
| `includeRetracted` | boolean | no | Allow resolving retracted shape or about targets (does not filter results) |
| `resolveCollections` | boolean | no | With `about`, include assertion history for Arc/Bond/Set/List collections containing the target identity, including when the about wref is pinned |
| `limit` | integer | no | Max versions to return |
| `cursor` | string | no | Pagination cursor from previous response |
At least one of `wref`, `shape`, or `about` is required.
`orgName`/`repoName` may be omitted only when `wref` is a [durable id](/data-modeling/wrefs/#durable-ids). `shape`/`about` surveys and local wrefs always require `orgName`/`repoName` — a repo-less filter query is rejected.
### warmhub_thing_about
By default, this tool returns assertions whose about target resolves to the supplied target identity. It does not expand collection member refs, so assertions about Arc, Bond, Set, or List collection things that contain the target appear only when `resolveCollections:true` is set. Pinned `@vN` inputs stay version-exact and do not expand collection members.
For broader graph discovery, use `warmhub_thing_refs` with `direction:"inbound"` to find current things that reference the target through wref fields. Use `warmhub_thing_about` when you specifically need assertion records and about-target filtering.
`orgName`/`repoName` may be omitted when `wref` is a [durable id](/data-modeling/wrefs/#durable-ids) — a durable id routes itself to the repo that owns the target. When `wref` is a local wref (e.g. `Shape/name` with no `wh:org/repo/` prefix), `orgName` and `repoName` are required; a repo-less call with a local wref is rejected.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | no | Organization name. Required when `wref` is a local wref (no `wh:org/repo/` prefix); may be omitted when `wref` is a durable id. |
| `repoName` | string | no | Repository name. Required when `wref` is a local wref (no `wh:org/repo/` prefix); may be omitted when `wref` is a durable id. |
| `wref` | string | yes | Target wref. Accepts a shaped-thing wref (`Shape/name`) or a bare shape wref (`Shape`). |
| `shape` | string | no | Filter assertions by shape |
| `match` | string | no | Glob pattern to filter assertion wrefs |
| `where` | object[] | no | Typed field-value predicates (`{ fieldPath, op, rhs }`), ANDed, max 8. See [Field-Value Predicates](/queries/filtering/#field-value-predicates). |
| `resolveCollections` | boolean | no | When `true`, includes assertions about Arc/Bond/Set/List collections containing the target entity. For identity-scoped inputs (bare wrefs, `@HEAD`, or `@ALL`), collection members are expanded. Pinned `@vN` inputs stay version-exact and do not expand collection members — the flag is accepted but has no expansion effect on pinned targets. |
| `role` | string | no | Filter by the role the target plays within a collection. Accepted only when `resolveCollections:true` is set; rejected otherwise. |
| `includeRetracted` | boolean | no | Resolve a retracted target and include retracted assertions in the returned results, including within the single `children` layer when `depth` is also set. |
| `depth` | integer | no | When set, returns one level of child assertions for each top-level result. Values greater than 1 do not produce additional nesting — only one `children` layer is returned regardless of the value supplied. Each child entry has an empty `children` array. |
| `limit` | integer | no | Max assertions to return |
| `cursor` | string | no | Pagination cursor from previous response |
:::note[Binomial-opinion constraint]
Any returned subjective-logic opinion `(b, d, u, α)` is a binomial opinion — well-formed only when the underlying assertion expresses a binary proposition (true/false). See [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions).
:::
### warmhub_thing_query
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shape` | string | no | Filter by shape |
| `about` | string | no | Filter by about target |
| `affirmedAbout` | string | no | Pinned target wref (`Shape/name@vN`) that returns only active assertions whose current version was recorded as affirming that exact pinned version of their `about` target. Use this filter when you need assertions that were explicitly evaluated against a specific version of a target — for example, to find all assertions that affirmed `Device/sensor-42@v7`. At least one `affirmedTargets` entry on the assertion must match the supplied pinned wref exactly. See [`warmhub_commit_submit`](#warmhub_commit_submit) for how `affirmedTargets` are set and updated. May be combined with `match`, `count`, scoped tokens, and search. |
| `kind` | string | no | Filter by kind. One of `shape`, `thing`, `assertion`, or `collection`. |
| `match` | string | no | Glob pattern to filter wrefs |
| `where` | object[] | no | Typed field-value predicates (`{ fieldPath, op, rhs }`), ANDed, max 8. See [Field-Value Predicates](/queries/filtering/#field-value-predicates). |
| `count` | boolean | no | Return count of matching items instead of the full result list |
| `resolveCollections` | boolean | no | When `about` is set, also include assertions about collections containing the target |
| `includeRetracted` | boolean | no | Include retracted entities |
| `componentRef` | string | no | Filter results to items owned by the given component (its `Org/Name` ref) |
| `excludeComponents` | boolean | no | Exclude component-owned items from results. Accepted alongside `componentRef`, but the two are mutually exclusive filters, so passing both returns no items. |
| `excludeInfraShapes` | boolean | no | Hide internal infra shapes from results |
| `limit` | integer | no | Max results. Must be between `1` and `500`. |
| `cursor` | string | no | Pagination cursor from previous response |
| `sinceRepoSeq` | integer | no | Return only items that changed after this repo sequence number. Pass `-1` to start from the beginning. Reuse the same lower bound across all pages of a single incremental read; only persist the `repoSeq` returned on the exhausted final page or on an exact count result — intermediate pages do not carry `repoSeq`. Requires full-repository read authority. Not available when using glob filters (`match`), collection expansion (`resolveCollections`), or queries against foreign sources — those query shapes cannot guarantee incremental results. |
When an incremental read is complete (final page exhausted, or `count: true` result), the response carries a top-level `repoSeq` field. Use that value as `sinceRepoSeq` on the next poll to receive only subsequent changes.
### warmhub_thing_search
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | yes | Search query text |
| `shape` | string | no | Filter by shape name |
| `kind` | string | no | Filter by kind. One of `shape`, `thing`, `assertion`, or `collection`. |
| `about` | string | no | Filter by about target (not supported with vector mode) |
| `affirmedAbout` | string | no | Pinned target wref (`Shape/name@vN`) that returns only active assertions whose current version was recorded as affirming that exact pinned version of their `about` target. Same semantics as the [`warmhub_thing_query` filter](#warmhub_thing_query). |
| `match` | string | no | Glob pattern to filter wrefs |
| `resolveCollections` | boolean | no | When `about` is set, also include assertions about collections containing the target (text mode only) |
| `mode` | string | no | `"text"` (default), `"vector"`, or `"hybrid"` |
| `includeRetracted` | boolean | no | Include retracted entities |
| `componentRef` | string | no | Filter results to items owned by the given component (its `Org/Name` ref) |
| `excludeComponents` | boolean | no | Exclude component-owned items from results. Accepted alongside `componentRef`, but the two are mutually exclusive filters, so passing both returns no items. |
| `excludeInfraShapes` | boolean | no | Hide internal infra shapes from results |
| `limit` | integer | no | Max results. Must be between `1` and `500`. |
| `cursor` | string | no | Pagination cursor from previous response (text mode only — vector and hybrid reject cursor). When `about` or `resolveCollections` is set, pages may be sparse — paginate until `nextCursor` is absent. |
### warmhub_thing_refs
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | WarmHub reference |
| `direction` | string | no | `"inbound"` (default) or `"outbound"` |
| `fieldPath` | string | no | Filter by field path (inbound only) |
| `limit` | integer | no | Max results. Must be between `1` and `500`. |
| `cursor` | string | no | Pagination cursor from previous response |
Direction `"inbound"` returns things that reference the target wref. Direction `"outbound"` returns things the target wref references.
Use inbound refs as a broad discovery tool when you are unsure whether data points directly at a thing or at a collection containing it. Inbound refs are not a substitute for `warmhub_thing_about` when you need assertion-only results or assertion filters.
## Cross-repo visibility
Cross-repo wref lookups (canonical forms `wh:org/repo/Shape` and `wh:org/repo/Shape/name`) require effective [`repo:read`](/auth/getting-access/#repository-visibility) permission on the target repo. Public repos are readable by anyone. For private repos, callers without that access see an error — except `warmhub_thing_search` with a cross-repo `about` (returns `{ items: [] }`) and `warmhub_thing_get` (puts unreadable wrefs into `missing[]`) — both to keep batch and search streaming-friendly.
See [Getting Access](/auth/getting-access/) for the precise rules.
## Write Tools
| Tool | Description |
|------|-------------|
| `warmhub_commit_submit` | Submit a list of operations against a repo. Returns a structured write result containing a `receipt` object and a per-operation `results` array. |
| `warmhub_commit_validate` | Evaluate one bounded operation batch with the real commit evaluator without persisting repository state or consuming a receipt. |
| `warmhub_commit_receipt_get` | Look up one immutable operation-event receipt by any caller-known `eventRequestId` in the repo. Most often used to resolve an ambiguous write, but not limited to that. |
Use `warmhub_thing_history` for per-thing version trails.
### warmhub_commit_submit
The tool's input schema contains the full structural contract for all operation variants. Supply a caller-known `submissionId`; the call returns a structured success payload with a top-level `receipt` object and a `results` array containing one outcome entry per submitted operation. Opinion-bearing assertions must be [binary propositions](/data-modeling/patterns/#opinions-as-separate-assertions).
Ambiguous append failures are separate from per-operation failures and return a tool-result error. **An ambiguous append failure may have landed server-side.** The error names `eventRequestId` and `warmhub_commit_receipt_get`; call that tool before retrying, rather than submitting another write or inferring the outcome from repository state. See [MCP Error Handling](/agent-integration/mcp-server/#error-handling) for the full failure taxonomy.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `committer` | string | no | Optional untyped [wref](/data-modeling/wrefs/) identifying the actor on whose behalf the writes are made. It must identify an existing thing. Omit it to use the authenticated caller's normal attribution. |
| `componentRef` | string | no | Attribute writes to an installed component, identified by its `Org/Name` ref. See component identity rules below. |
| `message` | string | no | Optional message recorded with each thing-version produced by this call |
| `submissionId` | UUID | yes | Caller-known logical submission identity. UUID casing is ignored; reuse only for an identical retry. |
| `operations` | array | yes | Non-empty atomic operation batch. After an ambiguous append failure, use the named receipt lookup before retrying. |
| `streamId` | string | no | Caller-managed stream identity for observability and diagnostic correlation; it does not provide recovery or idempotency. |
**Response shape:**
A submission returns a structured success payload. The receipt fields are spread onto the response root **and** nested under a `receipt` key, so callers can read either form. The top-level fields you can rely on are:
| Response Field | Type | Description |
|----------------|------|-------------|
| `event` | object \| null | Header for the persisted event: `committedAt`, `eventDigest`, `operationCount`, `repo.orgName` / `repo.repoName`, and `repoSeq` (a decimal string). `null` when `outcome` is `"no_event"`. Also present at `receipt.event`. |
| `eventRequestId` | UUID | Receipt identity for this submission. Pass it to `warmhub_commit_receipt_get` to resolve an ambiguous write. Also present at `receipt.eventRequestId`. |
| `operations` | array | One outcome entry per submitted operation — same entries as the top-level `results` array. Also present at `receipt.operations`. |
| `outcome` | string | `"event"` when the submission persisted an operation event, `"no_event"` when it persisted nothing. This is the submission-level result, not a per-operation status. Also present at `receipt.outcome`. |
| `requestDigest` | string | `sha256:`-prefixed digest of the submitted request. Also present at `receipt.requestDigest`. |
| `schemaVersion` | string | `"operation-event-receipt/v2"`. Also present at `receipt.schemaVersion`. |
| `submissionId` | UUID | The `submissionId` you supplied, normalized to lowercase. Also present at `receipt.submissionId`. |
| `results` | array | One outcome entry per submitted operation — see the per-operation entry description below. |
| `allocatedTokenRanges` | array | Always `[]`. |
| `createdByEmail` | string | Email of the authenticated writer, when available. |
| `receipt` | object | Nested copy of the `operation-event-receipt/v2` receipt. Contains the same `event`, `eventRequestId`, `operations`, `outcome`, `requestDigest`, `schemaVersion`, and `submissionId` fields described above. |
The receipt fields (`event`, `eventRequestId`, `operations`, `outcome`, `requestDigest`, `schemaVersion`, `submissionId`) are present both at the response root and under `receipt`. Read them from whichever location suits your client; both are authoritative. The root may also carry write-path timing fields; treat any key not listed above as diagnostic and do not branch on it.
Every entry in `results[]` (and in `receipt.operations[]`) carries `opIndex`, `name`, `operation`, and `status`; `warnings` and `resolvedName` may appear on any entry. The rest of the entry depends on `status`:
- `"applied"` or `"noop"` — may also carry `version` and `dataHash`.
- `"error"` — carries a non-empty `errors[]` array describing the per-operation failure, and no `version` or `dataHash`.
A `"noop"` entry is a successful outcome: the operation was valid and changed nothing.
:::note[older receipt shape]
A retry that replays a pre-v2 receipt returns `schemaVersion: "operation-event-receipt/v1"`, whose `operations[]` entries carry an optional `status` of `"success" | "noop" | "failed"` and a singular `error` instead of `errors[]`. Branch on `schemaVersion` before reading per-operation results.
:::
:::note[committer identity restriction]
`warmhub/users` Identity wrefs (e.g. `wh:warmhub/users/Identity/`) are only valid as the committer when the token belongs to the user that Identity represents. You cannot set `committer` to another user's Identity even if that Identity is visible in the public graph. Other existing things, such as `Agent` or `Agent/...`, are valid committer targets without this restriction.
:::
:::note[Binomial-opinion constraint]
When writing an assertion that carries a subjective-logic opinion `(b, d, u, α)`, the assertion must express a binary proposition (true/false). The opinion tuple is a binomial opinion and is meaningless on open-ended claims. See [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions).
:::
Component identity rules:
- User tokens may claim components installed by that user.
- Callers with [`org:configure`](/auth/personal-access-tokens/#available-permissions) for the org may claim any installed component in the org.
- Action tokens derive the component from the running subscription and reject mismatched explicit values.
**Operation variants:**
- ADD shape: `{ operation: "add", kind: "shape", name, data, skipExisting? }`
- ADD thing: `{ operation: "add", kind: "thing", name, data, skipExisting? }`
- ADD assertion: `{ operation: "add", kind: "assertion", name, about, data, affirmedTargets?, skipExisting? }`
- ADD collection: `{ operation: "add", kind: "collection", type, name, members, skipExisting? }`
- REVISE shape/thing/assertion: `{ operation: "revise", kind, name, data, expectedVersion?, leaseId? }` — for assertions, also accepts `affirmedTargets?`
- REVISE collection: `{ operation: "revise", kind: "collection", type, name, members, expectedVersion?, leaseId? }`
- RETRACT: `{ operation: "retract", name, reason?, kind?, expectedVersion?, leaseId? }` — withdraws the entity from default reads
- RENAME: `{ operation: "rename", name, newName, kind? }` — changes identity metadata without creating a new body version
- REAFFIRM assertion: `{ operation: "reaffirm", name, add?, remove?, leaseId?, expectedVersion? }` — mints a new assertion version that keeps the existing assertion content unchanged while updating which pinned target versions it affirms, via the `add?` and `remove?` delta fields. At least one of `add` or `remove` must be present; a reaffirm with neither is rejected. `kind` is optional.
Write path rejects `@ALL`. Create things with explicit names; dependent assertions and collections should reference those names directly.
`affirmedTargets` on assertion `add` and `revise` is an array of pinned target wrefs (`Shape/name@vN`) that records which specific versions of the about-target the assertion was evaluated against. The `reaffirm` operation updates the affirmed targets via `add?` and `remove?` delta fields while keeping the assertion content the same.
On `revise`, `affirmedTargets` is the **complete** new set, not a delta — and omitting it **clears** the assertion's affirmations rather than carrying them forward. This is deliberate: a revise changes the claim, and affirmations recorded against the old claim do not transfer to the new one. If a revise should keep its existing affirmations, restate them in `affirmedTargets` on that same operation. Use `reaffirm` when the affirmation set should change but the claim should not.
**Per-operation result warnings:**
Successful and noop result entries can include:
```json
{
"warnings": {
"undeclaredFields": ["status", "filePath"],
"undeclaredFieldsTruncated": true,
"totalUndeclared": 600,
"coalescedWrefs": [
{ "fieldPath": "owner", "wref": "User/nobody", "reason": "thing_absent" }
]
}
}
```
`warnings.undeclaredFields` lists top-level keys present in the submitted `data` but not declared in the target shape. When the list is capped, `undeclaredFieldsTruncated: true` is set and `totalUndeclared` reports the full count. `warnings.coalescedWrefs` lists optional wref fields whose resolver outcome was `thing_absent` and was coalesced to `null` — each entry carries `fieldPath`, `wref`, and `reason`; missing shapes and every other failure remain hard errors. When capped, `coalescedWrefsTruncated: true` is set with `totalCoalescedWrefs`. The struct is additive — an operation can carry either warning kind or both. Both warnings are informational; they do not turn the operation into a failure. See [Optional Wref Fields](/data-modeling/wrefs/#optional-wref-fields).
For add operations, `skipExisting: true` returns `noop` when the target already exists instead of failing. For revise and retract operations, `expectedVersion` applies the write only if the target is still at that version, otherwise it rejects with `CONFLICT`. A revise whose data matches the current version returns `noop` instead of creating a new version. See [Conditional Operations](/writes/operations/#conditional-operations) for the full model.
Unauthorized component claims reject with `FORBIDDEN`.
### warmhub_commit_validate
Evaluates the complete ordered operation batch with the same server evaluator as
`warmhub_commit_submit`, but creates no repository state or receipt and dispatches
no asynchronous action. The tool requires write authority even though its MCP
contract is read-only and non-destructive.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `operations` | array | yes | One complete ordered batch, at most 10,000 operations. |
| `message` | string | no | Message projected with the preview; not persisted. |
| `committer` | string | no | Optional untyped committer wref, evaluated under the same rules as submit. |
| `componentRef` | string | no | Optional installed component attribution. |
| `skipExisting` | boolean | no | Apply `skipExisting` to add operations only. |
| `includeWouldBeBody` | boolean | no | Include projected resulting bodies when disclosure rules permit. |
The response contains one ordered top-level result per input, optional nested
effects, aggregate counts, `canCommit`, persistence caveats, and an
authorization-safe baseline. Statuses are `would_apply`, `noop`, or `error`;
failed one-to-many lowering marks valid sibling effects `discarded`.
MCP's shared route limit is 1 MiB, smaller than the 4 MiB SDK/CLI validation
limit. The tool has no `submissionId`, `streamId`, chunking, or receipt arguments.
Use the CLI or an SDK for larger previews. A preview is a snapshot, so concurrent
repository changes or real-write admission can still make a later submit fail.
### warmhub_commit_receipt_get
Looks up one immutable write receipt by a caller-known `eventRequestId`. Any exact write that carries an `eventRequestId` produces a receipt this tool can read — `warmhub_repo_content_set`, `warmhub_collection_create`, and `warmhub_collection_revise` as well as `warmhub_commit_submit` — so it is not limited to one tool or one failure mode.
The common use is resolving an ambiguous write: call it after an ambiguous append failure, before retrying, to determine whether the submission landed server-side.
When `warmhub_commit_submit` returns an ambiguous append failure, the error includes an `eventRequestId`. Pass that error-provided value here. Do not pass your original `submissionId` — the two values differ.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `eventRequestId` | UUID | yes | The id of the write whose receipt you want. After an ambiguous `warmhub_commit_submit` this is the `eventRequestId` the error carries — not the `submissionId` you supplied, which is a different value. For the other write tools it is the `eventRequestId` you passed in. |
The tool rejects any argument outside this set, so do not pass `submissionId` alongside them.
Returns the same immutable write receipt documented under [`warmhub_commit_submit`](#warmhub_commit_submit) when a matching write exists — the receipt on its own, without the compatibility keys that tool adds alongside it. Missing receipts and receipts the caller is not authorized to read are intentionally indistinguishable — both return a not-found result rather than a distinct authorization error, so that the existence of a write cannot be inferred from the error shape.
## Shape Tools
| Tool | Description |
|------|-------------|
| `warmhub_shape_list` | List shapes in a repository. Each item includes per-shape `queryHints`. |
| `warmhub_shape_get` | Get a shape by name. Response includes `queryHints`. |
| `warmhub_shape_template` | Generate write-operation templates for one or more shapes. Read-only; `commit` category. See [Generating Templates](/writes/operations#generating-templates). |
### warmhub_shape_list
Lists shapes in the repo. `orgName` and `repoName` are required.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name. |
| `repoName` | string | yes | Repository name. |
| `match` | string | no | Glob pattern to filter shape names by bare name (e.g. `Sensor*`), not by full wref. |
| `componentRef` | string | no | Return only shapes owned by this component, identified by its `Org/Name` ref (e.g. `warmhub/veritas`). Mutually exclusive with `excludeComponents`. |
| `excludeComponents` | boolean | no | Exclude all component-owned shapes from results. Mutually exclusive with `componentRef` — passing both returns no shapes. |
Each item in the response includes a `queryHints` block (`queryableFields`, `wrefFields`, `suggestedPatterns`) to help you choose the right `warmhub_thing_query`, `warmhub_thing_search`, or `warmhub_thing_refs` pattern for that shape.
### warmhub_shape_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shapeName` | string | yes | Shape name |
Both `warmhub_shape_get` and `warmhub_shape_list` include a `queryHints` block (`queryableFields`, `wrefFields`, `suggestedPatterns`) to help agents choose `warmhub_thing_query`, `warmhub_thing_search`, and `warmhub_thing_refs` patterns for each shape.
### warmhub_shape_template
Generates write-operation templates for one or more shapes in the repo. Read-only tool in the `commit` category. For conceptual background see [Generating Templates](/writes/operations#generating-templates).
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shapeNames` | string | yes | Comma-separated shape names to generate templates for |
| `kind` | string | no | Filter templates to a specific kind. One of `thing` or `assertion`. |
| `operation` | string | no | Filter to a specific operation type. One of `add`, `revise`, or `retract`. |
| `about` | string | no | Populate the `about` field in generated assertion templates with this wref |
| `count` | integer | no | Number of example templates to generate per shape |
## Collection Tools
First-class tools for working with [named collections](/data-modeling/collections/) in a repo.
**Shape support at a glance:**
| Shape family | Writable (`create`, `revise`) | Readable (`members`, `contains`, `diff`, `stats`) |
|---|---|---|
| Arc | ✓ | ✓ |
| Bond | ✓ | ✓ |
| Set | ✓ | ✓ |
| List | ✓ | ✓ |
| Pair | ✓ (deprecated — accepted with a deprecation warning) | ✓ |
| Triple | — | ✓ (legacy, read-only) |
Use `create` to make a collection and `revise` to change its membership; `members`, `stats`, and `contains` to read one; and `diff` to compare two collections or two versions of the same collection.
| Tool | Description |
|------|-------------|
| `warmhub_collection_create` | Create a new named collection in the repo. Requires `repo:write`. |
| `warmhub_collection_members` | List the current members of a named collection. |
| `warmhub_collection_contains` | Check whether a named collection contains one or more given wrefs. |
| `warmhub_collection_diff` | Compute the diff between two versions of a named collection. |
| `warmhub_collection_revise` | Revise the membership of an existing named collection. Requires `repo:write`. |
| `warmhub_collection_stats` | Return summary statistics for a named collection (`type`, `wref`, `version`, `memberCount`, and `uniqueMemberCount`). |
### Selector-backed collection rules
`warmhub_collection_create` and `warmhub_collection_revise` both accept selector fields — `shape`, `about`, `match`, `kind`, `excludeComponents`, `componentRef`, `where`, `sourceOrgName`, `sourceRepoName` — which populate membership from a live query instead of an explicit member list. These rules apply to both:
- **Set-only.** Any selector field requires the collection to be a `set`; every other type is rejected, including the deprecated `pair`, whether the selector reads this repo or a foreign one.
- **Needs an anchor.** At least one of `shape`, `about`, `match`, `componentRef`, or `where` is required when any selector field is used. `kind` and `excludeComponents` only narrow an already-anchored selector.
- **Foreign source repos.** `sourceOrgName` and `sourceRepoName` must be paired with each other and require an anchor selector. They cannot be combined with any explicit member argument — use canonical `wh:org/repo/...` member wrefs instead, or omit the source repo.
- **Otherwise selectors and `members` compose** on create: the result set is seeded from `members` first, then selector query matches are added. A foreign source repo is the exception, per the rule above — and on revise, `add`/`remove` are a further exception, below.
Each tool then adds one rule of its own, below.
### warmhub_collection_create
Selector fields follow the [shared rules above](#selector-backed-collection-rules), where create's explicit member arguments are `members`, `add`, `remove`, and `replaceMembers`. Create has one rule of its own:
- **`add`, `remove`, and `replaceMembers` require `from`.** They adjust a collection copied by `from`, so without it the call is rejected. `from` is in turn exclusive with `members`, with selector fields, and with `sourceOrgName`/`sourceRepoName` — so a create either copies an existing collection and adjusts it, or builds a fresh set from `members` and/or a selector, never both.
- **`add`/`remove` only work on a `set`.** Delta updates are rejected for `arc`, `bond`, `list`, and the deprecated `pair` — copying one of those and changing its membership means passing the full list via `replaceMembers`, which is the non-`set` escape hatch and is not a delta.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `eventRequestId` | UUID | yes | Caller-known idempotency key for this write request. Reuse only for an identical retry. |
| `type` | string | yes | Collection type: `"arc"`, `"bond"`, `"set"`, `"list"`, or `"pair"` (deprecated — accepted with a deprecation warning; use `"arc"`, `"bond"`, `"set"`, or `"list"` for new collections) |
| `name` | string | yes | Collection name (local wref segment) |
| `members` | string[] | no | Initial member wrefs. May be combined with selector fields — the result set is seeded from these wrefs before selector query matches are added. |
| `from` | string | no | Source wref to copy members from. Exclusive with `members`, selector fields, and `sourceOrgName`/`sourceRepoName`. |
| `add` | string[] | no | Wrefs to add to the copied member set. Requires `from`. |
| `remove` | string[] | no | Wrefs to remove from the copied member set. Requires `from`. |
| `replaceMembers` | string[] | no | Replace the copied member list with these wrefs. Requires `from`. |
| `shape` | string | no | Selector: filter by shape. Acts as an anchor selector. |
| `about` | string | no | Selector: filter by about target. Acts as an anchor selector. |
| `match` | string | no | Selector: glob pattern to filter wrefs. Acts as an anchor selector. |
| `kind` | string | no | Selector: filter by kind. One of `shape`, `thing`, `assertion`, or `collection`. Requires at least one anchor selector to be present. |
| `excludeComponents` | boolean | no | Selector: exclude component-owned items from the selector result. Requires at least one anchor selector to be present. |
| `componentRef` | string | no | Selector: filter to items owned by the given component. Acts as an anchor selector. |
| `sourceOrgName` | string | no | Selector: org name of a foreign repo to read selector results from. Must be paired with `sourceRepoName`. Only valid for selector-backed set collections. Cannot be combined with any explicit member argument — `members`, `add`, `remove`, or `replaceMembers`. |
| `sourceRepoName` | string | no | Selector: repo name of a foreign repo to read selector results from. Must be paired with `sourceOrgName`. Only valid for selector-backed set collections. Cannot be combined with any explicit member argument — `members`, `add`, `remove`, or `replaceMembers`. |
| `where` | object[] | no | Selector: typed field-value predicates. Acts as an anchor selector. |
| `message` | string | no | Message recorded with the write |
| `committer` | string | no | Wref identifying the actor on whose behalf the write is made |
| `skipExisting` | boolean | no | Return `noop` instead of failing when the collection already exists |
### warmhub_collection_members
`orgName` and `repoName` may be omitted when `wref` is a self-routing durable ID — the durable ID routes itself to the repo that owns the collection. `cursor` must be paired with `limit`.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | no | Organization name. Required unless `wref` is a durable ID. |
| `repoName` | string | no | Repository name. Required unless `wref` is a durable ID. |
| `wref` | string | yes | Collection wref |
| `version` | integer | no | Pin the lookup to a specific collection version |
| `limit` | integer | no | Max members to return. Required when `cursor` is provided. |
| `cursor` | string | no | Pagination cursor from previous response. **Must be paired with an explicit `limit`.** |
### warmhub_collection_contains
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `wref` | string | yes | Collection wref |
| `members` | string[] | yes | Wrefs to check for membership |
| `position` | integer | no | Check membership at a specific position. Valid for ordered collection types (Arc, Pair, List, and legacy Triple); rejected for unordered Set and Bond collections. |
| `version` | integer | no | Check membership at a specific collection version |
### warmhub_collection_diff
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `leftWref` | string | yes | Left-hand collection wref |
| `rightWref` | string | yes | Right-hand collection wref |
| `leftVersion` | integer | no | Version to use for the left-hand collection. Defaults to HEAD. |
| `rightVersion` | integer | no | Version to use for the right-hand collection. Defaults to HEAD. |
| `mode` | string | no | Comparison mode: `"auto"` (default), `"membership"`, or `"ordered"` |
### warmhub_collection_revise
Selector fields follow the [shared rules above](#selector-backed-collection-rules), where revise's explicit member arguments are `members`, `add`, and `remove`. Revise takes no `from`, and has one rule of its own:
- **`add`/`remove` are exclusive with replacement membership.** Passing either alongside `members` or any selector field is rejected. So a revise is either a *delta* — `add` and/or `remove` against current membership — or a *replacement* built from `members` and/or a selector, never both.
- **Deltas only work on a `set`.** `add`/`remove` are rejected for `arc`, `bond`, `list`, and the deprecated `pair`; revise those by passing the full `members` list.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `eventRequestId` | UUID | yes | Caller-known idempotency key for this write request. Reuse only for an identical retry. |
| `wref` | string | yes | Collection wref |
| `members` | string[] | no | Replace the full member list with these wrefs. May be combined with selector fields — the result set is seeded from these wrefs before selector query matches are added. |
| `add` | string[] | no | Wrefs to add to the collection |
| `remove` | string[] | no | Wrefs to remove from the collection |
| `shape` | string | no | Selector: filter by shape. Acts as an anchor selector. |
| `about` | string | no | Selector: filter by about target. Acts as an anchor selector. |
| `match` | string | no | Selector: glob pattern to filter wrefs. Acts as an anchor selector. |
| `kind` | string | no | Selector: filter by kind. One of `shape`, `thing`, `assertion`, or `collection`. Requires at least one anchor selector to be present. |
| `excludeComponents` | boolean | no | Selector: exclude component-owned items from the selector result. Requires at least one anchor selector to be present. |
| `componentRef` | string | no | Selector: filter to items owned by the given component. Acts as an anchor selector. |
| `sourceOrgName` | string | no | Selector: org name of a foreign repo to read selector results from. Must be paired with `sourceRepoName`. Only valid for selector-backed set collections. Cannot be combined with any explicit member argument — `members`, `add`, or `remove`. |
| `sourceRepoName` | string | no | Selector: repo name of a foreign repo to read selector results from. Must be paired with `sourceOrgName`. Only valid for selector-backed set collections. Cannot be combined with any explicit member argument — `members`, `add`, or `remove`. |
| `where` | object[] | no | Selector: typed field-value predicates. Acts as an anchor selector. |
| `message` | string | no | Message recorded with the write |
| `committer` | string | no | Wref identifying the actor on whose behalf the write is made |
### warmhub_collection_stats
`orgName` and `repoName` may be omitted when `wref` is a self-routing durable ID.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | no | Organization name. Required unless `wref` is a durable ID. |
| `repoName` | string | no | Repository name. Required unless `wref` is a durable ID. |
| `wref` | string | yes | Collection wref |
| `version` | integer | no | Pin the lookup to a specific collection version |
Returns the following fields:
| Response Field | Type | Description |
|----------------|------|-------------|
| `type` | string | Collection shape family. One of `"set"`, `"list"`, `"arc"`, `"bond"`, `"pair"` (deprecated — writable but accepted with a deprecation warning), or `"triple"` (legacy, read-only). |
| `wref` | string | The returned collection wref, always in local form (e.g. `Set/my-set`) — it names the collection within the repo you queried and is never expanded to the cross-repo `wh:org/repo/...` form, even when you addressed the collection by a canonical wref. |
| `version` | integer | Version number of the returned collection snapshot. When no `version` parameter is supplied this is the HEAD version; when a `version` parameter is supplied this reflects that pinned version. |
| `memberCount` | integer | Total number of member entries, including duplicates for ordered collection types. |
| `uniqueMemberCount` | integer | Number of distinct member wrefs in the collection. |
## Subscription Tools
See [Subscriptions](/subscriptions/overview/) for concepts and [Creating Subscriptions](/subscriptions/creating/) for setup guides with filter and credential examples.
Every subscription tool addresses either scope. Omitting `scope` (or passing `scope: "repo"`) addresses a repository subscription and requires `repoName`. Passing `scope: "org"` addresses an organization subscription: supply `orgName` only, and omit `repoName`, `shapeName`, `filterJson`, and `sourceRepoRef`. A missing `repoName` is an error rather than a silent switch to org scope, so a lifecycle call can never retarget the org subscription that happens to share a name.
| Tool | Description |
|------|-------------|
| `warmhub_subscription_list` | List non-retired subscriptions. Retired subscriptions remain readable by name via `warmhub_subscription_get` but do not appear in list results — see [Managing Subscriptions](/subscriptions/managing/) for what retirement means and when it happens. |
| `warmhub_subscription_get` | Get subscription metadata by name. |
| `warmhub_subscription_create` | Create a webhook subscription. |
| `warmhub_subscription_update` | Update an existing subscription's trigger or webhook config. |
| `warmhub_subscription_pause` | Pause an active subscription. |
| `warmhub_subscription_resume` | Resume a paused subscription. |
| `warmhub_subscription_delete` | Delete a subscription. |
### warmhub_subscription_list
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | no | Repository name. Required when `scope` is `"repo"` (the default); omit when `scope` is `"org"`. |
| `scope` | string | no | `"repo"` (default) or `"org"` |
### warmhub_subscription_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | no | Repository name. Required when `scope` is `"repo"` (the default); omit when `scope` is `"org"`. |
| `name` | string | yes | Subscription name |
| `scope` | string | no | `"repo"` (default) or `"org"` |
### warmhub_subscription_create
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | no | Repository name. Required when `scope` is `"repo"` (the default); omit when `scope` is `"org"`. |
| `name` | string | yes | Subscription name |
| `scope` | string | no | `"repo"` (default) or `"org"`. Under `"org"` the event catalog is the org one below and `repoName`, `shapeName`, `filterJson`, and `sourceRepoRef` are rejected |
| `kind` | string | yes | `"webhook"` or `"cron"`. `"webhook"` is the supported value for new subscriptions; `"cron"` is accepted by the schema only so older callers receive a deterministic rejection. |
| `eventType` | string | no | Event type to subscribe to. Under repo scope, one of `commit`, `repo.renamed`, `thing.renamed`, or `shape.renamed`, defaulting to `commit`. Under `scope: "org"`, one of `org.renamed`, `org.member_added`, `org.repo_created`, or `org.repo_published`, defaulting to `org.renamed`. |
| `shapeName` | string | no | Shape to subscribe to. For `commit` subscriptions, provide either `shapeName` or `filterJson.shape` — except for [shape lifecycle subscriptions](/subscriptions/filter-json/#shape-lifecycle-subscriptions), which omit both and rely on a `{"kind":"shape", ...}` filter. Not applicable for `repo.renamed`, `thing.renamed`, or `shape.renamed` subscriptions. |
| `filterJson` | object | no | Recursive [`subfilter/v1` commit-operation filter](/subscriptions/filter-json/) with `operation`, `kind`, `shape`, `name`, `match`, `all`, `any`, and `not`. Required for `commit`; rejected for metadata rename events. |
| `webhookUrl` | string | yes | Webhook endpoint URL |
| `fallbackWebhookUrl` | string | no | Optional fallback endpoint called after a terminal delivery failure |
| `allowTraceReentry` | boolean | no | Reentry policy for write-triggered subscriptions. Defaults to `false` |
| `sourceRepoRef` | string | no | Source repo (`org/repo`) for a [cross-repo subscription](/subscriptions/creating/#cross-repo-subscriptions). Must be in the same org as the home repo |
| `componentRef` | string | no | Attribute this subscription to an installed component, identified by its `Org/Name` ref (e.g. `warmhub/veritas`). See [Component Tools](#component-tools) for the component ref format. |
| `notifyOnSuccess` | boolean | no | Deprecated compatibility field; accepted but ignored. Action notifications are terminal-failure-only |
For a commit subscription, omitted `eventType` defaults to `commit`, so this is a complete filter example:
```json
{
"orgName": "acme",
"repoName": "sensors",
"name": "new-sensors",
"kind": "webhook",
"shapeName": "Sensor",
"filterJson": {
"all": [
{ "operation": "add" },
{ "kind": "thing" }
]
},
"webhookUrl": "https://hooks.example.com/warmhub"
}
```
When both `shapeName` and `filterJson.shape` are present, they must identify the same shape. A structurally shape-only filter such as `{"kind":"shape"}` is the only commit form that needs neither binding. Metadata rename events reject `shapeName`, `filterJson`, and `sourceRepoRef`.
#### Creating an org-scoped subscription
`scope: "org"` creates organization-owned automation. The caller must be an
organization owner or admin. `repoName`, `shapeName`, `filterJson`, and
`sourceRepoRef` are rejected, and an omitted `eventType` means `org.renamed`.
```json
{
"orgName": "acme",
"scope": "org",
"name": "org-rename-hook",
"kind": "webhook",
"eventType": "org.member_added",
"webhookUrl": "https://hooks.example.com/warmhub"
}
```
### warmhub_subscription_update
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | no | Repository name. Required when `scope` is `"repo"` (the default); omit when `scope` is `"org"`. |
| `name` | string | yes | Existing subscription name |
| `scope` | string | no | `"repo"` (default) or `"org"`. `shapeName` and `filterJson` are rejected under `"org"` |
| `shapeName` | string | no | Replacement commit-subscription shape. Rejected for metadata-event and cross-repo subscriptions. |
| `filterJson` | object | no | Replacement recursive [`subfilter/v1` filter](/subscriptions/filter-json/). Rejected for metadata-event and cross-repo subscriptions. |
| `webhookUrl` | string | no | Replacement webhook URL |
| `fallbackWebhookUrl` | string or null | no | Replacement fallback webhook URL. Use `null` to clear it |
| `allowTraceReentry` | boolean | no | Replacement reentry policy for write-triggered subscriptions |
| `notifyOnSuccess` | boolean | no | Deprecated compatibility field; accepted but ignored. Action notifications are terminal-failure-only |
Updates patch only the fields supplied; omitted fields retain their current values. `name`, `kind`, and `eventType` cannot be changed. For commit subscriptions, the merged `shapeName` and `filterJson.shape` constraints must still agree and retain a valid shape binding (or a structurally shape-only filter).
### warmhub_subscription_pause / warmhub_subscription_resume / warmhub_subscription_delete
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | no | Repository name. Required when `scope` is `"repo"` (the default); omit when `scope` is `"org"`. |
| `name` | string | yes | Subscription name |
| `scope` | string | no | `"repo"` (default) or `"org"` |
## Action Tools
| Tool | Description |
|------|-------------|
| `warmhub_subscription_log` | Get delivery feed for a subscription. Narrowed readers receive operational metadata and visible matched operations; full-repository readers also receive free-form diagnostics. |
| `warmhub_subscription_runs` | List a repository's subscription delivery runs. For application-level round or session containers, use `warmhub_thing_query` with a shape filter. |
| `warmhub_subscription_attempts` | Get attempt history for one delivery run of a subscription. Free-form diagnostics require full-repository read. |
| `warmhub_notifications` | List terminal action-failure notifications for a repo. Only runs that have reached a non-recoverable failure state appear in this feed. |
### warmhub_subscription_log
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `subscriptionName` | string | yes | Subscription name |
| `limit` | integer | no | Max items, 1–500. Required when `cursor` is provided |
| `cursor` | string | no | Pagination cursor from previous response. **Must be paired with an explicit `limit`** — supplying `cursor` alone is rejected with `"cursor" requires "limit"` |
Each item in the response includes delivery fields plus optional run diagnostics:
| Response Field | Type | Description |
|----------------|------|-------------|
| `deliveryId` | string | Required identifier for this delivery. Use this value when waiting on lease-based deliveries. |
| `runId` | string? | Identifier for the action run associated with this delivery. May be absent when the delivery has not yet been associated with a run. |
| `runStatus` | string? | Run outcome: `succeeded`, `failed_terminal`, `dead_letter`, etc. |
| `attemptCount` | number? | Current attempt number |
| `maxAttempts` | number? | Maximum attempts allowed |
| `lastErrorCode` | string? | Error classification code (for example `HTTP_502` or `WEBHOOK_TARGET_REJECTED`) |
| `lastErrorMessage` | string? | Human-readable error description |
### warmhub_subscription_runs
Lists action runs in a repository. Accepts optional filters to narrow results by status, outcome, time range, or exact run or subscription identity.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `runId` | string | no | Return the single run with this identifier. Can be combined with `subscriptionName`, `status`, `outcome`, `since`, and `cursor`. |
| `subscriptionName` | string | no | Filter runs to those triggered by this subscription. |
| `status` | string | no | Filter by status: `pending`, `running`, `processing`, `retry_wait`, `suppressed`, `succeeded`, `failed_terminal`, `dead_letter` |
| `outcome` | string | no | Filter by terminal outcome. Accepted values: `succeeded` or `failed`. `failed` matches runs in a terminal failure state (`failed_terminal` or `dead_letter`); it does not match every non-success state. Cannot be combined with `status`. |
| `since` | string | no | ISO datetime or unix timestamp. Cannot be combined with `cursor`. |
| `limit` | integer | no | Max results. Must be between `1` and `200`. |
| `cursor` | string | no | Pagination cursor from previous response. Cannot be combined with `since`. When present, the response includes `nextCursor` when more results are available. |
The combinations `status`+`outcome` and `since`+`cursor` are not accepted and will be rejected.
### warmhub_subscription_attempts
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `runId` | string | yes | Action run identifier (UUIDv7) for the target run. |
### warmhub_notifications
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `since` | string | no | ISO datetime or unix timestamp |
| `limit` | integer | no | Max results (1–200) |
Returns terminal action-failure notifications for the repo — only runs that have reached a non-recoverable failure state produce records in this feed. See [Actions](/http-api/actions/) for the full HTTP contract, including the `GET /api/repos/:orgName/:repoName/actions/notifications` endpoint.
## Component Tools
| Tool | Description |
|------|-------------|
| `warmhub_component_install` | Install a registered component (`/`) into the repo: applies its manifest (shapes, credential sets, subscriptions, seeds) and runs the optional setup handshake. Returns the install state plus any setup-deferred resources. Requires `repo:write`. Components whose manifests declare credentials or subscriptions require additional permissions — see the note below. |
| `warmhub_component_uninstall` | Uninstall a registered component (the `wh component teardown` operation): pauses its subscriptions, revokes its tokens, dispatches the optional uninstall callback, and marks the install record `uninstalled`. Frees the shape names the component claimed so a reinstall can reclaim them, while leaving the existing shapes and any seeded data in place. Non-destructive — reinstall revives the install. Requires `repo:write`. |
### warmhub_component_install / warmhub_component_uninstall
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes (global) | Organization name |
| `repoName` | string | yes (global) | Repository name |
| `componentRef` | string | yes | The registered component to install or uninstall, as `/` (e.g. `warmhub/veritas`). |
| `submissionId` | UUID | yes | Caller-owned root idempotency key for this install or uninstall request; every install phase derives its own event-request id from it. Casing is normalized, so a mixed-case UUID is accepted. Reuse it only for an identical retry — and note that reuse is not a blanket replay: when a setup dispatch from the first attempt may already have reached the component's receiver, the retry is refused rather than dispatched a second time. |
:::note[Additional permissions for manifest-driven installs]
`repo:write` is always required for `warmhub_component_install`. Components whose manifests declare credential sets or subscriptions require the token to also carry the corresponding repo-level permissions for those resources. If the caller is missing any manifest-required permissions, the install is rejected with `Cannot install: caller is missing repo permissions required by the manifest: ...`. Check the component's published manifest to determine which permissions apply before attempting an install.
:::
---
# Skills
> Overview of the WarmHub skills published in warmhub/warmhub-skills — what each one does and when to reach for it.
[`warmhub/warmhub-skills`](https://github.com/warmhub/warmhub-skills) is a set of skills for Claude Code, Codex, and other AI coding agents. Each skill packages the WarmHub knowledge an agent needs to do one job well — designing a repo, planning an ingest, building a collector, wiring an app to WarmHub data — so the agent loads that context only when the task calls for it.
This page is the at-a-glance map of what's published. For full install and update detail, see the [skills repo README](https://github.com/warmhub/warmhub-skills#readme).
The skills assume familiarity with WarmHub's [core concepts](/get-started/core-concepts/) — repos, shapes, things, and assertions.
## Install
Run from the project directory where you want the skills installed:
```bash
# Browse what's available
npx skills add warmhub/warmhub-skills --list
# Install everything
npx skills add warmhub/warmhub-skills --all
# Install one skill
npx skills add warmhub/warmhub-skills --skill build-warmhub-repo
# Target a specific agent (e.g. Codex)
npx skills add warmhub/warmhub-skills --all -a codex
```
To pull updates later, run `npx skills update` (aliases: `check`, `upgrade`) — it reports which of your installed skills differ from the published versions. See the repo's [`CHANGELOG.md`](https://github.com/warmhub/warmhub-skills/blob/main/CHANGELOG.md) for what changed. There are no version numbers to pin: skills are versioned by content hash, not semver, so a skill is "out of date" exactly when its content differs from the published copy.
## Available skills
The skills are grouped by goal. If you're not sure where to start, begin with `warmhub-builder` — it routes you to the right skill for your stage.
### Start here
| Skill | What it does | Use it when |
|-------|--------------|-------------|
| [`warmhub-builder`](https://github.com/warmhub/warmhub-skills/tree/main/skills/warmhub-builder) | Coordinates a WarmHub project from idea intake to the right builder stage. | Starting a WarmHub project and unsure which builder skill comes next. |
### Design and modeling
| Skill | What it does | Use it when |
|-------|--------------|-------------|
| [`modeling-foundations`](https://github.com/warmhub/warmhub-skills/tree/main/skills/modeling-foundations) | Applies WarmHub repo-modeling foundations — things vs. assertions, about-cardinality, and the four-direction traversability test. | Grounding the data model before any design, build, or review work. |
| [`design-warmhub-repo`](https://github.com/warmhub/warmhub-skills/tree/main/skills/design-warmhub-repo) | Designs or reviews a repo's shapes, assertions, source identity, certainty, and derivation policy. | Creating a new repo, modeling something as shapes, or judging whether a graph is ready to ship. |
| [`discover-warmhub-repo`](https://github.com/warmhub/warmhub-skills/tree/main/skills/discover-warmhub-repo) | Reads and summarizes an existing repo — shape inventory, counts, commit cadence, sensitivity signals — without mutating it. | Inspecting an existing repo for a fact summary before building against it. |
### Ingestion
| Skill | What it does | Use it when |
|-------|--------------|-------------|
| [`plan-warmhub-ingestion`](https://github.com/warmhub/warmhub-skills/tree/main/skills/plan-warmhub-ingestion) | Turns a design summary or source into an ingestion plan covering access, transforms, idempotency, backfill, quality checks (QC), and write boundaries. | Planning how a data source becomes WarmHub data, before writing the build. |
| [`build-warmhub-repo`](https://github.com/warmhub/warmhub-skills/tree/main/skills/build-warmhub-repo) | Builds a complete ingestion repo — Bun/TS scaffold, shapes, source fetches, WarmHub operations, quality checks, auth, and a verified first ingest. | Implementing an approved ingestion plan or repo design. |
| [`wh-commit-design`](https://github.com/warmhub/warmhub-skills/tree/main/skills/wh-commit-design) | Designs commit plans for large or non-trivial ingests — chunking, sizing, resumable multi-commit workflows. | Batching a large dataset into safe `wh commit` runs. |
### Apps and surfaces
| Skill | What it does | Use it when |
|-------|--------------|-------------|
| [`build-warmhub-collector`](https://github.com/warmhub/warmhub-skills/tree/main/skills/build-warmhub-collector) | Builds a mobile-first data collection app that submits end-user observations as things and assertions. | Adding a write/ingress surface with forms, media, offline queueing, and provenance. |
| [`build-warmhub-display`](https://github.com/warmhub/warmhub-skills/tree/main/skills/build-warmhub-display) | Builds a read-only display, report, notebook, or analysis surface for WarmHub data. | Building an egress surface — dashboard, report, or notebook — over a repo. |
| [`connect-warmhub-app`](https://github.com/warmhub/warmhub-skills/tree/main/skills/connect-warmhub-app) | Wires a TypeScript app or notebook to WarmHub — SDK setup, env config, server-side token handling, a one-fact connection probe. | Connecting a display, collector, or app to WarmHub data safely. |
### Components and specialized
| Skill | What it does | Use it when |
|-------|--------------|-------------|
| [`add-warmhub-component`](https://github.com/warmhub/warmhub-skills/tree/main/skills/add-warmhub-component) | Adds or installs a WarmHub component — manifest design, subscription wiring, credential sets, install/doctor/update/teardown. | Componentizing a repo or installing a published component such as Veritas. |
| [`veritas-design`](https://github.com/warmhub/warmhub-skills/tree/main/skills/veritas-design) | Designs how a repo uses [Veritas](/veritas/overview/) — which propositions get certainty opinions, who asserts, how to seed reputations, how to consume consensus. | Wiring multiple sources to express belief-disbelief-uncertainty about the same binary propositions. |
| [`find-self-verifying-objectives`](https://github.com/warmhub/warmhub-skills/tree/main/skills/find-self-verifying-objectives) | Finds adjacent, simpler optimization targets measurable from collected data without external ground truth. | Optimizing any iterative or measurement-driven system without labels — applies beyond WarmHub repos. |
---
# Agent Context (wh prime)
> Context bootstrapping for AI agents — how to run it, its flags, output format, and token budget.
`wh prime` prints a compact CLI context dump built for AI agents. It's meant to be the first command an agent runs at the start of a session, so the agent knows WarmHub's concepts, wref syntax, and command surface before it does any work.
## When to Use
- **Session start** — give the agent full CLI context before it begins work.
- **After compaction** — restore context that was dropped when the conversation history was compressed.
- **New agent** — bootstrap a fresh agent with everything it needs to operate.
## How Agents Run It
`wh prime` is a normal CLI command, so any harness with shell access can run it. The goal is to feed its output into the agent's context once, early.
### Claude Code
Claude Code can run `wh prime` automatically with a [`SessionStart` hook](https://docs.claude.com/en/docs/claude-code/hooks), so every new session begins with WarmHub context loaded. Pair it with a `PreCompact` hook to re-run `wh prime` and restore context after the conversation is compacted:
```json
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "wh prime" }] }],
"PreCompact": [{ "hooks": [{ "type": "command", "command": "wh prime" }] }]
}
}
```
### Other harnesses
Any agent harness with shell access can run `wh prime` and include the output in its system prompt or first message. Two common patterns:
- **System prompt** — run `wh prime` once at startup and prepend the output to the agent's system prompt.
- **First turn** — let the agent call `wh prime` as its first tool use, then continue with the result in context.
Use `--json` (below) when your harness ingests structured context rather than prose.
### No shell? Use the MCP or SDK surface
Agents that can't run a shell don't use `wh prime` directly. The MCP equivalent is `warmhub_capabilities`, which returns the tool catalog, usage patterns, wref syntax, workflow guidance, and a pointer to the write contract. Call `warmhub_repo_describe` for the full contract and shape-specific examples.
SDK callers don't get a prose context dump. `client.diagnostics.capabilities()` returns compatibility information — the backend API version, minimum supported SDK, minimum supported clients, write contract version, and feature flags — not orientation prose. SDK agents read these docs instead.
See the [MCP Tools Reference](/agent-integration/mcp-tools-reference/#warmhub_capabilities) for the MCP bootstrapping tool.
## Usage
```bash
# Markdown output (human-readable)
wh prime
# Structured JSON output
wh prime --json
```
### Flags
`wh prime` takes no command-specific flags. It accepts the standard global flags, of which two affect the output:
| Flag | Effect on `wh prime` |
|------|----------------------|
| `--json` | Emit structured JSON instead of Markdown (alias for `--format json`). |
| `--format pretty\|json\|jsonl` | Choose the output format. |
`wh prime` emits the same CLI bootstrap context regardless of `--repo` or `--profile` — it describes the CLI itself, not a specific repo's data. The Environment section reflects your configured default repo (from `WARMHUB_REPO` or `wh use org/repo`), and shows `No default repo` when none is set.
## Markdown Output
The default output includes:
- **Environment** — your default repo, if one is configured.
- **Core concepts** — thing, assertion, shape, write, and wref definitions.
- **Versioned wrefs** — current and pinned thing targets, including retraction and rename behavior.
- **Wref quick reference** — local and canonical forms, version modifiers, and explicit naming guidance.
- **Key workflows** — read, write, and query command patterns.
Example excerpt:
```markdown
## Core Concepts
- **Thing**: A named entity versioned by writes. **Assertion**: A thing that makes a shape-validated claim about another thing.
- **Shape**: A thing defining data structure; every other thing has one. **Write**: One or more add/revise/retract operations with per-operation results.
- **wref**: A reference to a thing. Local: `Player` (the shape) or `Player/alice` (a thing with that shape). Cross-repo: `wh:org/repo/Shape` or `wh:org/repo/Shape/name`.
## Versioned Wrefs
- `Shape` addresses the shape itself; `Shape/name` addresses a thing with that shape. `@vN` pins either.
- Floating write refs to retracted targets fail; existing pinned versions remain valid.
- Rename invalidates old spellings, including `@vN`; the new name resolves history.
- Untyped wrefs accept any thing. `wref` requires the target's shape to be `T`; a shape has no shape, so never satisfies it. `wref?` coalesces only `thing_absent`, never a missing shape.
```
## JSON Output
`wh prime --json` returns the same context as a structured object — each domain lists its verbs with args, flags, aliases, and status, so a harness can parse the command surface directly:
```json
{
"version": "0.2.0",
"defaultOrg": null,
"defaultRepo": null,
"wrefSyntax": {
"localExamples": ["GameState", "GameState@v3", "Player/alice", "GameState/round-1/state"],
"canonicalFormat": "wh:org/repo/Shape or wh:org/repo/Shape/name",
"versionModifiers": ["@HEAD", "@vN", "@ALL"]
},
"domains": [
{
"domain": "auth",
"summary": "Authentication management",
"verbs": [
{ "name": "login", "summary": "Log in via browser or --with-token for PATs", "args": "", "flags": ["--with-token", "--profile"], "status": "live" }
]
}
]
}
```
## Token Budget
The JSON output is approximately **4,800 tokens** (about 19 KB); the Markdown output is a bit smaller, around **3,500 tokens** (about 14 KB). Either way it leaves most of an agent's context window free for reasoning and conversation. Measure the current size yourself with `wh prime --json | wc -c` and divide by ~4 for a rough token count before you budget for it.
## Best Practice
Agents should run `wh prime` (CLI) or `warmhub_capabilities` (MCP) as their first action, then use targeted commands and tools for specific work. Loading context once up front avoids guessing at concepts or syntax and gives the agent accurate command contracts from the start.
---
# Why Agent-Native?
> Why WarmHub is agent-native and how AI agents connect.
WarmHub is designed as an **agent-native knowledge platform**. AI agents can read and write structured knowledge through standard protocols, with built-in attribution, versioning, and context bootstrapping. Every agent builds on what previous agents discovered — knowledge compounds across your work, your team, and your community instead of resetting at the end of each conversation.
## Agent-Friendly Properties
**Multiple access methods.** WarmHub supports the [Model Context Protocol](/agent-integration/mcp-server/), a TypeScript SDK, and a CLI — so any MCP-compatible client (Claude, Cursor, custom agents), TypeScript application, or shell-based harness can connect using whichever fits your workflow. Each interface covers the core read/write surface; some operations are interface-specific (for example, live and low-level stream APIs are SDK-only, while workflow utilities such as `wh doctor` and `wh onboard` are CLI-only).
**Structured knowledge.** Shapes enforce schema on agent-written data. Agents can't write malformed records — the write path validates every operation.
**Attribution.** User-authenticated writes return an optional `createdByEmail` field in the write response as display metadata identifying who submitted the write. Writes may also include an optional `committer` wref (e.g. `Agent/claude`) identifying the actor on whose behalf the write was made. When multiple agents collaborate on the same repo, you can always trace who wrote what.
**Immutable history.** Agents can't silently overwrite data. Every change creates a new version, and old versions are preserved. You can always answer "what did agent X assert at time T?"
**Context bootstrapping.** Agents orient in two steps. First, `wh prime` (CLI) or `warmhub_capabilities` (MCP) return static bootstrap context — concepts, wref syntax, and the command/tool surface. Then `warmhub_repo_describe` returns a repo-specific dump of shapes, sample data, and the write contract tailored to the repo's actual schema. See the [bootstrap surface map](#agent-bootstrap-surface-map) for the CLI/MCP/SDK equivalents.
**Batch operations.** Agents can submit multiple operations in one request. Use explicit names so later operations in the same write can reference earlier things or collections directly.
## Agent Bootstrap Surface Map
Every surface has the same two bootstrap steps — get static orientation, then load the repo's schema. The tools line up across CLI, MCP, and SDK:
| Step | CLI | MCP | SDK |
|------|-----|-----|-----|
| **Static orientation** — concepts, wref syntax, the command/tool surface | `wh prime` | `warmhub_capabilities` | Read these docs; `client.diagnostics.capabilities()` returns backend capabilities including the API version, minimum supported SDK, minimum supported clients, write contract version, and feature flags — not orientation prose |
| **Repo schema** — shapes and stats for a specific repo (MCP also returns the full write contract and generated write examples) | [`wh repo describe`](/cli-reference/commands/) | `warmhub_repo_describe` | `client.repo.describe()` — returns repo metadata, shapes, stats, and a snapshot of the repo's current records; response structure differs from MCP (see note below) |
Only `warmhub_repo_describe` (MCP) also returns the full write contract and generated write examples; `wh repo describe` (CLI) covers schema, shapes, and stats. `client.repo.describe()` (SDK) returns the same repo metadata, shapes, and stats plus a snapshot of the repo's current records, but its response structure differs from the MCP tool and does not include the write contract or generated write examples — see the [SDK and MCP method map](/agent-integration/mcp-tools-reference/#sdk-and-mcp-method-map) for field-level detail.
## Integration Paths
### MCP Server
For AI agents that support the Model Context Protocol:
1. Configure the MCP endpoint in your client
2. Agent discovers the MCP tool catalog automatically
3. Start with `warmhub_capabilities` for orientation, then `warmhub_repo_describe` for repo-specific schema and write examples
4. Use `warmhub_commit_submit` for writes, `warmhub_thing_query` for reads
See [MCP Server](/agent-integration/mcp-server/) for setup and [MCP Tools Reference](/agent-integration/mcp-tools-reference/) for the full tool catalog.
### TypeScript SDK
For custom agents and applications built in TypeScript:
1. Install `@warmhub/sdk-ts` (`react` is only needed if you use the React provider)
2. Create a `WarmHubClient` with your deployment URL
3. Use typed surfaces: `client.thing.head(...)`, `client.commit.apply(...)`
4. Use `OperationBuilder` for multi-operation submissions with client-side validation
See [SDK Overview](/sdk/overview/) for setup and [Client Surfaces](/sdk/client/) for the full API.
### CLI with --json
For agents with shell access:
1. Install `wh` CLI
2. Set `WARMHUB_REPO` environment variable
3. Run `wh prime --json` for structured context
4. Use `wh` commands with `--json` for machine-readable output
```bash
export WARMHUB_REPO=myorg/myrepo
wh prime --json # bootstrap context
wh thing list --json # read current state
wh commit submit --ops '...' # write data
```
See [Agent Context (wh prime)](/agent-integration/wh-prime/) for how harnesses invoke `wh prime`, its flags, and its token budget.
## Agent Workflow Pattern
A typical agent session:
1. **Bootstrap** — call `warmhub_capabilities` (MCP) or `wh prime` (CLI) for static orientation, then `warmhub_repo_describe` to load the repo's schema, existing data, and write contract
2. **Read** — use `warmhub_thing_head` for broad orientation, `warmhub_thing_query` for targeted lookups
3. **Write** — commit observations and decisions via `warmhub_commit_submit` (opinion-bearing assertions must be [binary propositions](/data-modeling/patterns/#opinions-as-separate-assertions))
4. **Iterate** — read updated state, make new assertions, revise existing ones
The describe tool dynamically generates write examples based on the repo's current shapes, so agents get correct field names and types without guessing.
For guidance on what to model and how — when to use assertions vs things, shape design, naming conventions, and common patterns — see [Modeling Overview](/data-modeling/overview/). For the underlying data model, see [Core Concepts](/get-started/core-concepts/).
---
# Access reference
> What each org role can do, what each scope grants, and the minimum scope for common tasks.
WarmHub access composes from your **org role** and the **scopes** that narrow it. Your role sets the ceiling on what you can do; a [personal access token](/auth/personal-access-tokens/)'s scopes narrow a token to a subset of that ceiling; and an org admin can attach [member scope overrides](/sdk/client/#clientorg) that narrow a member below their role on a given org or repo. A request is allowed only when your role permits it **and** every scope layer that applies to it covers it. This page is the canonical map of role → capability, scope → capability, and task → minimum scope.
## Roles and capabilities
Every member of an org has one role: **viewer**, **editor**, **admin**, or **owner**. Roles are cumulative — each includes everything the one before it can do. This table answers "what can an admin do that an editor can't?":
| Capability | viewer | editor | admin | owner |
|------------|:------:|:------:|:-----:|:-----:|
| Read repositories, things, assertions, and shapes (`repo:read`) | ✓ | ✓ | ✓ | ✓ |
| Read repository checkpoint metadata and signed artifacts (`repo:checkpoint-read`, with unrestricted `repo:read`) | | | ✓ | ✓ |
| Generate or retry repository checkpoints (`repo:checkpoint-generate`, with unrestricted `repo:read` and `repo:checkpoint-read`) | | | ✓ | ✓ |
| Read the org profile, members, and installed components (`org:read`) | ✓ | ✓ | ✓ | ✓ |
| Write — create, update, and rename things and shapes (`repo:write`) | | ✓ | ✓ | ✓ |
| Configure repos — subscriptions, credentials, actions, notifications, repo settings (`repo:configure`) | | | ✓ | ✓ |
| Administer repos — delete, archive, change visibility (`repo:admin`) | | | ✓ | ✓ |
| Receive repo-level action callbacks (`repo:action-callback`) | | | ✓ | ✓ |
| Configure the org — create repos, manage members, org settings, install components, org-scoped subscriptions and credential bindings (`org:configure`) | | | ✓ | ✓ |
| Receive org-level action callbacks (`org:action-callback`) | | | ✓ | ✓ |
| Administer the org — rename, archive (`org:admin`) | | | | ✓ |
Owner and admin grant the **same repository access**; they differ only at the org level — **only an owner can rename or archive the org**. Member management has one further owner-only carve-out within `org:configure`: only an owner can assign or remove the **owner** role, and the last owner can't be removed or demoted. Admins manage all other members and roles.
## Scopes and what they grant
A token scope binds a [resource](/auth/personal-access-tokens/#scopes) to one or more of these permissions. Scopes are independent — `repo:write` does not include `repo:read` — and can only narrow access, never raise it above your role.
| Scope | Grants |
|-------|--------|
| `repo:read` | Read repositories, queries, things, and shapes |
| `repo:checkpoint-read` | Read retained repository checkpoint metadata and obtain signed artifact URLs; requires unrestricted `repo:read` too |
| `repo:checkpoint-generate` | Generate and retry repository checkpoints; requires unrestricted `repo:read` and `repo:checkpoint-read` too. `repo:admin` remains compatible for this capability only. |
| `repo:write` | Writes, shape mutations, and thing/shape renames |
| `repo:configure` | Repo-scoped subscriptions, credentials, action runs, notifications, and repo settings |
| `repo:admin` | Delete, archive, and change repository visibility |
| `repo:action-callback` | Receive repo-level action callbacks — the delivery of [action](/subscriptions/delivery-lifecycle/) results and status updates addressed to a specific repo; held by repo owners and admins |
| `org:read` | Read the org profile and members, see installed components, and list the org's repositories — including private ones — in org-level views; reading a repo's contents still needs `repo:read` |
| `org:configure` | Create repos, manage members and org settings, install and manage [components](/sdk/component-identity/), and manage org-scoped subscriptions and credential bindings |
| `org:action-callback` | Receive org-level action callbacks — the delivery of [action](/subscriptions/delivery-lifecycle/) results and status updates addressed to the org rather than to a specific repo; held by org owners and admins |
| `org:admin` | Rename and archive the org |
## Minimum scope by task
The minimum scope a token needs for each common task. Anything not listed for a scope is not covered by it — request the narrowest scope that covers your task.
| Task | Minimum scope |
|------|---------------|
| Read things, assertions, shapes, or write history | `repo:read` |
| Read checkpoint metadata or obtain a signed checkpoint artifact URL | `repo:read` + `repo:checkpoint-read` |
| Generate or retry a repository checkpoint | `repo:read` + `repo:checkpoint-read` + `repo:checkpoint-generate` (`repo:admin` also works for compatibility) |
| Submit a write (create or rename things and shapes) | `repo:write` |
| Read notifications | `repo:configure` |
| Create, update, pause, or remove a repo-scoped subscription | `repo:configure` |
| Read, lease, or deliver actions | `repo:configure` |
| Manage repo-scoped credentials — create, bind, grant, revoke | `repo:configure` |
| Rename a repo or change its settings | `repo:configure` |
| Delete, archive, or change a repo's visibility | `repo:admin` |
| Receive repo-level action callbacks | `repo:action-callback` |
| Read an org profile or list its members | `org:read` |
| Read installed components | `org:read` |
| Create a repo in an org | `org:configure` |
| Add, remove, or change a member's role (assigning or removing **owner** requires owner) | `org:configure` |
| Install or manage a component | `org:configure` |
| Change org settings | `org:configure` |
| Create, update, pause, or remove an org-scoped subscription | `org:configure` |
| Bind or unbind org-scoped credentials | `org:configure` |
| Receive org-level action callbacks | `org:action-callback` |
| Rename or archive an org | `org:admin` |
Scopes are checked against your role: a token can carry `repo:write`, but the write still fails if your role is `viewer`. Use the [`role:` shorthand](/auth/personal-access-tokens/#role-shorthand) to mint a token that mirrors a whole role at once.
---
# Getting Access
> Create an account, log in, and manage your authentication state.
WarmHub handles sign-up and sign-in through a hosted login flow — there are no credentials to configure locally. The first time you sign in, your account is created automatically.
## Creating an Account
There is no separate registration step. The first time you log in (via the CLI or web app), WarmHub creates your account. Supported sign-in methods include:
- Email and password
- Google
- GitHub
- SSO (SAML/OIDC) — if configured for your organization
Your identity works across the CLI, web app, SDK, and HTTP API.
### Accepting an org invite
Org invites are sent to a specific email address. Sign in with an account that uses that same address — otherwise you land in a different account than the one that was invited.
- **Signing in with GitHub uses the primary email on your GitHub account.** If that primary email doesn't match the invited address, GitHub sign-in authenticates as the wrong identity, and GitHub offers no way to pick a different email for sign-in. When the invited address isn't your GitHub primary, sign in with **email and password** instead.
- **Each account is tied to a single email today.** A personal email and a work email are separate accounts — there's no way to consolidate them. Keep them distinct and sign in with the email that matches the org you're joining.
- **Your sign-in provider must have verified your email.** WarmHub only lets you accept an invited org membership once the provider you sign in with (GitHub, your SSO, or email-and-password) has confirmed that email address. If it hasn't, the sign-in is rejected and asks you to verify first — verify your email with that provider, then sign in again.
### Your personal org
First-time sign-in also provisions a **personal org** for you automatically — a namespace you own where you can create repos immediately without waiting to be added to a team org. WarmHub picks the slug as follows:
1. **GitHub login** — if you signed in with GitHub (or your identity exposes a GitHub login), WarmHub prefers that login as your personal org slug.
2. **Adoption** — if you already own an org whose name matches your GitHub login, WarmHub adopts it as your personal org rather than creating a duplicate.
3. **Suffixing** — if the preferred slug is taken by someone else, WarmHub appends a numeric suffix (`alice-2`, `alice-3`, …).
4. **Fallback** — if no usable login is available, the slug falls back to `user-` derived from your user ID.
Personal orgs are regular orgs in every other respect — you own them, you can create repos and invite members, and they appear in `wh org list`. Two constraints apply:
- **Rename.** Personal orgs linked to a GitHub login (including fallback slugs) can't be renamed with `wh org rename`. Personal orgs without a linked GitHub login (SSO or email-only) can be renamed normally.
- **Reserved names.** The following slugs are reserved and rejected on create and rename (case-insensitive): `admin`, `api`, `billing`, `blog`, `docs`, `help`, `login`, `public`, `settings`, `signup`, `status`, `support`, `system`, `warmhub`, `www`. This applies to all orgs, not just personal ones.
### Your public identity
When you sign in, WarmHub publishes a public identity for you — the identity your commits are attributed to, and the one other records point at when they name you as an author. It shows a display name derived from your account and **never includes your email address**.
Behind that, the identity is a record named `Identity/` in the globally readable `warmhub/users` repo, so it's visible in the graph and can be referenced from anywhere. It's published the first time you sign in and refreshed periodically; publishing is best-effort, so a delay never blocks sign-in or any write.
## CLI Authentication
### Log in
```bash
wh auth login
```
This opens your browser for a device authorization flow:
1. The CLI displays a verification URL and one-time code.
2. Your browser opens to the login page.
3. You authenticate and confirm the code.
4. The CLI receives a token and saves it to `~/.warmhub/auth.json`.
Tokens auto-refresh in the background for active device-flow sessions. You should only need to log in once per machine.
If your session is invalidated (for example, revoked server-side or an expired refresh token), the CLI surfaces a `Session expired` error and prompts you to run `wh auth login` again.
#### Non-TTY and machine-readable challenge output
When `wh auth login` runs in a context where its output isn't connected to an interactive terminal — for example, a remote session, a wrapper script, or an agent process (technically: when stdout is not a TTY) — the CLI writes the device-flow challenge to stdout before it begins polling. This gives wrappers and agents a reliable way to capture the verification URL and code without scraping interactive prompts.
**Pretty mode (non-TTY stdout):** the verification URL and user code are printed to stdout as human-readable text, mirroring what an interactive session would display in the terminal.
**JSON mode:** pass `--format json` to receive a structured object:
```json
{
"deviceChallenge": {
"verificationUri": "https://signin.warmhub.ai/device?user_code=ABCD-1234",
"userCode": "ABCD-1234",
"expiresInSeconds": 300
}
}
```
`verificationUri` is the complete, prefilled link — it already embeds the `user_code` as a query parameter so the user can open it directly without typing the code. `userCode` repeats the code separately so your UI can display it alongside the link (for example, as a fallback if the user prefers to navigate to the activation page manually).
The command remains interactive after emitting this object — it continues polling until the user completes the flow or the challenge expires. A wrapper can read the JSON from stdout, surface the `verificationUri` and `userCode` to the end user through its own UI, and then wait for the command to exit.
`expiresInSeconds` is the lifetime of the challenge from the moment it is issued. If the user does not complete the flow within that window, the challenge expires and the command exits with an error; run `wh auth login` again to get a fresh challenge.
**Non-interactive environments** (CI, containers, headless servers):
For automation, use a [personal access token](/auth/personal-access-tokens/) via the `WH_TOKEN` environment variable instead of interactive login. See the [`wh token` commands](/cli-reference/commands/#token--personal-access-tokens) for creating tokens.
If you need to pipe a raw JWT (e.g., from another auth system):
```bash
echo "$JWT" | wh auth login --with-token
```
Piped tokens cannot auto-refresh — you'll need to provide a new one when it expires. PATs are the preferred approach for most non-interactive use cases.
### Named profiles
You can manage multiple credential sets with the `--profile` (`-P`) flag. Each profile stores its own tokens, the API endpoint used at login time, and any client flags set at login.
```bash
# Log in with a named profile (e.g., for a separate account)
wh auth login --profile work
wh auth login --profile personal
# Use a profile for any command
wh thing list --repo myorg/myrepo --profile work
# Or set via environment variable
export WH_PROFILE=work
wh thing list --repo myorg/myrepo
```
The CLI resolves the active profile in this order:
1. `--profile` flag — explicit per-command override; always wins.
2. `WH_PROFILE` environment variable — process-wide override, useful in scripts.
3. A profile stored in the current directory's `.wh` file — a repo can pin a saved profile so that commands run inside that directory automatically use the right credentials. See [Default Repo (`.wh` file)](/cli-reference/overview/#default-repo-wh-file).
4. The `default` profile — used when none of the above apply.
Each profile remembers the API URL it was logged in with — that stored URL applies when the profile is active, unless you override it with `--api-url`. The `WARMHUB_API_URL` environment variable is the fallback when no profile is loaded (for example, on a fresh machine before login).
#### Client flags on a profile
Profiles can store optional login-time flags alongside the token and API endpoint. When a flag is persisted on a profile, it is restored automatically on every command that uses that profile — you don't need to re-supply it each time. Client flags are feature-specific tokens that enable or configure particular CLI behaviors; see the [CLI reference](/cli-reference/commands/) for the flags available to you.
The `--flag` option accepts a repeatable string token (matching `[a-z0-9-]+`). Pass it once per flag you want to store:
```bash
# Persist a client flag on the "work" profile at login time
# (use whatever flag token applies to your situation)
wh auth login --profile work --flag
```
Subsequent commands that use the `work` profile pick up the stored flags without any extra arguments.
To update which flags are stored on a profile, log in again and supply the full set of `--flag` values you want going forward. Note that omitting `--flag` on a subsequent login does **not** clear previously stored flags — the profile retains its existing flags.
To remove all stored flags from a profile, log out of that profile and log in again without any `--flag` arguments. Logging out removes the profile's saved tokens from `~/.warmhub/auth.json` (including any stored flags), so the subsequent login starts clean:
```bash
wh auth logout --profile work
wh auth login --profile work
```
### Check status
```bash
wh auth status
```
Shows auth state for every profile and the `WH_TOKEN` environment variable. Here's what each entry includes:
- **Saved profiles** (including `default`) — auth method (device flow or piped token), token expiry, and API endpoint.
- **Active profile** — identity is resolved and shown.
- **Non-active saved profiles** — listed without identity information.
- **`WH_TOKEN` entry** — auth method and token expiry; no API endpoint and no identity are shown.
Use `--profile ` to check a specific profile.
### Log out
```bash
wh auth logout
```
Removes the default profile's saved tokens from `~/.warmhub/auth.json`. Use `--profile ` to log out a specific profile. Other profiles are not affected. If `WH_TOKEN` is set as an environment variable, unset it separately.
## The `WH_TOKEN` Environment Variable
For scripts and automation, set `WH_TOKEN` to a [personal access token](/auth/personal-access-tokens/) or JWT:
```bash
export WH_TOKEN=eyJhbGciOi...
wh thing list # authenticates automatically
```
`WH_TOKEN` takes precedence over saved credentials from `wh auth login`. The CLI checks for it on every command.
## Token Types
All API requests authenticate via Bearer tokens in the `Authorization` header. WarmHub supports three token types:
| Type | Obtained via | Use case | Expires |
|------|-------------|----------|-------|
| **Session JWT** | `wh auth login` or web app sign-in | Interactive use | Short-lived (auto-refreshes) |
| **Personal access token** | `wh token create` or SDK `client.token.create` | Scripts, CI/CD, integrations | 30 days default, 1 year max |
| **MCP OAuth token** | Automatic (OAuth flow in MCP client) | MCP clients (Claude, Cursor, etc.) | Session-based |
All token types share a unified validation model — they go through the same server-side JWT verification and permission checking.
See [Personal Access Tokens](/auth/personal-access-tokens/) for creating and managing PATs.
## Repository Visibility
Repositories are either **public** or **private**. Visibility is set at creation time and can be changed by org administrators.
| | Public | Private |
|--|--------|-------|
| **Read (queries, HEAD, history)** | Anyone — no auth required | Authenticated, `repo:read` permission |
| **Write (commits, shape mutations)** | Authenticated, `repo:write` permission | Authenticated, `repo:write` permission |
| **Manage (subscriptions, credentials)** | Authenticated, `repo:configure` permission | Authenticated, `repo:configure` permission |
| **Anonymous read requests** | Return data normally | Return `404` |
All writes require authentication regardless of visibility. Private reads require `repo:read` permission on the target repo — sign-in alone is not enough; you need to be a member of the repo's org. PATs can narrow what a member is allowed to access, but they don't grant access on their own. Cross-repo reads to a private target apply the same check, so a canonical `wh:org/repo/...` wref only resolves when you can read the target repo.
The anonymous-access row covers ordinary reads (queries, HEAD, history). Some endpoints require management-level access even on a public repo: action observability (run history, attempts, live feed, notifications) needs `repo:configure`, so anonymous callers get an opaque `404` rather than data. See [Actions API](/http-api/actions/) for the full contract.
## Next Steps
- [Personal Access Tokens](/auth/personal-access-tokens/) — create tokens for scripts and CI/CD
- [HTTP API Authentication](/http-api/authentication/) — PAT endpoint reference
- [CLI Commands](/cli-reference/commands/) — full command list including `auth` and `token`
---
# Personal Access Tokens
> Create scoped tokens for scripts, CI/CD pipelines, and integrations.
Personal access tokens (PATs) let scripts, CI/CD pipelines, and integrations authenticate with WarmHub without interactive login.
## Overview
| Property | Detail |
|----------|--------|
| **Format** | Signed JWT |
| **Default expiry** | 30 days |
| **Maximum expiry** | 1 year (set via `--expires`) |
| **Management** | CLI (`wh token`) or SDK (`client.token.*`) |
| **Management auth** | Interactive session (manages all your tokens) or a PAT (manages only tokens in its own descendant subtree) |
## Token Lifecycle
```
Create → Use → [Expire] → Revoke
```
1. **Create** — Generate a named token via `wh token create`. The token value is displayed once — save it immediately.
2. **Use** — Set the `WH_TOKEN` environment variable or pass the token in the `Authorization: Bearer` header. WarmHub validates the token on every request.
3. **Expire** — Tokens expire after 30 days by default. Use `--expires` to set a custom duration (max 1 year). Expired tokens are rejected immediately.
4. **Revoke** — Revoke a token by name via `wh token revoke`. Takes effect immediately.
Expired and revoked tokens are preserved for audit visibility.
## Scopes
Scopes limit what a token can do. They are optional — a token created without scopes has the full permissions of its owning user.
Each scope entry binds a **resource** to a set of **permissions**:
| Tier | Resource format | Example | Meaning |
|------|----------------|---------|-------|
| Repo-scoped | `org/repo` | `myorg/myrepo` | Specific repo only |
| Org-level | `org` | `myorg` | All repos in org, capped by role |
| Global wildcard | *(omitted)* | | All resources, capped by role |
More specific entries take precedence. Scopes can only narrow access, never escalate beyond the user's role.
### Available permissions
Each `--scope` entry lists one or more permissions: `repo:read`, `repo:checkpoint-read`, `repo:checkpoint-generate`, `repo:write`, `repo:configure`, `repo:admin`, `repo:action-callback`, `org:read`, `org:configure`, `org:admin`, or `org:action-callback`. Scopes are independent — `repo:checkpoint-read` does not include ordinary repository read authority — so request every permission your token needs. For what each permission grants and the minimum scope per task, see the [access reference](/auth/access-reference/).
### Role shorthand
Instead of listing individual permissions, a scope entry can name a role with `role:`. The role expands to a fixed set of permissions when the token is created:
| Role | Repo permissions | Org-level entry also grants |
|------|------------------|---------------------------|
| `role:owner` | `repo:read`, `repo:checkpoint-read`, `repo:checkpoint-generate`, `repo:write`, `repo:configure`, `repo:admin`, `repo:action-callback` | `org:read`, `org:configure`, `org:admin`, `org:action-callback` |
| `role:admin` | `repo:read`, `repo:checkpoint-read`, `repo:checkpoint-generate`, `repo:write`, `repo:configure`, `repo:admin`, `repo:action-callback` | `org:read`, `org:configure`, `org:action-callback` |
| `role:editor` | `repo:read`, `repo:write` | `org:read` |
| `role:viewer` | `repo:read` | `org:read` |
A `role:` entry requires a concrete repo or org resource — it is rejected on the global wildcard. (Omit `--scope` entirely for full user access.) On a repo-scoped entry, `role:owner` and `role:admin` grant the same permissions; they differ only on an org-level entry. Like any scope, a role can only narrow access, never escalate beyond your own role.
The role is expanded at creation time, so redefining a role later does not change tokens already issued.
## CLI Usage
You can run `wh token` commands from an interactive session — which manages all your tokens — or with a PAT, which manages only the tokens in its own descendant subtree (the tokens it created, and theirs). Component (action) tokens cannot manage tokens.
### Create a token
The `--scope` flag accepts three forms:
| Form | Meaning | Example |
|------|---------|--------|
| `org/repo=perms` | Repo-scoped | `--scope myorg/myrepo=repo:read,repo:write` |
| `org=perms` | Org-level (all repos in org) | `--scope myorg=repo:read` |
| `perms` | Global wildcard (all resources) | `--scope repo:read,repo:write` |
Separate multiple permissions with commas. Repeat `--scope` for multiple entries. In the repo-scoped and org-level forms, the permission list can be a [`role:` shorthand](#role-shorthand) instead of individual permissions.
```bash
# Full user access, default 30-day expiry
wh token create --name ci-bot
# Repo-scoped token
wh token create --name ci-bot --scope myorg/myrepo=repo:read,repo:write
# Role shorthand — same as myorg/myrepo=repo:read,repo:write
wh token create --name ci-bot --scope myorg/myrepo=role:editor
# Org-level wildcard (all repos in org, capped by role)
wh token create --name org-reader --scope myorg=repo:read
# Global wildcard with description and custom expiry
wh token create -n deploy --scope repo:write -d "Deploy pipeline" --expires 90d
# Multiple scopes — repeat --scope for each entry
wh token create --name ci-bot \
--scope myorg/private-repo=repo:read,repo:write \
--scope myorg=repo:read \
--expires 90d
```
### Ref-restricted tokens with `--scopes-json`
For advanced use cases, the `--scopes-json` flag accepts a JSON array of scope entries with optional `allowedMatches` patterns that restrict which refs within a repo the token can access. Patterns match against repo-scoped refs such as `Sensor/temp-1` (a thing ref) or `Subscription/sub-foo` (a subscription ref). The same patterns are enforced across thing operations, all subscription management operations (including creating and reading subscriptions), and subscription-keyed action reads (the action live feed, run listings and stats, run attempts, and notifications). For `repo:configure` entries, `allowedMatches` also gates credential-set surfaces: `bindCredentials` requires both the subscription ref and the `CredentialSet/` ref to match a positive pattern.
`--scopes-json` is mutually exclusive with `--scope` — you cannot use both in the same command.
Each entry in the JSON array has:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `resource` | string | No | Resource name: `"org/repo"`, `"org"`, or omitted (global wildcard) |
| `permissions` | string[] | Yes | Permissions for this resource |
| `allowedMatches` | string[] | No | Glob patterns restricting which repo-scoped refs this entry can access. **Repo-scoped entries only** (`"org/repo"`). Ignored on org-level and global entries. |
`allowedMatches` behavior:
- **Repo-scoped entries only.** `allowedMatches` on org-level or global wildcard entries is stripped with a warning — ref restrictions require a specific repo target.
- Patterns use glob syntax and match against the full ref, for example `Sensor/*` (things of type Sensor), `Config/**` (all Config things), or `Subscription/sub-foo` (a specific subscription).
- When present, the token can only access refs that match at least one positive pattern (and no carve-out). This applies to both thing refs and subscription refs.
- **Shape visibility follows the same matcher.** `allowedMatches` applies to shape reads as well as thing reads: `shape.list` filters results to shapes whose bare name matches, and `shape.get` (and `client.shape.get`) rejects requests for a bare shape name that falls outside the token's patterns. For example, a token with `allowedMatches: ["GameState/**"]` will not surface the `GameState` schema itself — you must also include the bare name `"GameState"` as a positive pattern if your tooling needs to read the shape definition.
- A leading `!` marks a deny carve-out: `["Player/**", "!Player/secret/**"]` grants Player things except those under `Player/secret/`. Evaluation is two-phase — positives establish the grant, carve-outs subtract from it, and a carve-out always wins regardless of order.
- Carve-outs match the full ref, not a sub-path of the positives: `["Player/**", "!secret/**"]` still grants all Player things (the carve-out targets the separate `secret` namespace).
- A list of only carve-outs (e.g. `["!secret/**"]`) grants everything the entry's permissions grant, minus the carved-out refs.
- Extglob negation `!(...)` is rejected at token creation; express denies with a leading `!` instead.
- Omitting `allowedMatches` means no ref restriction for that entry.
- An empty array (`[]`) is valid and means deny-all for that entry.
With `--scopes-json`, you can assign different `allowedMatches` to different permissions on the same resource by using multiple entries. Each `(resource, permission)` pair must be unique — duplicate pairs are rejected with a validation error.
```bash
# Read Sensor/* and Config/*, but only write Sensor/*
wh token create --name scoped-bot --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Sensor/*","Config/*"]},
{"resource":"myorg/myrepo","permissions":["repo:write"],"allowedMatches":["Sensor/*"]}
]'
# Simple ref restriction — read-only for Sensor/*
wh token create --name reader --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Sensor/*"]}
]'
# Subscription-scoped — configure only a specific subscription
wh token create --name sub-manager --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:configure"],"allowedMatches":["Subscription/sub-foo"]}
]'
# No allowedMatches — same as --scope shorthand
wh token create --name full --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read","repo:write"]}
]'
```
The token is printed once. Save it to a secret store or environment variable immediately.
### Child-token carve-out inheritance
When a PAT creates a child token, the child's `allowedMatches` list must preserve or broaden every deny carve-out from the parent. You can narrow the child's positive matches, add new carve-outs, or widen an existing carve-out to cover more refs — but you cannot drop a carve-out that the parent already enforces.
For example, if the parent token was created with `["Player/**", "!Player/secret/**"]`, any child token for the same entry must include a deny carve-out that covers at least `Player/secret/**`. A child using `!Player/secret/**` (identical) or `!Player/**` (broader) both satisfy the contract. Attempting to create a child token whose `allowedMatches` omits an inherited carve-out entirely — leaving refs the parent denied now accessible — is rejected with an authorization error.
This rule applies at every level of the subtree: a grandchild token cannot drop or narrow a carve-out that the grandparent established, even if the intermediate parent did not add new ones.
### Committer identity
A token can carry a default committer identity with `--committer-identity`. Writes made with the token are attributed to that identity unless a per-command `--committer` overrides it:
```bash
wh token create --name ci-bot --committer-identity wh:warmhub/users/Identity/
```
The value is an [identity](/auth/getting-access/#your-public-identity) wref. Without it, writes are attributed to your own user identity.
**The identity must be your own.** `--committer-identity` only accepts a public Identity that belongs to you — passing another user's public Identity is rejected. Globally readable `warmhub/users` identities are not forgeable committer labels.
### List tokens
```bash
wh token list
wh token list --json
```
Shows all your tokens with their status (active, expired, or revoked) and scopes.
### View a token
```bash
wh token get --name ci-bot
```
Shows token details: name, description, scopes, status, creation date, and expiry.
### Revoke a token
```bash
wh token revoke --name ci-bot
```
Revocation is immediate. The token is rejected on the next API request.
## Using PATs with the HTTP API
Pass the token in the `Authorization` header:
```bash
curl -H "Authorization: Bearer eyJhbGciOi..." \
https://api.warmhub.ai/api/repos/myorg/myrepo/head
```
Or with the CLI via `WH_TOKEN`:
```bash
export WH_TOKEN=eyJhbGciOi...
wh commit submit --add cave --shape Location \
--data '{"x":3,"y":7}' -m "Add cave"
```
`WH_TOKEN` takes priority over any stored CLI profile. See [Environment Variables](/reference/env-vars/) for the full precedence rules.
## Using PATs with MCP Clients
MCP clients like Claude and Cursor normally authenticate via OAuth automatically. However, PATs are useful when:
- Your organization restricts custom MCP connectors
- You're on WSL2, where the OAuth callback can't reach the browser
- You want to authenticate a dev instance without configuring OAuth
Use [`mcp-remote`](https://github.com/geelen/mcp-remote) as a stdio bridge with your PAT:
```json
{
"mcpServers": {
"warmhub": {
"command": "npx",
"args": [
"-y", "mcp-remote@latest",
"https://mcp.warmhub.ai/mcp",
"--header", "Authorization:${WH_TOKEN}",
"--transport", "http-only"
],
"env": {
"WH_TOKEN": "Bearer eyJhbGciOi..."
}
}
}
}
```
This works in both Claude Desktop (`claude_desktop_config.json`) and Claude Code (`.mcp.json`). The `--transport http-only` flag ensures HTTP Streamable transport.
See the [Quickstart](/get-started/#connect-via-mcp) for standard OAuth-based setup.
## Security Model
- **No escalation.** A PAT can manage only the tokens in its own descendant subtree — the tokens it created, and theirs. It cannot list, read, or revoke tokens outside that subtree, so a leaked PAT can't reach the rest of your tokens. An interactive session manages all your tokens.
- **Scope enforcement.** Each API endpoint checks the token's scopes. A `repo:read` token cannot push commits.
- **Immediate revocation.** Revoking a token takes effect on the next request — there is no grace period.
- **Name restrictions.** Token names must be alphanumeric, hyphens, and underscores only (URL-safe).
## Limitations
- **No token rotation.** To rotate, revoke the old token and create a new one.
- **No web UI.** Tokens are managed via the CLI or SDK only.
- **Subtree-scoped under a PAT.** `wh token` and `client.token.*` accept an interactive session (all your tokens) or a PAT (only its own descendant subtree); component (action) tokens are rejected. To manage tokens outside a PAT's subtree, sign in with `wh auth login` (CLI) or use an interactive session before calling the SDK methods.
## Next Steps
- [Getting Access](/auth/getting-access/) — interactive login and account creation
- [HTTP API Authentication](/http-api/authentication/) — using PATs as Bearer tokens
- [CLI Commands](/cli-reference/commands/) — full `token` command reference
---
# Commands
> All CLI commands organized by domain — org, repo, shape, thing, assertion, collection, commit, view, grant, use, init, doctor, sub, auth, token, credential, component, prime, onboard, update, channel, notifications.
Most commands follow the pattern `wh [args] [--flags]`. A few are flat single commands that take no verb:
| Flat command | Description |
|---|---|
| `wh use` | Local repo context |
| `wh init` | Bootstrap local harness |
| `wh doctor` | Diagnostics and health checks |
| `wh prime` | Agent context |
| `wh onboard` | Agentic onboarding script |
| `wh update` | Update the CLI |
| `wh notifications` | Action notifications |
| `wh channel` | Real-time repo events in Claude Code |
Global flags available on most commands:
| Flag | Description |
|---|---|
| `--repo` | Target repo |
| `--json` | Alias for `--format json` |
| `--live` | Stream live updates |
| `--format` | Output mode: `pretty` (default), `json`, or `jsonl` |
| `--profile` / `-P` | Named credential profile |
| `--no-update-check` | Skip the CLI update notice for this run |
| `--dry-run` | Preview command dispatch without calling the backend. Exception: `wh commit submit --dry-run` calls the server's real commit evaluator and returns a non-persisted operation preview. |
`--format` controls output mode: `pretty` (default), `json`, or `jsonl`. When `--live` is active, `json` and `jsonl` emit structured output per update; `pretty` (or unset) renders a human-readable display. `--json` is the constant alias for `--format json`. Choose either `--json` or one `--format ` spelling; combining them is rejected as a duplicate scalar flag.
Two additional flags apply to any command that supports `--live`: `--max-updates ` exits automatically after receiving `n` updates, and `--live-timeout-ms ` exits after `ms` milliseconds of inactivity. During normal execution both require live mode to be enabled; omitting `--live` or passing `--live=false` is rejected.
## Retrying a write: `--event-request-id`
Several write commands accept `--event-request-id `. Pass your own UUID, and if the command fails in a way that leaves you unsure whether it landed — a timeout, a dropped connection — run it again with the **same** id. The second run returns the original outcome instead of writing twice. Omit the flag and the CLI generates one per run, which is fine until you need to retry.
The commands that take it are noted below: `wh repo content set`, `wh shape create` / `revise` / `rename`, `wh thing rename`, and `wh collection create` / `revise`. Not every write accepts it — `wh shape retract`, for instance, does not. Component lifecycle commands take `--submission-id`, which is **not** the same guarantee: it lets you recover what a run already changed, but it does not make re-running the command safe — see `wh component install`.
```bash
REQID=$(uuidgen)
wh repo content set myorg/world --kind readme --file README.md --event-request-id "$REQID"
# unclear result? run the identical command again with the same id
wh repo content set myorg/world --kind readme --file README.md --event-request-id "$REQID"
```
## use — Local repo context
| Command | Description |
|---------|-------------|
| `wh use [--profile ]` | Set the default repo (and optionally the auth profile) for the current directory |
| `wh use` | Show the active repo and profile context and where each comes from |
| `wh use --clear` | Remove the `.wh` context file |
Writes a `.wh` JSON file in the current directory holding the repo and, when `--profile` is passed, the auth profile. The CLI resolves the target repo using this priority: `--repo` flag > `WARMHUB_REPO` env > `.wh` file, and the auth profile using: `--profile` / `-P` flag > `WH_PROFILE` env > `.wh` file. The repo is validated against the backend before writing.
```bash
wh use myorg/world # writes .wh, validates repo exists
wh use myorg/world --profile work # also records the auth profile in .wh
wh use # shows active repo + profile with provenance
wh use --clear # removes .wh
wh thing list # uses .wh repo (no --repo needed)
```
## org — Organization management
| Command | Description |
|---------|-------------|
| `wh org create [--display-name "..."] [--description "..."]` | Create a new organization. Description is trimmed; max 2000 characters. Requires an interactive browser sign-in — personal access tokens can't create organizations. |
| `wh org view ` | View organization details |
| `wh org list [--include-archived]` | List the organizations you belong to (archived hidden by default) |
| `wh org rename [] [--slug ] [--display-name "..."]` | Rename an organization and/or update its display name. The positional `` updates the slug; `--slug` is the named equivalent. Use `--display-name` alone to update only the display name without changing the slug. |
| `wh org member add [--role role]` | Add a member or send an invite. Only owners can assign `owner` role. If the email is not a WarmHub user, a pending invite is created and an invite email is attempted (best-effort, async). |
| `wh org member remove ` | Remove a member or revoke a pending invite |
| `wh org member list [--pending]` | List members of an organization. Requires authentication and org membership. `--pending` filters to pending invites only. |
| `wh org member set-role --role ` | Change a member's role. Only owners can promote to or demote from `owner`. Cannot demote the last owner. |
| `wh org archive [--yes]` | Archive an organization (blocks new repos and members) |
| `wh org update --description "..."` | Update org description. Trimmed; empty strings clear the value; max 2000 characters. |
| `wh org unarchive ` | Unarchive an organization |
Roles: `owner` (full control), `admin` (manage members and repos), `editor` (read/write data), `viewer` (read-only). Default: `editor`.
### Reserved org names and personal orgs
`wh org create` and `wh org rename` both reject these reserved slugs (case-insensitive): `admin`, `api`, `billing`, `blog`, `docs`, `help`, `login`, `public`, `settings`, `signup`, `status`, `support`, `system`, `warmhub`, `www`.
When a user signs in for the first time, WarmHub provisions a **personal org** for them. GitHub-linked personal orgs use a login-based slug when available, and collisions receive a suffix. GitHub-linked personal orgs can't be renamed — `wh org rename` returns `FORBIDDEN`. Personal orgs without a linked GitHub login can be renamed normally.
```bash
wh org create acme --display-name "Acme Corp"
wh org list
wh org member add acme alice@example.com --role editor
wh org member list acme
wh org member remove acme alice@example.com
wh org rename acme newname # rename slug via positional
wh org rename acme --slug acme-co # rename slug via flag
wh org rename acme --display-name "Acme Inc" # update display name only
```
## repo — Repository management
| Command | Description |
|---------|-------------|
| `wh repo create [--display-name "..."] [--description "..."] [--visibility ]` | Create a repo (auto-creates org if needed). Description is trimmed; max 2000 characters. |
| `wh repo list [org] [--include-archived] [--limit n] [--cursor c] [--all]` | List repos in an org (archived hidden by default). Results are paginated: by default the first page is returned with a notice when more exist. Use `--all` to fetch every page, or `--limit` / `--cursor` to page manually. |
| `wh repo search [--limit n] [--cursor c] [--all]` | Search repos across all orgs **visible to you** (public, plus private repos you can read) by name, display name, description, shape names/descriptions, and README content. Without pagination flags, only the first page of results is returned and the CLI prints a notice if more pages exist — pass `--all` to fetch every page automatically, or use `--limit` / `--cursor` to step through pages manually. Cross-org discovery (not org-scoped like `list`). |
| `wh repo view [org/repo]` | Show repo details |
| `wh repo describe [org/repo] [--indexed-fields]` | Describe repo schema: shapes, fields, inline descriptions, and per-shape counts. Shapes from installed component manifests appear with count `0` when no things have been written yet. Pass `--indexed-fields` to append an `Indexed Fields` section to the normal schema output, showing typed field index state (`building`, `ready`, `failed`, or `other`) — one row per indexed field, with both the shape name and field path reported for each entry. |
| `wh repo rename [] [--slug ] [--display-name "..."]` | Rename a repo and/or update its display name. The positional `` updates the slug; `--slug` is the named equivalent. Use `--display-name` alone to update only the display name without changing the slug. |
| `wh repo archive [--yes]` | Archive a repo (blocks new commits) |
| `wh repo update --description "..."` | Update repo description. Trimmed; empty strings clear the value; max 2000 characters. |
| `wh repo visibility ` | Set repo visibility |
| `wh repo unarchive ` | Unarchive a repo |
| `wh repo export [org/repo] [--output ] [--mode heads\|ops] [--since-repo-seq n] [--at-repo-seq n] [--async] [--wait] [--resume] [--token ]` | Export the repository as verified NDJSON at an exact sequence. A bare `--async` prepares the export and prints a redeemable token; `--output PATH\|-` is required only when the command will download bytes (pass a file path or `-` for stdout). Pass `--async` together with `--wait` to start the export and block until it completes, then download the result. Pass `--token` with `--output` to download a previously prepared export. See [repository export](/cli-reference/repository-export/). |
| `wh repo verify ` | Verify a saved repository export locally: framing, canonicality, record count, sequence, and digest. Contacts no server. |
| `wh repo delete [--yes]` | Delete a repo. Hidden immediately and purged after the retention window. Blocks if another repo holds inbound cross-repo references or subscriptions. |
`wh repo create` also accepts `--org ` as a tolerance fallback — `wh repo create --org ` is combined into `/` and the CLI prints a one-line hint pointing at the canonical positional form. Prefer `/` in scripts.
For checkpoint operations on a repo, see [Repository Checkpoints](/cli-reference/repository-checkpoints/).
```bash
wh repo create myorg/world -d "Game world"
wh repo create myorg/world -d "Game world" --visibility public
wh repo create myorg/world --display-name "My World" -d "Game world"
wh repo list myorg
wh repo search users
wh repo rename myorg/world newworld # rename slug via positional
wh repo rename myorg/world --slug world-v2 # rename slug via flag
wh repo rename myorg/world --display-name "My World" # update display name only
wh repo describe myorg/world # schema view
wh repo describe myorg/world --indexed-fields # schema view plus indexed fields section
```
## repo content — README, AGENTS.md, llms.txt
WarmHub stores per-repo markdown content as built-in `Content` instances. Two are stored and editable: `Content/Readme` (human-facing) and `Content/Agents` (agent-facing). A third, `Content/LlmsTxt`, is synthesized per request from the repo's shapes and content and is read-only.
:::note[`--event-request-id` on `repo content set`]
`wh repo content set` accepts [`--event-request-id`](#retrying-a-write---event-request-id) — reuse the same id to retry safely after an unclear result.
:::
| Command | Description |
|---------|-------------|
| `wh repo content get [org/repo] --kind readme\|agents\|llms-txt` | Fetch repo Content markdown by kind. |
| `wh repo content set [org/repo] --kind readme\|agents\|llms-txt [--content "..." \| --file ] [--event-request-id ]` | Set Content markdown (file, inline, or piped stdin). `llms-txt` is read-only — `set` attempts are rejected. Pass `--event-request-id` for safe resubmission — see the note above. |
| `wh repo content prompt [org/repo] --kind readme\|agents\|llms-txt` | Print an agent-ready prompt (bounded repo context + the follow-up `set` command) so your own agent can draft Content locally — no hosted LLM. `llms-txt` is read-only and cannot be drafted or set; passing `--kind llms-txt` errors immediately. |
| `wh repo content generate [--kind readme\|agents\|llms-txt]` | **Deprecated — always errors.** WarmHub does not host README/AGENTS generation. Use `wh repo content prompt` instead: it prints a prompt your own agent drafts from, and you save the result with `wh repo content set`. |
`set` reads from `--file `, `--content "..."`, or stdin when neither flag is passed. Use `--file -` to force stdin.
The `[org/repo]` argument is optional for all three verbs — when omitted, the command falls back to the repo set via `wh use` for the current directory. You can also override the target repo for any single invocation with `--repo`.
```bash
wh repo content get myorg/world --kind readme
wh repo content set myorg/world --kind readme --file README.md
cat README.md | wh repo content set myorg/world --kind readme
wh repo content set myorg/world --kind agents --content "# Agents"
# Draft locally with your own agent, then save the result:
wh repo content prompt myorg/world --kind readme
wh repo content set myorg/world --kind readme --file README.md
wh repo content get myorg/world --kind llms-txt
```
## shape — Shape management
:::note[`--event-request-id` on `wh shape create` / `revise` / `rename`]
`wh shape create`, `wh shape revise`, and `wh shape rename` all accept [`--event-request-id`](#retrying-a-write---event-request-id) — reuse the same id to retry safely after an unclear result.
:::
| Command | Description |
|---------|-------------|
| `wh shape list [--match "glob"] [--include-retracted] [--component ] [--exclude-components]` | List shapes. Use `--include-retracted` to show retracted shapes and `--exclude-components` to hide component-owned shapes. |
| `wh shape view [--include-retracted]` | Show shape details with field definitions |
| `wh shape create (--fields '' \| --file ) [--description '...'] [--event-request-id ]` | Create a new shape. `--file` reads the same fields object from disk and avoids shell-specific JSON quoting. In pretty mode, the request ID is written to stderr; pass `--json` to receive the full shape-change response on stdout, which carries the receipt under `receipt`. Pass `--event-request-id` for safe resubmission — see the note above. |
| `wh shape revise (--fields '' \| --file ) [--description '...'] [--show-diff] [--event-request-id ]` | Revise shape fields from inline JSON or a JSON object file (creates a new version when the definition changes; produces a `noop` when the fields are identical to the current version). **The fields object is a full replacement, not a merge** — omitting a field removes it from the shape. Include all fields you want to keep. Pass `--show-diff` to print the field additions, removals, and type changes the revision applied. When `--json` is used together with `--show-diff`, the result includes a `diff` object describing those changes. Pass `--event-request-id` for safe resubmission — see the note above. |
| `wh shape retract [--reason ''] [-m msg] [--committer ] [--expected-version ]` | Retract a shape, marking it inactive. `--expected-version` retracts only if the shape is still at that version. |
| `wh shape history [--include-retracted] [--limit n] [--cursor c] [--all]` | Show shape version history |
| `wh shape rename [--event-request-id ]` | Rename a shape. Pass `--event-request-id` for safe resubmission — see the note above. |
Teaching rejection: `wh shape delete ` exits with a hint to use `wh shape retract`.
```bash
wh shape create Location --fields '{"x":"number","y":"number"}'
wh shape create Location --file location-fields.json
wh shape list
wh shape view Location
wh shape revise Location --fields '{"x":"number","y":"number","label":"string"}'
wh shape revise Location --file location-fields.json --show-diff # print added/removed/changed fields
wh shape retract OldShape -m "Withdraw old shape"
```
## thing — Thing operations
:::note[`--event-request-id` on `thing rename`]
`wh thing rename` accepts [`--event-request-id`](#retrying-a-write---event-request-id) — reuse the same id to retry safely after an unclear result.
:::
| Command | Description |
|---------|-------------|
| `wh thing list [--shape s] [--kind shape\|thing\|assertion\|collection] [--limit n] [--cursor c] [--all] [--match glob] [--where "field=val"] [--include-retracted] [--count] [--since-repo-seq ] [--component ] [--exclude-components]` | Show current HEAD state. Results are paginated with a default page cap of 50 rows; use `--all` to auto-fetch every page, or use `--limit` / `--cursor` to page manually. Without `--all`, pretty output prints a notice when additional rows exist; either `--json` or `--format jsonl` suppresses that notice, but combining them is rejected. Use `--count` to return only the count of matching items. `--where` is a repeatable typed field-value predicate (`field=val`, `field>=val`, `field~prefix`, `field in:[a,b]`, `field?`) — see [Field-Value Predicates](/queries/filtering/#field-value-predicates). Use `--since-repo-seq ` to return only identities changed after that repository sequence number (`-1` or any nonnegative safe integer; cannot be combined with `--live`). When no `--shape` is given, system-managed component infrastructure records are hidden by default. Use `--exclude-components` to also hide component-owned data things. |
| `wh thing create [--shape s] (--data '' \| --file ) [-m msg] [--committer ]` | Create a thing from inline JSON or a JSON object file. Pass either a qualified `Shape/name` or an unqualified name with `--shape`, not both. |
| `wh thing view [...] [--file ] [--version n] [--depth n] [--include-retracted] [--data-mode auto\|full]` | Show a shape or shaped thing's details. Variadic: a single wref routes to the single-target path; multiple wrefs or `--file` routes to batch fetch (max 500 wrefs per call). `--depth` returns an embedded graph view and cannot be combined with `--live`, `--version`, or `--include-retracted` — graph reads are active-only and version-pinned graph reads are not supported on this command. Pretty output includes a `by:` row with the committer wref when the version was written with `--committer`. `--data-mode` controls collection data output for built-in collection shapes (`Arc`, `Bond`, `Set`, and `List`): `auto` (default) summarizes large bodies; `full` forces the complete collection body into the output. `--data-mode` cannot be combined with `--depth`. |
| `wh thing history [wref] [--shape] [--about] [--resolve-collections] [--limit] [--cursor] [--all] [--include-retracted]` | Show version history for a shape, shaped thing, or — when `--about ` is passed — assertions about the target. When `--about ` is used, the optional positional wref scopes which assertion shape's history to return (e.g. `wh thing history Location/cave --about Player/alice` returns the version history of assertions whose shape is `Location/cave` and whose `about` target is `Player/alice`). Each version line ends with `by ` when attribution is recorded, followed by `born: ` when creation metadata is present. |
| `wh thing resolve ` | Resolve wref to a shape or shaped-thing identity. Accepts canonical `wh:org/repo/...` inputs and self-routes them without requiring `--repo`. |
| `wh thing lease [--ttl ]` | Acquire a short [read lease](#thing-read-leases) on a thing and read it in one step. Prints the lease id and expiry. `--ttl` defaults to `5000` (min `1000`, capped at a maximum); a value outside that range is rejected, never clamped. |
| `wh thing revise --data '' [-m msg] [--committer ] [--expected-version ] [--lease-id ]` | Revise a thing. `--committer` is optional; when omitted, the commit is attributed to your token's bound committer identity (set via `wh token create --committer-identity`) if one is configured, otherwise to your authenticated user identity. Pass `--expected-version` to apply the revise only if the thing is still at that version (optimistic concurrency). Pass `--lease-id` to write under a [read lease](#thing-read-leases); the lease auto-releases on a successful or no-op write. |
| `wh thing retract [--kind thing\|assertion\|shape\|collection] [--reason ''] [-m msg] [--committer ] [--expected-version ] [--lease-id ]` | Retract an entity (thing, assertion, shape, or collection), marking it inactive. `--kind` is an optional safety check: when supplied, the command errors if the resolved entity's actual kind does not match the value you passed. Accepted values are `thing`, `assertion`, `shape`, and `collection`. `--committer` is optional; when omitted, the commit is attributed to your token's bound committer identity (set via `wh token create --committer-identity`) if one is configured, otherwise to your authenticated user identity. `--expected-version` retracts only if the entity is still at that version; `--lease-id` writes under a [read lease](#thing-read-leases). |
| `wh thing release-lease --lease-id ` | Release a [read lease](#thing-read-leases) early when you decide not to write. Idempotent — a non-matching or already-released lease is not an error. |
| `wh thing query [--shape s] [--kind shape\|thing\|assertion\|collection] [--about wref] [--affirmed-about ] [--resolve-collections] [--role from\|to\|ends] [--limit n] [--cursor c] [--all] [--match glob] [--where "field=val"] [--include-retracted] [--count] [--since-repo-seq ] [--component ] [--exclude-components]` | Query by filters. Results are paginated with a default page cap of 50 rows; use `--all` to auto-fetch every page, or use `--limit` / `--cursor` to page manually. Without `--all`, pretty output prints a notice when additional rows exist; either `--json` or `--format jsonl` suppresses that notice, but combining them is rejected. Use `--count` to return only the count of matching items — `--count` cannot be combined with `--role`, `--cursor`, `--all`, `--limit`, or `--live`. `--where` is a repeatable typed field-value predicate (`field=val`, `field>=val`, `field~prefix`, `field in:[a,b]`, `field?`) — see [Field-Value Predicates](/queries/filtering/#field-value-predicates). Use `--since-repo-seq ` to return only identities changed after that repository sequence number (`-1` or any nonnegative safe integer; cannot be combined with `--live`). When no `--shape` is given, system-managed component infrastructure records are hidden by default. `--role` filters collection membership by direction and is only valid together with `--about` and `--resolve-collections`. `from` returns collections where the target is the source end, `to` returns collections where the target is the destination end, and `ends` returns collections where the target appears at either end. `--affirmed-about` accepts a pinned target wref (e.g. `Shape/name@vN`) and narrows results to active assertions whose current version affirms that pinned target. The wref must be pinned; any pinned target wref is accepted, not only shape versions. |
| `wh thing search [--shape s] [--kind shape\|thing\|assertion\|collection] [--about wref] [--affirmed-about ] [--mode text\|vector\|hybrid] [--resolve-collections] [--include-retracted] [--limit n] [--cursor c] [--all] [--component ] [--exclude-components]` | Search repository content by text, vector, or hybrid mode. `--cursor` and `--all` only apply in text mode. `--component` and `--exclude-components` only apply in text mode. In text mode, system-managed component infrastructure records are hidden by default when no `--shape` is given. `--resolve-collections` also only applies in text mode. `--affirmed-about` accepts a pinned target wref (e.g. `Shape/name@vN`) and narrows results to active assertions whose current version affirms that pinned target; the wref must be pinned, and any pinned target wref is accepted, not only shape versions. See [search indexing behavior](#thing-search-indexing) below. |
| `wh thing refs [--inbound\|--outbound] [--field path] [--limit n --cursor c] [--all]` | Show things that store this shape or shaped-thing wref in a field (inbound, default), or wrefs stored in this target's fields (outbound). Use `wh thing about` for assertions targeting it. `--field` filters inbound refs by field path. `--cursor` requires `--limit` (cursors are scoped to a specific page size). |
| `wh thing about [--shape s] [--match glob] [--depth n] [--resolve-collections] [--role from\|to\|ends] [--limit n --cursor c] [--all] [--include-retracted]` | Show assertions whose `about` target is this shape or shaped thing (or shape wref). Use `--shape` or `--match` to narrow the assertions, `--depth` to include child assertions about the returned assertions, and `--resolve-collections` to include assertions about collections containing the target. `--role` filters which collections are considered when `--resolve-collections` is active: `from` includes only collections where the target is the source end, `to` includes only collections where the target is the destination end, and `ends` includes collections where the target appears at either end. `--role` requires `--resolve-collections`. `--cursor` requires `--limit` (cursors are scoped to a specific page size). Accepts both thing wrefs and shape wrefs as the target. |
| `wh thing rename [--event-request-id ]` | Rename a thing. Pass `--event-request-id` for safe resubmission — see the note above. |
:::caution[Default 50-row page cap]
`wh thing list` and `wh thing query` both cap output at **50 rows per page** by default. In pretty output mode, the CLI prints a notice when additional rows exist. If you are running analytic workloads or aggregations, always pass `--all` to retrieve every page automatically, or use `--limit` / `--cursor` to step through pages manually. Either `--json` or `--format jsonl` suppresses this notice; combining them is rejected.
:::
```bash
wh thing list --shape Location
wh thing create Location/cave --file cave.json
wh thing view Location/cave --version 3
wh thing view Location/cave --depth 2
wh thing query --kind assertion --about Location/cave
wh thing search "safe location" --shape Observation --mode hybrid
wh thing history Location/cave --limit 10
wh thing history Location/cave --about Player/alice # history of assertions whose shape is Location/cave and whose about target is Player/alice
wh thing refs Location/cave # inbound refs (default)
wh thing refs Location/cave --field target # filter by field path
wh thing refs Observation/obs1 --outbound # outbound refs
wh thing about Location/cave # assertions about this thing
wh thing about Location/cave --shape Observation # filter assertions by shape
wh thing about Location/cave --depth 2 # include child assertions
wh thing about MyShape # assertions about a shape wref
wh thing list --count # count all active items
wh thing list --shape Location --count # count by shape
wh thing query --about Location/cave --count # count matching query results (cannot combine --count with --role)
# Filter to active assertions whose current version affirms a pinned target wref
wh thing query --affirmed-about Observation@v2
wh thing search "cave" --affirmed-about Observation@v2
# Incremental reads — return only identities changed after a repo sequence number
wh thing list --shape Location --since-repo-seq 42
wh thing query --kind assertion --since-repo-seq 0
wh thing list --count --since-repo-seq 100 # count items changed after seq 100
# wh thing view is variadic — batch fetch up to 500 wrefs in one call
wh thing view Player/alice Player/bob Player/cara # variadic positionals
cat wrefs.txt | wh thing view # piped stdin (one wref per line)
wh thing view --file wrefs.txt --version 3 # pin all to @v3 (implies --include-retracted)
wh thing view Player/alice@v1 Player/bob@v2 # per-wref pinning
wh thing view --file=- < wrefs.txt --format jsonl # `--file=-` reads stdin (use the `=`; `--file -` errors with `Flag --file requires a value`)
# Force full collection body output for a large built-in collection
wh thing view Set/large-set --data-mode full
# --data-mode also works in batch reads
wh thing view Set/large-set Set/other-set --data-mode full
```
Batch `wh thing view` (multiple wrefs or `--file`) supports three output modes:
- **Pretty** (default): `Requested N, found N, missing N` header, then one line per resolved item with kind label and `[RETRACTED]`/`[draft]` markers, then a `Missing:` block listing wrefs that didn't resolve.
- **`--json`** (or `--format json`): a single `{ requested, items, missing }` object — `items[]` carries the same fields as single-thing view, `missing[]` is `string[]` of unresolved wrefs (qualified when applicable).
- **`--format jsonl`**: one JSON record per *deduped* requested wref, in input order. Inputs from positionals + `--file` + stdin are unioned and deduped before the round-trip, so a wref supplied twice produces one row. Each row is `{requested, found, wref, ...}`. The fields below distinguish two [wref](/data-modeling/wrefs/) shapes — the **local form** (`Shape/name`, repo-relative) and the **canonical form** (`wh:org/repo/Shape/name`, fully qualified for cross-repo reads):
- `requested` — the original input string verbatim, so canonical inputs like `wh:org/repo/Loc/cave` are still identifiable downstream after the backend normalizes them to local form.
- `wref` — the local form on hits (e.g. `Loc/cave` even when the input was canonical), or the version-qualified form from `result.missing` on misses.
- `found` — boolean.
Correlate input lines to result lines by `requested` and filter by `found` without reconstructing the envelope.
`--data-mode` controls how built-in collection shapes (`Arc`, `Bond`, `Set`, and `List`) render their body: `auto` (default) summarizes large bodies; `full` forces the complete body into the output. It is accepted on both single-target and batch reads. Passing `--data-mode` together with `--depth` is an error: `--data-mode cannot be combined with --depth on view`.
Passing `--version` automatically implies `--include-retracted`, so retract versions can be retrieved by their pinned id.
`--live` is rejected in batch mode — batch reads are one-shot. Use `wh thing view --live` for per-thing polling.
:::note[`--depth` flag constraints on `thing view`]
Passing `--depth` together with `--version` or `--include-retracted` on `wh thing view` is an error:
```
--depth cannot be combined with --include-retracted or --version on `view`
(graph reads are active-only).
```
`wh thing graph` accepts a `--version` flag for version-pinned graph reads. Retracted graph reads are not supported by either command.
:::
### Thing read leases
A read lease gives you a short, exclusive window on a single thing for a read-modify-write cycle. While you hold the lease, another caller's `revise`/`retract` of that thing fails fast with a recoverable `LEASE_UNAVAILABLE` error. You learn about the conflict *before* computing a new value, not after the write. Leasing is opt-in: plain `wh thing view` reads are unaffected, and an expired or released lease falls back to a normal optimistic write.
```bash
# 1. Acquire + read in one step (prints lease id and expiry).
wh thing lease Player/alice --ttl 5000
# 2. Compute the new value, then write it back under the lease.
# The lease auto-releases on a successful or no-op write.
wh thing revise Player/alice --data '{"score":2}' --lease-id
# Or hand the lease back early if you decide not to write.
wh thing release-lease Player/alice --lease-id
```
Notes:
- `wh thing lease` is the acquire verb; the plain read verb stays `wh thing view`. Acquiring requires write access. If another caller already holds an active lease, it fails fast with `LEASE_UNAVAILABLE` instead of waiting.
- TTL defaults to 5s and has a fixed 1s floor and an upper bound. An out-of-range `--ttl` is rejected, never silently clamped.
- On [`wh commit submit`](/cli-reference/write-submit-deep-dive/), `--lease-id` binds to a single `--revise`/`--retract` short-form op and requires exactly one `--retract` target — a multi-target `--retract` with `--lease-id` is rejected. For `--ops`/`--file`/`--stream` writes, carry `leaseId` inline on the operation instead.
- A `LEASE_UNAVAILABLE` error tells you when the current holder's lease expires, so you can retry after that time.
### Thing search indexing
`wh thing search` behavior varies by mode:
**Text mode** (default when `--mode` is omitted or `--mode text`)
- Every field a thing's shape declares is indexed when it holds a value, regardless of type. Strings, numbers, booleans, arrays, and nested object values are all converted to searchable text. Only fields described by the shape contribute — values not covered by a shape field are not indexed.
- New things and revisions are indexed asynchronously after the write completes. There is a short backfill delay before a newly written thing appears in text search results.
**Vector mode** (`--mode vector`) and **hybrid mode** (`--mode hybrid`)
- Vector and hybrid search depend on embeddings being enabled for the WarmHub deployment you connect to. When embeddings aren't available, these modes return an error; text mode always works. See [search modes](/queries/filtering/#search) for what each mode does.
- `--cursor` and `--all` are not supported in vector or hybrid mode.
### Durable ids on `thing` reads
A [durable id](/data-modeling/wrefs/#durable-ids) is accepted anywhere a wref is. Because a bare durable id self-routes, repo context is not required on `wh thing view`, `history`, `resolve`, `refs`, and `about` when the target is a bare durable id:
```bash
wh thing view # no repo context needed
wh thing history
wh thing about
```
`wh thing resolve` also self-routes canonical `wh:org/repo/...` inputs — no repo context is required when the wref is in canonical form.
`wh assertion view` and `wh assertion history` accept bare durable ids without repo context.
`wh thing graph` and `wh thing view --depth` also self-route when the root is a bare durable id — no repo context is required in that case. When the root is a canonical `wh:`-prefixed wref (e.g. `wh:org/repo/Shape/name`), the command still needs repo context for graph expansion — supply it via `--repo`, `WARMHUB_REPO`, or a `.wh` file.
## thing graph — Embedded graph reads
`wh thing graph` is a sub-command of the `thing` domain. Starting from the requested thing or shape wref, it traverses outbound wref fields and assertion backlinks — specifically, assertion `about` targets and outbound wref fields — and returns them embedded as objects. Depth defaults to 2 and is capped at 5. The traversal does **not** follow inbound wref-field backlinks (things that happen to reference the target in a field); use `wh thing refs --inbound` for that case. Only data the caller can read is resolved — inaccessible refs stay as string wrefs and the output does not expose internal IDs or denial reasons.
`wh thing graph` accepts `--version ` to pin the root thing to a specific version. It does not accept `--include-retracted` — retracted graph reads are not supported.
```bash
wh thing graph Location/cave
wh thing graph Location/cave --depth 3
wh thing graph Location/cave --depth 2 --json
wh thing graph Location/cave --version 2
```
## assertion — Assertion operations
| Command | Description |
|---------|-------------|
| `wh assertion list [--about wref] [--shape s] [--depth n] [--limit n] [--cursor c] [--all] [--match glob] [--resolve-collections] [--include-retracted] [--count] [--field ]` | Browse assertions in HEAD, optionally scoped to assertions about a thing or shape wref. Use `--count` to return only the count. Use `--field` to filter by an exact data-field value (`data.path=value`). |
| `wh assertion view [--version n] [--depth n] [--include-retracted]` | Show an assertion's details (equivalent to `wh thing view` — assertions are things). `--depth` returns an embedded graph view instead of the flat assertion record and cannot be combined with global `--live`. `--depth` is active-only: it cannot be combined with `--version` or `--include-retracted`. If you need a version-pinned graph read, use `wh thing graph`. Accepts a bare durable id without repo context; when the root is a bare durable id, `--depth` also self-routes without repo context. When the root is a canonical `wh:`-prefixed wref, repo context is still required for graph expansion — supply it via `--repo`, `WARMHUB_REPO`, or a `.wh` file. |
| `wh assertion create --shape --name --about [--data ''] [--affirm ] [-m msg] [--committer ]` | Create a named assertion. `--about` accepts an untyped shape or shaped-thing wref. To assert about a collection, create a named collection first and point `--about` at that collection's wref. `--affirm` is repeatable; each value is a pinned wref (e.g. `Shape/name@vN`) that the assertion affirms. Pass `--affirm` multiple times to affirm more than one target. |
| `wh assertion revise --data '' [--affirm ] [-m msg] [--committer ]` | Revise an assertion. `--affirm` supplies the **complete affirmation set** for the revised assertion (repeatable); omitting `--affirm` entirely clears the existing affirmation snapshot. Pass `--affirm` once per pinned wref you want the revised assertion to affirm — include every target you want to keep, not just new ones. See [Assertion operations](/data-modeling/assertions/) for the full affirmation contract. |
| `wh assertion reaffirm (--add \| --remove ) [--add ...] [--remove ...] [--expected-version ] [-m msg] [--committer ]` | Update an assertion's affirmed targets without changing its data, using delta flags. `--add` adds a pinned wref to the affirmed set; `--remove` removes one. Both flags are repeatable and can be combined in a single call. At least one `--add` or `--remove` is required — omitting both is an error. Pass `--expected-version` to apply the reaffirm only if the assertion is still at that version. See [Assertion operations](/data-modeling/assertions/) for the full reaffirm contract. |
| `wh assertion retract [--reason ''] [-m msg] [--committer ] [--expected-version ]` | Retract an assertion, marking it inactive. `--expected-version` retracts only if the assertion is still at that version. |
| `wh assertion history [--include-retracted] [--limit n] [--cursor c] [--all]` | Show assertion version history. Accepts a bare durable id without repo context. |
Prefer `--about` for target-scoped assertion lists. A bare target after `list` is accepted as an agent convenience when translating user phrasing like "assertions about X". `--about` accepts both thing wrefs and shape wrefs.
`wh thing view ` also works since assertions are things.
:::note[`--depth` is active-only on `assertion view`]
Passing `--depth` together with `--version` or `--include-retracted` on `wh assertion view` is an error:
```
--depth cannot be combined with --include-retracted or --version on `view`
(graph reads are active-only).
```
`wh thing graph` accepts `--version ` for version-pinned graph reads. Retracted graph reads are not supported by either command.
:::
```bash
wh assertion create --shape Observation --name cave-safe --about Location/cave --data '{"safe":true}'
wh assertion create --shape Observation --name cave-safe --about Location/cave --data '{"safe":true}' --affirm Location/cave@v1
wh collection create --type bond --name cave-lake --members Location/cave,Location/lake
wh assertion create --shape Connected --name cave-lake-connected --about Bond/cave-lake --data '{"connected":true}'
wh assertion list --about Location/cave --shape Observation
wh assertion list --about MyShape # assertions about a shape wref
wh assertion view Observation/cave-safe --depth 2
wh assertion revise Observation/cave-safe --data '{"safe":true,"confidence":0.9}'
# --affirm is a complete replacement — repeat every pinned target you want to keep
wh assertion revise Observation/cave-safe --data '{"safe":true}' --affirm Location/cave@v2
wh assertion reaffirm Observation/cave-safe --add Location/cave@v2
wh assertion reaffirm Observation/cave-safe --add Location/cave@v2 --add Location/cave@v3
wh assertion reaffirm Observation/cave-safe --remove Location/cave@v1 --add Location/cave@v2
wh thing view Observation/cave-safe
wh assertion list --shape Observation
wh assertion list --count
wh assertion history Observation/cave-safe --include-retracted
wh assertion list --about Session/run-001 --shape HypothesisCandidate --field data.round=1
```
## collection — Collection operations
Collections group wrefs into typed containers. `--type arc` creates a directed `Arc/`, `--type bond` creates a symmetric `Bond/`, `--type set` creates a `Set/`, and `--type list` creates a `List/`. See [Collections](/data-modeling/collections/) for the full data model.
:::note[`--event-request-id` on `wh collection create` / `revise`]
`wh collection create` and `wh collection revise` both accept [`--event-request-id`](#retrying-a-write---event-request-id) — reuse the same id to retry safely after an unclear result.
:::
| Command | Description |
|---------|-------------|
| `wh collection create --type --name [--members ] [--file ] [--stdin] [--wref-field ] [--shape ] [--where "field=val"] [--match glob] [--kind ] [--about ] [--component ] [--exclude-components] [--source-repo org/repo] [--from ] [--add ] [--remove ] [--replace-file ] [--replace-stdin] [--skip-existing] [--event-request-id ] [-m msg] [--committer ]` | Create a named collection. For new models, use `arc`, `bond`, `set`, or `list`. Supports three creation flows — explicit members, selector-backed set, and copy/delta — described below the table. Pass `--skip-existing` (valid only with `--name`) to make the create a no-op when a collection with that name already exists, instead of erroring. Pass `--event-request-id` for safe resubmission — see the note above. |
| `wh collection revise [--members ] [--file ] [--stdin] [--wref-field ] [--add ] [--remove ] [--shape ] [--where "field=val"] [--match glob] [--kind ] [--about ] [--component ] [--exclude-components] [--source-repo org/repo] [--event-request-id ] [-m msg] [--committer ]` | Update an existing collection. Pass `--members`, `--file`, or `--stdin` to replace the full member list, `--add` / `--remove` for set deltas, or `--shape` / `--where` / `--match` (and the selector filters `--kind`, `--about`, `--component`, `--exclude-components`) to refresh a selector-backed set. Pass `--event-request-id` for safe resubmission — see the note above. |
| `wh collection members [--version n] [--limit n] [--cursor c] [--all]` | List the members of a collection. Paginated. Pass `--version` to read a specific historical version. |
| `wh collection contains [--members ] [--file ] [--stdin] [--position ] [--version n] [--wref-field ]` | Check whether a collection contains one or more member wrefs. Pass members as positional arguments, via `--members` (comma-separated), `--file`, or `--stdin`. Use `--position ` to constrain the check to a specific zero-based list index, `--version` to check against a specific version, and `--wref-field` to specify which field to match on. Pretty output prints hit positions without requiring `--position`. |
| `wh collection diff [--mode ] [--left-version n] [--right-version n]` | Show the member-level diff between two collections (or two versions of the same collection). `--mode` controls diff output format. `--left-version` and `--right-version` pin each side to a specific version. |
| `wh collection stats [--version n]` | Show summary statistics for a collection (member count, type, version). Pass `--version` to read stats for a specific historical version. |
`wh collection create` supports three creation flows:
- **Explicit members** — pass `--members`, `--file`, `--stdin`, or `--wref-field` to supply members directly.
- **Selector-backed set** — pass `--shape`, `--where`, `--match`, and/or the selector filters `--kind`, `--about`, `--component`, `--exclude-components` (optionally `--source-repo`) to define a dynamic query that populates the set.
- **Copy or delta** — pass `--from ` to base the new collection on an existing one, optionally combined with `--add`, `--remove`, `--replace-file`, or `--replace-stdin` to apply changes on top.
```bash
# Explicit-members creation
wh collection create --type set --name cave-set --members Location/cave,Location/lake
wh collection create --type bond --name cave-lake --members Location/cave,Location/lake
wh collection create --type list --name route --members Location/start,Location/end
# Selector-backed set creation
wh collection create --type set --name all-caves --shape Location --where "kind=cave"
# Copy or delta creation
wh collection create --type set --name cave-set-v2 --from Set/cave-set
# Revise: full replacement
wh collection revise Set/cave-set --members Location/cave,Location/lake,Location/river
# Revise: set delta
wh collection revise Set/cave-set --add Location/river --remove Location/lake
# Revise: refresh selector-backed set
wh collection revise Set/all-caves --shape Location --where "kind=cave"
# Read commands
wh collection members Set/cave-set
wh collection members Set/cave-set --version 2
wh collection contains Set/cave-set Location/cave
wh collection contains Set/cave-set Location/cave --position 0 --version 3
wh collection diff Set/cave-set Set/other-set
wh collection diff Set/cave-set Set/cave-set --left-version 1 --right-version 3
wh collection stats Set/cave-set
wh collection stats Set/cave-set --version 2
```
## commit — Write operations
| Command | Description |
|---------|-------------|
| `wh commit submit [--ops json] [-f file] [--stream] [--stream-id id] [--server-stream] [--group-size n] [--skip-existing] [--add name] [--revise wref] [--retract wref] [--expected-version n] [--reason text] [--affirm ] [flags]` | Submit a stream of operations. Bare `wh commit` is equivalent. `--expected-version` requires `--revise` or exactly one `--retract` and is shorthand for the short-form flags only; for `--ops` inline JSON arrays, `--file`, and `--stream` batches, carry `expectedVersion` inside each operation object instead. `--affirm` is repeatable; each value is a pinned wref (e.g. `Shape/name@vN`) that the submitted assertion affirms — see [Affirming a target with `--affirm`](#affirming-a-target-with---affirm) for where it can be used. |
`wh commit submit` is the write entrypoint. Use `wh thing history ` for
the per-thing version trail.
#### Affirming a target with `--affirm`
`--affirm` works only when the command writes exactly one assertion:
- Use it with `--add` or `--revise`. It is rejected with `--ops`, `--file`, or `--stream` — for those, put `affirmedTargets` on each operation object instead.
- With `--add`, affirm one add at a time. Repeating `--add` alongside `--affirm` is an error, because the repeated `--affirm` values form a single affirmation set.
- The write must be an assertion. `--add` becomes one as soon as you pass `--about`; `--revise` does not, so add `--kind assertion` when you affirm on a revise.
See [Commit Submit Deep-Dive](/cli-reference/write-submit-deep-dive/) for the full breakdown of `commit submit` flags, including:
- **JSONL client-streaming** with `--stream` / `--file *.jsonl` (both require `--stream-id` and `--skip-existing`), plus `--progress` and `--chunk-size`.
- **JSONL server-streaming** with `--server-stream` and `--stream-id`. In server-stream mode the server applies each operation and emits a versioned result row as it goes; use `--format jsonl` or default pretty output to receive that row stream — `--server-stream` rejects only `--json`. `--server-stream` requires `--stream-id` and accepts `--group-size ` to control how many operations are grouped per server-side batch.
```bash
wh commit submit --add cave --shape Location --data '{"x":3,"y":7}' -m "Add cave"
wh commit submit --retract Location/old-cave --reason "duplicate" -m "Retract duplicate cave"
wh commit submit --add alice --data '{"score":1}' --add bob --data '{"score":2}' --shape Player -m "seed players"
wh commit submit --file dataset.jsonl --stream-id bulk-import --skip-existing --progress -m "Bulk import"
wh commit submit --file dataset.jsonl --stream-id bulk-import --skip-existing -m "Rerun seed"
cat ops.jsonl | wh commit submit --stream --stream-id pipe-stream --skip-existing -m "Pipe stream"
wh commit submit --file dataset.jsonl --server-stream --stream-id server-import --format jsonl --group-size 50 -m "Server-stream import"
```
`--add` is repeatable up to 20 operations; pair each with its own `--data`. See [Commit Submit Deep-Dive](/cli-reference/write-submit-deep-dive/#add-multiple-things-in-one-write-request) for cardinality rules.
## view — Stored-View read operations
A View is a saved query stored in the repo as a versioned object. `wh view evaluate` runs the query and returns the resulting rows. Views are addressed by wref (e.g. `View/my-view`).
| Command | Description |
|---------|-------------|
| `wh view evaluate [--limit n] [--cursor c] [--all]` | Evaluate a stored View by its wref and return the resulting rows. Results are paginated: by default the first page is returned with a notice when more exist. Use `--all` to fetch every page automatically, or use `--limit` together with `--cursor` to page manually. Note: `--cursor` requires `--limit` — passing `--cursor` without `--limit` is rejected. |
```bash
wh view evaluate View/my-view
wh view evaluate View/my-view --limit 25
wh view evaluate View/my-view --cursor --limit 25
wh view evaluate View/my-view --all
```
## grant — Grant management
Grants control which members, personal access tokens, and components can perform specific operations on a repo's data. Use `wh grant create` to issue a grant, `wh grant get` or `wh grant list` to inspect grants, and `wh grant revoke` to remove one. Revoked grants stay queryable: `get` returns a grant whether it is active or revoked, and `list` returns both, so revocation history remains available for audit.
The target repo for a grant comes from the active repo context — set via `wh use` or `--repo`. `--key` is an issuer-scoped idempotency key: passing the same `--key` value a second time returns the original grant instead of creating a duplicate.
The grantable operations are: `things:read`, `commits:read`, `shapes:read`, `notifications:read`, `subscriptions:read`, and `subscriptions:create`.
:::caution[Grant issuance is off by default]
`wh grant create` is disabled per repo and must be enabled for your repo before it will work. Until then it returns `FORBIDDEN`, even when the syntax is correct. `wh grant get`, `wh grant list`, and `wh grant revoke` are not gated and work regardless.
:::
**Only a signed-in member can create a grant.** Personal access tokens and components can *receive* grants but cannot issue them, so a request authenticated with a PAT or as a component is rejected. Automation that issues grants cannot run under a PAT.
The recipient you name has to resolve to something live at issuance time:
- `member ` — a member of the repo's org.
- `pat ` — one of **your own** active tokens. Another user's token name is rejected, and so is one of yours that has been revoked or expired.
- `component ` — a component with an active installation in the target repo.
Once issued, a grant keeps replaying for the same request key even if its recipient's token is later revoked or its component is uninstalled.
| Command | Description |
|---------|-------------|
| `wh grant create --key --op [--op ...] (--coverage JSON \| --view View/NAME[@vN])` | Create a new grant against the active repo. The first positional is the grantee type (`member`, `pat`, or `component`); the second is the grantee identifier (email address, token name, or `org/name`). At least one `--op` is required and specifies the operation being permitted (repeatable). Exactly one of `--coverage` or `--view` must be supplied to scope what the grant covers. `--coverage` accepts a JSON object with a non-empty `include` array (e.g. `'{"include":["**"]}'`). `--key` is required. It is an issuer-scoped idempotency key — reuse the same value to retrieve the original grant instead of creating a duplicate. |
| `wh grant get ` | Get details for a specific grant by its grant ID. Returns the grant whether it is active or revoked. |
| `wh grant list [--limit n] [--cursor c]` | List active and revoked grants. Use `--limit` and `--cursor` to page through results manually. |
| `wh grant revoke [--reason ]` | Revoke a grant by its grant ID. Pass `--reason` to record why the grant was removed. |
Grant IDs are returned by `wh grant create` and appear in `wh grant list` output. Pass the ID to `wh grant get` or `wh grant revoke`.
```bash
# Grant a member things:read access covering all things in the active repo
wh grant create member alice@example.com --key grant-alice-read --op things:read --coverage '{"include":["**"]}'
# Grant a PAT things:read and commits:read access scoped to a specific view
wh grant create pat my-ci-token --key grant-ci-read --op things:read --op commits:read --view View/my-view
# Grant a component shapes:read and subscriptions:read access
wh grant create component myorg/my-component --op shapes:read --op subscriptions:read \
--coverage '{"include":["**"]}' --key my-idempotency-key
# Inspect and manage grants
wh grant list
wh grant list --limit 20 --cursor
wh grant get
wh grant revoke
wh grant revoke --reason "role change"
```
For the full repo and org permission model — role to capability, scope to capability, and the minimum scope for a given task — see the [Access reference](/auth/access-reference/).
## shape template — Scaffold write operations
`wh shape template` is a sub-command of the `shape` domain. It generates sample operations from shape definitions for use with `wh commit submit`.
| Command | Description |
|---------|-------------|
| `wh shape template [shape2 ...] [--operation add\|revise\|retract] [--kind thing\|assertion\|shape\|collection] [--about ] [--count n] [--output file]` | Generate sample operations from shapes. When `--operation retract` is used, `--kind` accepts `thing`, `assertion`, `shape`, or `collection`. For `add` and `revise`, passing one of the built-in collection shape names (`Arc`, `Bond`, `Set`, or `List`) as the `` argument emits a valid `kind: "collection"` operation payload instead of a thing scaffold — no extra flags required. Pass `--kind assertion` to scaffold an assertion about a collection instead. For all other shapes, `add` and `revise` accept `--kind thing` or `--kind assertion`. `--about` supplies the assertion-target placeholder (used with `--kind assertion`) and accepts a shape or shaped-thing wref. |
```bash
wh shape template Hypothesis
wh shape template Hypothesis --operation retract
wh shape template Hypothesis Evidence --count 3 -o experiment.json
wh shape template Hypothesis --kind assertion --about ResearchTopic/example
# Scaffold a collection add/revise operation for a built-in collection shape
wh shape template Arc
wh shape template Bond --operation revise
wh shape template Set --count 2 -o set-ops.json
wh shape template List
```
## init — Bootstrap local harness
| Command | Description |
|---------|-------------|
| `wh init [org/repo] [-d "description"]` | Install harness hooks and optionally onboard a repo |
Aliases: `init` passes unknown verbs as positional args, so `wh init myorg/myrepo` works directly.
```bash
wh init
wh init myorg/myrepo
wh init myorg/myrepo --description "Demo repo"
```
## doctor — Diagnostics and health checks
| Command | Description |
|---------|-------------|
| `wh doctor [--fix]` | Run environment and backend health checks |
`wh doctor` runs a set of environment and backend checks and reports their results. The checks cover:
- **Environment** — HTTP timeout configuration and API URL
- **Repo and profile** — default repo, active profile (including whether it came from `--profile`, `WH_PROFILE`, or `.wh`), and repo accessibility. The profile check runs before any network probes, so it appears in the output even when the backend is down.
- **Backend connectivity and auth** — authenticated identity (email plus server-validated scopes and token name for PATs). If the backend doesn't recognize your token — for example, when it was issued for a different API URL — the `auth` check warns, shows the API URL it tried, and suggests running `wh auth login`. The auth check is skipped when the backend is unreachable.
- **Claude settings** — validates that Claude settings files parse as valid JSON and reports any malformed files
- **Claude hooks** — checks whether the WarmHub hooks are installed
- **AGENTS.md** — checks whether AGENTS.md includes the WarmHub block
- **Compatibility** — CLI and backend version compatibility
Use `--fix` to auto-install missing hooks and to create or update AGENTS.md with the WarmHub harness block — this includes repairing the harness block in an existing AGENTS.md while preserving the rest of the file you have written.
```bash
wh doctor
wh doctor --fix
```
## sub — Subscription management
See [Subscriptions](/subscriptions/overview/) for concepts (filters, retry behavior) and [Creating Subscriptions](/subscriptions/creating/) for detailed setup guides.
| Command | Description |
|---------|-------------|
| `wh sub create [--event ] --filter '' --webhook-url [--on ] [flags]` | Create a commit-event webhook subscription. `--event` defaults to `commit` when omitted. `--filter` is required for commit subscriptions. Pair with `--on` to scope to a specific shape. |
| `wh sub create