> ## Documentation Index
> Fetch the complete documentation index at: https://docs.afauth.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Ship AFAuth in your CLI

> Distribute a CLI or client that provisions an agent-native identity for your users — its own keypair, no portal account — by embedding the AFAuth agent role.

You run a service, and you ship a **CLI** (or a language SDK, or a desktop app) that your users — and increasingly their coding agents — run locally. This page is how that client provisions an identity *on its own*, with a cryptographic keypair, instead of gating first use behind an email + OTP + browser session.

It's the **client half**. Your backend accepts AFAuth-signed requests already — if not, start at [Accept AFAuth](/quickstart-service) — and here you give the CLI you distribute an AFAuth identity so it can provision against that backend with no shared password and no portal to automate.

<Note>
  **Which front door is this?** [Build an agent](/quickstart-agent) is for a *roaming* agent product. [Accept AFAuth](/quickstart-service) is your *backend*. [Run a coding agent](/equip-your-agent) is an end user equipping Claude Code. **This page is where two roles fuse:** a service that distributes a client is *both* agent and service — your CLI consumes the agent SDK, your server consumes the server SDK.
</Note>

## The shape

Your CLI plays the **agent role** against your **own** backend. The user types *your* product's verbs — `acme deploy`, `acme login` — and AFAuth stays invisible plumbing: the client holds a `did:key` and signs each request. One call provisions it, and `@afauthhq/agent`'s `signup()` **adapts to whatever attestation mode your service advertises** — so the same client code works whether or not you gate signup.

```bash theme={null}
npm i @afauthhq/agent
```

## Provision in one call

`signup()` fetches your discovery doc, links to a human **only if** your service is `attested_only` (and only the first time on a machine), then sends the signed implicit-signup request and returns the binding to persist.

<Steps>
  <Step title="Load or create the identity">
    ```ts theme={null}
    import { loadOrCreateAgent, loadBinding, saveBinding } from "@afauthhq/agent/node";

    const { agent } = await loadOrCreateAgent(); // reads the shared ~/.afauth/key.json
    ```

    `loadOrCreateAgent()` reads the shared **agent home**, so if the user already ran `afauth init` (or any other AFAuth-aware tool), you reuse that identity — and any human link it already has. The user links at most **once, ever**, across every AFAuth service.
  </Step>

  <Step title="Sign up">
    ```ts theme={null}
    import { signup } from "@afauthhq/agent";

    const existing = (await loadBinding({ agentDid: agent.did })) ?? undefined;

    const result = await signup({
      agent,
      baseUrl: "https://api.acme.dev",
      binding: existing,
      label: "acme-cli",
      onLink: (url) => {
        // Fires ONLY when your service is attested_only and this machine isn't
        // linked yet — the one-time human approval. Under `optional`, never called.
        console.error("Approve this agent once — you, in a browser:\n  " + url);
      },
    });

    // Persist a freshly-established link so the human is never prompted again.
    if (result.binding && result.binding !== existing) {
      await saveBinding({ agentDid: agent.did, binding: result.binding });
    }
    ```

    The first signed request provisions the account on your backend — there's no separate create call. `signup()` is mode-agnostic: against an `optional` service `onLink` never fires; against `attested_only` it fires once, then the saved binding is reused.
  </Step>

  <Step title="Hand ownership to a human, later (optional)">
    ```ts theme={null}
    const signed = await agent.buildOwnerInvitation({
      baseUrl: "https://api.acme.dev",
      recipient: { type: "email", value: "you@example.com" },
    });
    await fetch(signed.url, { method: signed.method, headers: signed.headers, body: signed.body });
    ```

    The account runs unclaimed until a human claims it; the agent's signature alone can never bind ownership.
  </Step>
</Steps>

A full, runnable version is [`examples/cli`](https://github.com/AFAuthHQ/typescript-sdk/tree/main/examples/cli); [artidrop](https://artidrop.ai)'s CLI is a live, deployed example.

## Pick your attestation mode

This is the one decision that shapes the user experience — and it's **yours to make by economics**. Both modes are first-class:

* **`attested_only`** — a human links once at the trust attestor before the agent gets anything. Choose it when your **free tier is expensive** (compute, storage, inference — anything costly you give away per user): you want a real person on the hook before granting it, so a flood of throwaway keys can't drain you. The CLI's first signup then includes the one-time `onLink` approval (reused across every AFAuth service thereafter).
* **`optional`** — the agent signs up with just its key: no attestor, no browser. Choose it when **free resources are cheap** and content moderation + graded rate limits are enough anti-abuse. You can *grade trust* — give unclaimed keys a lower quota/rate than claimed, human-owned accounts.

`signup()` handles both, so your CLI code is identical either way — only whether `onLink` fires changes. The mode is a server-side switch; see [Accept AFAuth](/quickstart-service) and [Attestation](/concepts/attestation).

## The login ladder — and no merge

A distributed client usually wants three tiers, and the cleanest mental model is a ladder the user climbs only when they want more:

| Tier          | How                                                               | What it is                                               |
| ------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
| Throwaway     | `acme publish --anonymous`                                        | keyless, unlinkable; can't be managed                    |
| Guest / agent | default (`optional`) or after the one-time link (`attested_only`) | a real identity that owns and manages its own work       |
| Account       | `acme login`                                                      | a human-owned account: dashboard, recovery, cross-device |

Make `login` the **single** account verb, and have it fork:

* **Claim** — turn the current identity into an account *in place* (your existing owner-invitation/claim ceremony). Keeps everything.
* **Sign in to an existing account** — switch this machine to it. The identity's prior work **stays with the key** — it is **not** merged.

**Don't build account-merge.** Every system that tried regrets it — [Firebase Anonymous Auth](https://firebase.google.com/docs/auth/web/anonymous-auth) throws `credential-already-in-use` and refuses; consumer apps warn "guest progress may not transfer." Your claim ceremony already enforces this: claiming into an email that already has an account returns a 409 — that *is* the guard. Make the user choose at the fork; don't reconcile two pools of data.

## Logout: go dormant, don't delete

The ladder has a step down that most clients get wrong. What should `acme logout` do to a **self-provisioned** identity?

The wrong answer is `rm ~/.afauth/key.json`. That key is **shared** across every AFAuth-aware tool on the machine, and it **owns** the guest's work — delete it and you orphan their artifacts *and* sign their other tools out. Logging out is about no longer *using* an identity, not *destroying* it. **Possession ≠ use.**

So make `logout` write a **dormant marker**, not erase anything:

* Drop any stored bearer key and persist a `signed_out` flag. The client now resolves to **anonymous** — it signs nothing — until an explicit `login`.
* Leave `~/.afauth/` untouched. `acme login --afauth` lifts the marker and the same did:key is active again, intact, still owning its artifacts.

Resolution becomes a short, ordered ladder:

| Order | Resolves to | Trigger                                         |
| ----- | ----------- | ----------------------------------------------- |
| 1     | anonymous   | `--anonymous` (per-call escape hatch)           |
| 2     | bearer      | explicit key arg → `$ACME_API_KEY` → stored key |
| 3     | anonymous   | sticky `logout` marker                          |
| 4     | afauth      | default — sign as the did:key                   |

Bearer sits **above** the logout marker on purpose: an env key in CI must still authenticate even on a box where someone ran `logout`. For the same reason, when `logout` runs with `$ACME_API_KEY` set, *say so* — you can't unset their environment, and that key keeps forcing bearer auth until they do.

## Don't rebrand the plumbing

The user knows *your* product, not AFAuth:

* The account verb is `acme login` (or fold it into your flow) — **never** `afauth init`. Sending a user to a second, unfamiliar tool is friction at the worst moment.
* Don't mirror AFAuth's subcommands (`acme trust link`, …). Fold the human step into `login`/`signup`; the user's vocabulary stays at *your* verbs.
* Name "AFAuth" at most once, as reassurance. The one unavoidable AFAuth-branded moment is the trust attestor's approval page — and only for `attested_only` services. Pass a clear `label` so it shows what's being linked.

Power users who know AFAuth are served for free: because you reuse the shared `~/.afauth/`, `afauth trust status` / `forget` operate on your CLI's identity with no extra surface from you.

## Notes & gotchas

* **Pin `@afauthhq/agent@^0.6.1`.** (0.6.0 fails to load on a clean install — it re-exported a helper from a `@afauthhq/core` that hadn't shipped it.)
* **Signing multipart uploads:** `fetch` serializes a `FormData` with a boundary you can't see up front, so a pre-computed Content-Digest won't match. Serialize it yourself first, then sign over the bytes:
  ```ts theme={null}
  const probe = new Request(url, { method, body: formData });
  const bytes = new Uint8Array(await probe.arrayBuffer());
  const headers = await agent.signRequest({ method, url, body: bytes });
  await fetch(url, { method, headers, body: bytes }); // + probe.headers.get("content-type")
  ```
  Signing JSON bodies needs none of this — pass the JSON string straight to `signRequest`.
* **Auth resolution** in your request layer: an explicit key → bearer; else a stored/env key → bearer; else sign with the did:key; `--anonymous` → no auth.
* **Tests:** point `$AFAUTH_HOME` at a temp dir so signing never touches a developer's real `~/.afauth`.

## Next steps

<CardGroup cols={2}>
  <Card title="Accept AFAuth on your service" icon="server" href="/quickstart-service">
    The backend half — verify signed requests, host the claim page, pick your attestation mode.
  </Card>

  <Card title="@afauthhq/agent" icon="cube" href="/sdk/typescript/agent/overview">
    `signup()`, the `/node` persistence helpers, and the signing surface.
  </Card>

  <Card title="Attestation" icon="shield-check" href="/concepts/attestation">
    `attested_only` vs `optional` + graded trust — and when each fits.
  </Card>

  <Card title="Invite and claim" icon="user-check" href="/guides/invite-and-claim">
    The deferred human-ownership ceremony your `login` claim path drives.
  </Card>
</CardGroup>
