> Markdown version of https://agledger.ai/docs/guides/cli/
> Full index of this site for AI assistants: https://agledger.ai/llms.txt

# CLI

The AGLedger CLI is a thin cover over the API. Every route the API exposes is reachable through one pass-through command, `agledger api <METHOD> <path>`, so the CLI never drifts behind the API and the [API reference](/api/) is also the CLI reference. A small set of CLI-local commands handle the things a pass-through cannot: storing credentials, cold-start discovery, and verifying an audit export offline.

Use this guide when you want to notarize from a shell, script a chain in CI, or hand an agent a terminal instead of an SDK. For request and response shapes, follow the [API reference](/api/) - this page covers which command maps to which job and how the offline path works.

Video: [AGLedger: developer quickstart with the CLI](https://www.youtube.com/watch?v=ED8phnWFEUM)

A first session with the AGLedger CLI: install, log in, discover the API, and record an action in one call.

The CLI is published as `@agledger/cli` v1.4.0 and requires Node 24 or later.

## Install

```bash
npm install -g @agledger/cli
agledger --version
```

On Linux hosts where the global npm prefix is root-owned (the NodeSource default), `npm install -g`
fails with `EACCES` - run it with `sudo`, or point npm at a user-writable prefix first
(`npm config set prefix ~/.npm-global`).

**Air-gapped install.** The CLI is a single npm package with two runtime dependencies (`@oclif/core`, `@agledger/verify-core`) and no network calls of its own beyond the API requests you make. On a connected machine, pack it and carry the tarball across:

```bash
npm pack @agledger/cli
# produces agledger-cli-<version>.tgz — copy it to the target host, then:
npm install -g ./agledger-cli-<version>.tgz
```

The `agledger verify` command runs entirely offline once installed and never contacts a registry, our website, or any AGLedger Server.

## Point the CLI at your Server

Every command resolves its Server URL and key from, in order: explicit `--api-url` / `--api-key` flags, the `AGLEDGER_API_URL` / `AGLEDGER_API_KEY` environment variables, or a stored login profile. For a single shell session, environment variables are the simplest:

```bash
export AGLEDGER_API_URL=https://agledger.example.com
export AGLEDGER_API_KEY=agl_agt_...
```

There is no default Server - point the CLI at your own instance. The CLI talks only to the URL you give it.

## Authenticate

`agledger login` verifies a key against your Server (it calls `GET /v1/auth/me`) and, on success, stores it in `~/.agledger/config.json` with `0600` permissions. Use named profiles to hold keys for more than one Server or environment on the same machine:

```bash
agledger login --api-key agl_agt_... --api-url https://agledger.example.com --profile prod
```

{/* TESTBED: validate output */}

Switch and inspect profiles with `config`:

```bash
agledger config list             # all stored profiles, marking the active one
agledger config use prod         # set prod as the active profile
agledger config get              # show the active profile (URL only, never the key)
agledger config path             # print the config file path
```

Check login state without failing - `auth` returns `authenticated: false` and exit code 0 when no key is configured, which is convenient for scripts that gate on it:

```bash
agledger auth
```

Remove a stored credential with `agledger logout --profile prod`, or `agledger logout --all` to clear every profile.

Keys are minted and rotated on the Server, not by the CLI. For how to obtain an `agl_` key or an OIDC-bound certificate, see [Authentication](/docs/guides/authentication/).

## Confirm connectivity

`agledger discover` is the cold-start call. It checks Server health and your resolved identity, and returns the quickstart workflow. Partial results come back even if one call fails, so you can tell what is reachable:

```bash
agledger discover
```

{/* TESTBED: validate output */}

## Notarize a record

Creating a record is `POST /v1/records` through the pass-through command. For a notarize-only contract type the record terminalizes in this one signed call - this is the on-ramp.

```bash
agledger api POST /v1/records --data '{
  "type": "invoice-processed-v1",
  "criteria": { "invoiceId": "INV-4815", "amount": 2400 }
}'
```

You can build the body from typed `-F` fields instead of a JSON string. Dots nest, `[]` appends to an array, and values are typed (`true` / `false` / `null`, numbers, or bracketed JSON):

```bash
agledger api POST /v1/records \
  -F type=invoice-processed-v1 \
  -F criteria.invoiceId=INV-4815 \
  -F criteria.amount=2400
```

`-F` types its values, which is wrong for an identifier that happens to be all
digits. The Server does not coerce the fields of a JSON body, so a field declared
`string` refuses a number and `-F externalTaskId=4821` is rejected. Use `-f` /
`--raw-field` to send a value verbatim as a string:

```bash
agledger api POST /v1/records \
  -F type=invoice-processed-v1 \
  -f criteria.invoiceId=4815 \
  -F criteria.amount=2400
```

Path syntax is identical and the two forms parse together, so `-F a.b=1 -f a.c=2`
builds one tree. `publisher`, `platformRef`, `projectRef`, `externalTaskId` and
`correlationId` are declared strings carrying identifiers minted by other systems,
which makes them the fields most likely to arrive all-digit.

For larger payloads, read the body from a file or stdin:

```bash
agledger api POST /v1/records --input record.json
cat record.json | agledger api POST /v1/records --input -
```

Body sources compose in this precedence (low to high): `--data`, then `--input`, then `-F` and `-f`, then `--query`. Later sources override overlapping keys, so you can keep a base file and override one field on the command line.

To see exactly what would be sent without calling the Server, add `--dry-run`:

```bash
agledger api POST /v1/records --input record.json --dry-run
```

## Retry a write without recording it twice

Every `POST` the CLI sends carries an `Idempotency-Key`, generated per invocation.
A fresh invocation therefore mints a fresh key and is a new write. When you are
retrying a call that may already have reached the Server, pass the first attempt's
key so the Server replays the original result instead of creating a second record:

```bash
agledger api POST /v1/records --input record.json \
  --idempotency-key 4f1c8ad2-93e0-4a77-b8d1-6c2e5f0a91b4
```

The key binds to method, route and body, so a retry that changes the body is
rejected rather than replaying the earlier response against a different payload.
Only `POST` is affected: the 18 routes that opt into idempotency are all `POST`,
and the header is ignored elsewhere. `--dry-run` names the key it would send.

## Submit a completion (gated contract types)

A contract type that declares a completion schema runs the gated lifecycle: create, accept, completion, verdict. The performer submits evidence as a **Completion** against the record:

```bash
agledger api POST /v1/records/<record-id>/completions \
  --data '{"evidence":{"itemsDelivered":"copper wire","quantityDelivered":500}}'
```

The principal then renders the verdict with `POST /v1/records/<record-id>/verdict` (body `{"completionId":"<id>","verdict":"accept"}`). The CLI carries these calls because it is a pass-through; the lifecycle, fields, and states are owned by the [API reference](/api/).

## List and stream records

For `GET` and `DELETE`, parameters go to the query string rather than the body:

```bash
agledger api GET /v1/records -F status=ACTIVE -F limit=50
```

To walk every page of a paginated route, add `--paginate`. The CLI follows the cursor and streams each item as newline-delimited JSON (NDJSON), one record per line, which pipes cleanly into `jq` or a file:

```bash
agledger api GET /v1/records --paginate > records.ndjson
```

## Export a chain

A record's full audit chain is exported through the same pass-through command. Write it to a file so you can verify it later, on this host or an air-gapped one:

```bash
agledger api GET /v1/records/<record-id>/audit-export > audit-export.json
```

The export is format 2.0: each entry is a COSE_Sign1 envelope (RFC 9052) over an in-toto v1 Statement, hash-chained and Ed25519-signed. These are the Signed Statements that make the chain verifiable by anyone holding the public keys, with no access to the Server.

## Verify an export offline

`agledger verify` checks an export with no network access and no API key. It recomputes the hash chain, decodes each COSE_Sign1 envelope, and verifies every Ed25519 signature against the public keys. This is the default verification path - it runs the same on an air-gapped host as on a connected one.

```bash
agledger verify audit-export.json
```

The export embeds the Server's signing public keys, so a self-contained export verifies on its own. To verify against keys you hold out-of-band instead of trusting the embedded set, supply them as a JSON map of `signingKeyId` to base64 SPKI-DER public key:

```bash
agledger verify audit-export.json --keys vault-keys.json
```

To reject any export signed by an unexpected or retired key, pin the expected key id - every entry must reference it or verification fails:

```bash
agledger verify audit-export.json --require-key-id <key-id>
```

Read the export from stdin with `-`, and emit a machine-readable result with `--json`:

```bash
cat audit-export.json | agledger verify - --json
```

A passing run reports the record id and that every entry verified; `verify` exits non-zero on any failure, so it gates a pipeline cleanly. When the chain is broken, the result names the first failing position and the failure class - for example a `CHAIN_POSITION_GAP`, a `CHAIN_HASH_MISMATCH`, a `CHAIN_LINK_BROKEN`, a `CHAIN_SIGNATURE_MISSING_KEY`, or a `CHAIN_SIGNATURE_INVALID` - so you can tell tampering from a missing key. The [audit page](/docs/operations/audit/) owns the full code table.

{/* TESTBED: validate output */}

## Output, exit codes, and errors

Every command supports `--json` (the default when stdout is piped, so output is parseable in scripts automatically) and `--quiet` (suppress output, exit code only). Errors are written to stderr as a structured object - `{ "error": true, "code", "message", "suggestion", ... }` - and API errors pass through verbatim, including the API's own `suggestion` and `nextSteps`.

Exit codes are semantic and stable across releases, which lets a script branch on the failure without parsing text:

| Exit code | Meaning |
|---|---|
| `0` | Success |
| `1` | General error |
| `2` | Usage error (bad path, method, JSON, or field) |
| `3` | Auth error (`401`, or no key configured) |
| `4` | Forbidden (`403`, missing scope or role) |
| `5` | Not found (`404`) |
| `6` | Conflict (`409`) |
| `7` | Rate limited (`429`) |
| `8` | Server error (`5xx`) |
| `9` | Network error |
| `10` | Timeout |

## Discover the rest

Because the CLI is a thin cover, the API reference is the command reference. To explore the surface from the terminal:

```bash
agledger list-commands              # the ten CLI-local commands
agledger help-json api              # arguments and flags for one command
agledger api GET /openapi.json      # the full route catalog from your Server
```

Everything beyond the CLI-local commands - records, completions, schemas, webhooks, federation, admin - is reached with `agledger api <METHOD> <path>` using the exact path and body shape in the [API reference](/api/).

---

*Command surface, flags, and the offline-verify behavior on this page are drawn from `@agledger/cli` v1.4.0. The pass-through routes (`/v1/records`, `/v1/records/{id}/completions`, `/v1/records/{id}/audit-export`) and their request and response shapes are owned by the [API reference](/api/); confirm field-level detail there against your own Server.*
