---
title: ggui keys register
description: Register a publisher's Ed25519 public key with the ggui.ai marketplace registry so signed gadget and blueprint publishes validate.
---

:::caution[Coming soon]
This page describes the **managed hosted path** (`mcp.ggui.ai` / `console.ggui.ai` / the `registry.ggui.ai` marketplace), which is **not yet live** — it is not part of GGUI Preview 0.1.0. The self-hosted path is available today — start with the [Quickstart](/oss-quickstart/). This page is kept as a preview of the managed path and goes live when hosted ggui ships.
:::

`ggui keys register` ships your **publisher** Ed25519 public key to the marketplace registry's `POST /author-keys` endpoint. After this, every subsequent `ggui gadget publish` / `ggui blueprint publish` whose bundle is signed by the matching private key validates against the registry's stored row.

:::note[Publisher key vs. connector key]
The `keys list / create / revoke` subcommands manage **connector keys** (`ggui_user_*`) that an agent runtime uses to authenticate to `mcp.ggui.ai` as a CLI bearer. `keys register` manages a different keypair entirely: a per-scope **publisher key** that signs marketplace artifacts. Different audience, different lifecycle — see [`ggui login`](/cli/login/) for the connector-key flow.
:::

## When to use it

You run `ggui keys register` **after** `ggui gadget publish` (or `ggui blueprint publish`) has auto-generated a keypair on disk for a given scope. The publish flow does the keypair generation automatically on first run, then signs the artifact, then POSTs to `/publish`. The registry rejects a signed publish whose `publicKeyId` isn't already registered for the caller's identity — that's the moment to run this command.

The typical bootstrap sequence for a new publisher:

```bash
# 1. First publish under @my-org auto-generates
#    ~/.ggui/keys/@my-org/{private,public}.key and signs the bundle.
ggui gadget publish
# → Error: unknown_key — public key not registered for this publisher.

# 2. Register the public half with the registry.
ggui keys register --scope @my-org

# 3. Re-run the publish. The signature now verifies.
ggui gadget publish
```

After step 2, every future publish under `@my-org` from this machine (and any other machine you copy `~/.ggui/keys/@my-org/private.key` to) validates without a second `register` call.

## Usage

```text
ggui keys register --scope <@scope> [--registry <url>] [--auth=bearer [--token <token>]]
```

| Flag            | Required | Purpose                                                                                                                                                                                                                                                                               |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--scope`       | yes      | npm scope the key was generated under, prefixed with `@` (e.g. `--scope @my-org`). The CLI reads `~/.ggui/keys/<scope>/public.key` — must already exist (see "When to use it" above).                                                                                                 |
| `--registry`    | no       | Override the registry URL. Same resolution chain as `ggui gadget publish`: `--registry` flag, then `GGUI_REGISTRY` env var, then `ggui.json#registry` walking up from CWD. No hard-coded default — pick a registry deliberately so a typo doesn't accidentally register against prod. |
| `--auth=bearer` | no       | Send an explicit bearer token instead of the stored `ggui login` session — for self-hosted registries that authenticate with a static publish token. Pair with `--token <token>` or set `GGUI_REGISTRY_TOKEN`. Same flags the publish verbs take.                                     |
| `--token`       | no       | The bearer token for `--auth=bearer` (overrides `GGUI_REGISTRY_TOKEN`).                                                                                                                                                                                                               |

## Auth

`ggui keys register` uses your stored **`ggui login` session** by default — the same credential `ggui gadget publish` sends to `/publish`, and the one the hosted registry's `/author-keys` route authenticates (see [`Marketplace § Auth`](/sdk/marketplace/#auth)). The CLI reads `~/.ggui/auth.json`, refreshes the access token automatically when it has expired, and sends it as `Authorization: Bearer <token>` — the only identity surface; the request body carries only `publicKeyBase64`, never the caller's user id. The server derives the publisher subject from the verified credential and the `keyId` from the raw public-key bytes.

Self-hosted operators running their own [`@ggui-ai/registry-server`](/sdk/self-hosted-registry/) deployments pass `--auth=bearer --token <token>` (or set `GGUI_REGISTRY_TOKEN`) — the same escape hatch the publish flow takes (see [`ggui marketplace`](/sdk/marketplace/#auth) for the parallel).

## Output

Success — first-write (HTTP 201):

```bash
$ ggui keys register --scope @my-org
Registered publisher key for @my-org.
  registry: https://registry.ggui.ai
  subject:  <your-user-id>
  keyId:    a1b2c3d4e5f60718
```

Idempotent re-register (HTTP 200) — same public-key bytes for `(subject, keyId)` already on file:

```bash
$ ggui keys register --scope @my-org
Already registered publisher key for @my-org.
  registry: https://registry.ggui.ai
  subject:  <your-user-id>
  keyId:    a1b2c3d4e5f60718
```

Both exit `0`. Safe to run unconditionally in CI bootstrap scripts.

## Errors and exit codes

| Code                  | Exit | Meaning                                                                                                                                                                                                                                                                   |
| --------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `no-registry`         | 1    | No registry URL resolved. Pass `--registry`, set `GGUI_REGISTRY`, or add `registry` to `ggui.json`.                                                                                                                                                                       |
| `invalid-registry`    | 1    | Resolved URL is malformed.                                                                                                                                                                                                                                                |
| `no-keypair`          | 1    | No `~/.ggui/keys/<scope>/public.key` on disk. Run `ggui gadget publish` or `ggui blueprint publish` under this scope first — first-publish generates the keypair as a side-effect.                                                                                        |
| `auth_failed`         | 1    | The stored login session expired (or its refresh was rejected). Run `ggui login` again.                                                                                                                                                                                   |
| `auth_config_missing` | 1    | No stored login session (`~/.ggui/auth.json` missing or unreadable) — run `ggui login` first. Or `--auth=bearer` was passed without `--token` / `GGUI_REGISTRY_TOKEN`.                                                                                                    |
| `network-error`       | 1    | `fetch` threw — DNS, TLS, or connection refused.                                                                                                                                                                                                                          |
| `unauthorized`        | 1    | Registry rejected the bearer credential (HTTP 401). Re-run `ggui login` (the session may have been revoked) — or, for self-hosted registries, check the `--auth=bearer` token.                                                                                            |
| `invalid_request`     | 1    | Registry refused the body (HTTP 400). The public-key file is corrupted or the wrong length — regenerate by deleting `~/.ggui/keys/<scope>/` and re-running `ggui gadget publish`.                                                                                         |
| `key_conflict`        | 3    | Registry holds a different public key for the same `(subject, keyId)` tuple (HTTP 409). Vanishingly rare — a 64-bit `keyId` SHA-256 truncation collision OR a stale row from a previous owner. Exit `3` is distinct so scripts can detect it without parsing the message. |
| `http-error`          | 1    | Any other non-2xx response (typically 5xx). Check the message for the registry's error string; retry transient failures.                                                                                                                                                  |
| `bad-response`        | 1    | Registry returned a 2xx with a malformed body, or any status with invalid JSON. Almost always a registry-side bug — re-run; if persistent, the registry is misconfigured.                                                                                                 |

The structured `error` field of the registry's response body (closed enum: `unauthorized` / `invalid_request` / `key_conflict` / `server_error`) is preferred over status-code mapping when the response carries a well-formed body — `key_conflict` returned with a non-409 status still surfaces as `key_conflict` on the CLI side.

## Trust model

The on-disk private key under `~/.ggui/keys/<scope>/private.key` is mode `0o600` — treat it like a long-lived password. Copying it between machines lets you publish from CI without re-registering. Losing it means generating a new keypair under the same scope: when the new public key gets registered, both old + new keys are valid (publish flow pins the signing key onto each `ArtifactVersionRow` at publish time, so historical versions still verify under the previous key). To rotate out the old key entirely, register the new one + remove the old row server-side (operator-only).

See [`Marketplace § Trust model`](/sdk/marketplace/#trust-model) for the full per-author-key + per-version pinning design and the install-time two-leg verification (SHA-384 + Ed25519).