The official CLI for the AGLedger API: change control for AI agents. Agent memory, approvals, audit trail, and notifications: one API, one signed ledger, self-hosted.
A thin cover over the API. The CLI passes your request straight through to the API and forwards the response: no hand-coded per-endpoint wrappers, no flag-to-body translation, no drift. Every AGLedger API route is reachable via agledger api <METHOD> <path>.
Learn more
- agledger.ai: what AGLedger is and who needs it
- How it works: the record, completion, and verdict lifecycle
- Glossary: canonical definitions of Record, Completion, SCITT Receipt, Verdict, Settlement Signal
- API reference: every endpoint the CLI covers
- Documentation: installation and integration guides
npm install -g @agledger/cliexport AGLEDGER_API_KEY=agl_agt_...
export AGLEDGER_API_URL=https://your-agledger-instance
# Check health, identity, scopes, and get the quickstart workflow
agledger discover
# List Record types
agledger api GET /v1/schemas
# Create a record (raw JSON body). Types are customer-registered; a fresh org
# is seeded with `notarize-generic-v1` (and 3 other editable samples).
# `criteria` is validated against the JSON Schema you registered for the type.
agledger api POST /v1/records --data '{
"type": "notarize-generic-v1",
"criteria": { "summary": "summarize Q3 filings" }
}'
# Or build the body with typed fields. Use -f for a value that must stay a
# string: the Server does not coerce a JSON body, so an all-digit identifier
# passed with -F is sent as a number and refused.
agledger api POST /v1/records \
-F type=notarize-generic-v1 \
-F criteria.summary='summarize Q3 filings' \
-f externalTaskId=4821
# A type with a completion phase, such as the seeded principal-gate-generic-v1,
# takes a Completion once the record is ACTIVE; autoActivate gets it there on
# create. Note the `id` it returns.
agledger api POST /v1/records \
-F type=principal-gate-generic-v1 \
-F criteria.summary='deliver 500x copper wire' \
-F autoActivate=true
# Submit a completion. The principal then renders a Verdict (accept / reject)
# on the Completion; use the route documented in the API
# (see `agledger api GET /openapi.json`).
agledger api POST /v1/records/<record-id>/completions \
--data '{"evidence":{"summary":"delivered 500x copper wire","evidenceUrl":"https://orders.example.com/CW-500"}}'- Zero drift. When the API adds, renames, or removes a route, the CLI keeps working, no code change required.
- One mental model. The API docs are the CLI docs. What you read in the OpenAPI spec is what you type.
- Every API route on day one. You get full parity, not a hand-picked subset.
| Flag | When to use |
|---|---|
--data '{...}' |
Agent-friendly: one JSON string |
--input file.json |
Complex payloads; reuse files |
--input - |
Pipe JSON from stdin |
-F key=value (repeatable) |
Shell-friendly; typed (true/false/null/numbers); nested via a.b=v; arrays via arr[]=v; JSON literals via k={...} / k=[...] |
-f key=value (repeatable) |
Same, but the value is taken verbatim as a string |
Merging order (low → high): --data → --input → -F/-f → --query. Later sources override earlier keys.
The Server does not coerce the fields of a JSON body, so a field declared
string refuses a number. publisher, platformRef, projectRef,
externalTaskId and correlationId are plain strings that carry identifiers
minted by other systems, and those are frequently all digits:
agledger api POST /v1/records -F type=notarize-generic-v1 -F criteria.summary=x -F externalTaskId=4821 # sends 4821, refused
agledger api POST /v1/records -F type=notarize-generic-v1 -F criteria.summary=x -f externalTaskId=4821 # sends "4821"Reach for -f rather than quoting. Shell quotes that survive into the value
(-F externalTaskId='"4821"') reach the Server as a string with the quote
characters inside it, and the Server accepts that: the Record is notarized,
signed and immutable, with an identifier no other system will match.
agledger verify checks a Record's audit export with no network and no API
key: the hash chain, every COSE_Sign1 envelope signature (Ed25519 or ES256),
and the bindings between the signed bytes and the columns beside them.
# Save a Record's audit export
agledger api GET /v1/records/<record-id>/audit-export > audit-export.json
# Verify it against a vault key you pinned
agledger verify audit-export.json --trust-anchor sha256:<digest>The keys an export embeds, and the ones GET /v1/verification-keys serves,
come from the Server's database, so a key written into the database alone
would verify against them. --trust-anchor takes the SPKI digest of a vault
key you took out of band (the installer prints the first vault key's) and walks
the signed key statements the export carries from it:
PASSmeans the chain verifies, every key is linked to your anchor, and at least one signature verified under an anchored key.VERIFIED, NOT ANCHOREDmeans nothing failed, but it is not a trusted verdict. Either no--trust-anchorwas given, so nothing links the keys to one you trust (keyTrust.statusisno_anchor), or the anchor was walked but no signature verified under a key it anchors, as for an export of unsigned history (no_anchored_signature). It exits 0, and--jsonoutput carriesverdict: "unanchored"for both. Treat it as unverified provenance, not as a clean pass.FAILexits 1. An entry signed by a key the walk does not reach isCHAIN_SIGNING_KEY_UNANCHORED; a key statement that does not hold isKEY_STATEMENT_INVALID,KEY_CLOSURE_INVALIDorCHAIN_KEY_WINDOW_DRIFT. Those are listed underkeyTrust.findings, and arebrokenAtat position 0 only when the chain itself is intact; when the chain also breaks,brokenAtis the chain failure.
--distrusted-key sha256:<digest>[@<RFC 3339 instant>] mirrors the operator's
VAULT_DISTRUSTED_KEYS and needs --trust-anchor: what such a key signed from
that instant counts for nothing, and a key statement it signed counts for
nothing at any instant, because the write times an export carries are not
signed. A dated --distrusted-key may name a key also given to
--trust-anchor, as the Server's key-compromise runbook does: the pin then
vouches for what the key signed before the instant. An export accounts for
nothing a distrusted key signed; such entries fail here, and only a full dump
verified with @agledger/verify lists them as accounted for. An API 2.0
export lists a key the Server's VAULT_DISTRUSTED_KEYS names with
distrustedFrom, the entry's instant, and where that instant is earlier than
the retirement the key's closures sign, lists the key retired at it. A run
not given the same entry still fails CHAIN_KEY_WINDOW_DRIFT on that window
(KEY_CLOSURE_INVALID where no closure retires the key), but the finding names
the entry, as --distrusted-key sha256:<digest>@<distrustedFrom>, and says so
when the entry you gave carries another instant; one at an earlier instant
draws no finding on the window and is a Key note:, though entries the key
signed after that instant still fail. The listing is the Server's unsigned word, so confirm the instant
with its operator before passing that entry. Findings and notes name the flags
in human and --json output alike. --keys <file> supplies
keys (a saved GET /v1/verification-keys response works as is, and its
statements are walked too), --require-supplied-keys refuses the export's
embedded keys, and --agent-keys <file> re-verifies the agent signatures an
OIDC cert sealed on the chain. --trust-anchor and --distrusted-key are
given once per key. A malformed value, a distrusted key named twice,
--distrusted-key without --trust-anchor, a pinned key distrusted with no
instant (the Server refuses to start with that pair), and a
file that does not exist are each a usage error (exit 2), refused before the
export is read. A --keys file whose shape or key window (RFC 3339) is wrong
is INVALID_FIELD, exit 2. The flags,
these refusals and their messages, the headline words and the exit codes are
the same as @agledger/verify's agledger-verify and the Python
agledger-verify's, and --json output carries their verdict (trusted,
unanchored or failed).
--jsonon every command (auto when stdout is piped)--quietsuppresses output (exit code only)--dry-runonagledger apishows the request without sending--verboseon every command reports which credential was used, and each OIDC cert exchange, as JSON lines on stderr; it never prints a key, token or cert. A POST also reports theIdempotency-Keyit goes out under ({"event":"request",...})--paginateon GET follows cursor pagination and streams NDJSON- Every POST carries a generated
Idempotency-Key, so one invocation is replay-safe on its own. Retrying a call that may already have reached the Server? Pass--idempotency-keywith the first attempt's key and the Server replays the original result instead of recording the work twice. When a POST times out (exit 10,TIMEOUT) or its connection drops after the request went out (exit 9,NETWORK_ERROR), the error carries the key it sent asidempotencyKeyand the suggestion is the rerun:agledger api POST ... --idempotency-key <key>. A plain rerun mints a new key and can record the work twice. A GET has no key, and neither does a connection that was refused or never resolved, since nothing was sent - Structured errors on stderr: the CLI's own are
{error: true, code, message, suggestion, ...}; an API error passes through verbatim as the Server's RFC 9457 body, whose human-readable text isdetail - Semantic exit codes: 0 (OK), 1 (general), 2 (usage), 3 (auth), 4 (forbidden), 5 (not found), 6 (conflict), 7 (rate limit), 8 (server), 9 (network), 10 (timeout). 1 is the catch-all: an API error whose status maps to nothing more specific (a 400, for example) exits 1, as does a chain that fails
agledger verify. Read thecodefield on stderr to tell them apart, and treat any non-zero as failure rather than keying on 1 alone. NO_COLORsupported per no-color.org
agledger list-commands --json # 10 CLI-local commands
agledger help-json api --json # Schema for `api` (args + flags)
agledger discover # Health + identity + quickstart
agledger api GET /openapi.json # Full API route catalog| Command | Purpose |
|---|---|
api |
Call any API endpoint |
discover |
Health + identity + scopes + quickstart |
login |
Verify an API key (or, with --oidc, an OIDC token source) and store it in ~/.agledger/config.json (0600) |
logout |
Remove the active profile, the one named by --profile, or all of them with --all; exits 2 MISSING_INPUT when there is no such profile to remove. Removing the active profile leaves none active (activeProfile: null in the output): pick another with config use |
auth |
Check current login state and show the identity, including the OIDC cert (exit 0 when nothing is configured) |
config |
list / get / use <profile> / path |
verify |
Offline audit export verification (COSE_Sign1, RFC 9052; Ed25519 or ES256; no network). --trust-anchor sha256:<digest> anchors the keys to one you pinned; see Verifying an export offline |
docs |
Fetch the API's agent-oriented narrative (llms.txt / --full) |
list-commands |
Inventory (this list) |
help-json |
Per-command schema |
# Verifies the key against the API, then stores it under ~/.agledger/config.json (0600)
agledger login --api-url https://your-agledger-instance --api-key agl_agt_... --profile prod
# Switch the active profile; subsequent commands use its key automatically
agledger config use prod
# Run a one-off against a specific stored profile
agledger api GET /v1/records --profile prod
# Or pass credentials per-invocation via env or flags (no stored profile needed)
AGLEDGER_API_KEY=... AGLEDGER_API_URL=... agledger api GET /v1/recordsAn agent can authenticate with a token from your own identity provider instead
of an API key. The CLI runs a token source you name, exchanges the token at
POST /v1/auth/oidc/cert for a short-lived cert the Server signs, and sends
that cert as the bearer. Your operator first registers the IdP as a trusted
issuer on the Server.
The token decides which agent the cert binds to, never the CLI. An
administrator binds an agent to the token's identity in one of two ways:
PATCH /v1/agents/{id} with oidcIss and oidcSub (the token's issuer and
subject), or a claimMapping of agent_id on the trusted issuer, which reads
the agent id from a claim in the token. An issuer that auto-provisions agents
creates one for an unbound subject instead. A token that binds no agent is
refused, and the Server's recoveryHint says which binding is missing. A
token bound to a federation shadow agent (one listed at GET /v1/peer-agents)
is refused with 403 SHADOW_AGENT_CERT_FORBIDDEN: that agent authenticates on
its own Server.
| Variable | What it holds |
|---|---|
AGLEDGER_OIDC_TOKEN_CMD |
A shell command whose stdout is an OIDC JWT, run on every exchange: gcloud auth print-identity-token, az account get-access-token, vault, kubectl create token, or your own script |
AGLEDGER_OIDC_TOKEN_FILE |
A file holding an OIDC JWT, read on every exchange, such as a projected service-account token that Kubernetes rotates on disk. The Server exchanges each token once, so one file token serves one CLI run until the file rotates; for repeated runs, prefer a command that mints a new token |
AGLEDGER_OIDC_AGENT_ID |
Optional. An assertion of the agent id the token binds to, sent on the exchange. It never chooses the agent: when it names a different one, or the token binds none, the Server refuses the exchange with 403 CERT_AGENT_BINDING_MISMATCH rather than issue a cert for an agent you did not expect. Leave it unset unless you want that check |
unset AGLEDGER_API_KEY # an API key outranks a token source
export AGLEDGER_API_URL=https://your-agledger-instance
export AGLEDGER_OIDC_TOKEN_CMD='gcloud auth print-identity-token --audiences=agledger'
# Shows the account and the cert the Server issued: agent, issuer, subject, scopes, expiry
agledger auth
# Every call now exchanges a fresh token for a cert; nothing is written to disk
agledger api POST /v1/records -F type=notarize-generic-v1 -F criteria.summary='nightly reconciliation'
# Or store the token source (never a token) in a profile
agledger login --oidc --oidc-token-cmd 'gcloud auth print-identity-token --audiences=agledger' --profile workWhat the CLI does with it:
- A fresh token per exchange. The Server accepts each token id once, so the source is called again for every exchange and a token is never reused or cached.
- The key stays in memory. Each invocation generates an Ed25519 key pair, proves possession of it in the exchange, and discards it on exit. The cert and the key are never written anywhere;
login --oidcstores only the command or the file path. A token command is verified at login with one exchange; a token file is only checked to hold a JWT, because exchanging it would spend the token the next command needs until the file rotates. - Signed writes. Every request with a body carries
X-Agent-Signatureover the SHA-256 of the exact bytes sent, so the chain entry records the agent's own signature inon_behalf_of.agent_signature, not only the Server's. - Refresh. The cert is re-exchanged once half its lifetime has passed, and once more if the Server answers 401 to it, before the error is reported.
- Failures name the source. A token command that exits non-zero fails with
OIDC_TOKEN_SOURCE_FAILED(exit 3), naming the variable and carrying the command's stderr. A refused exchange fails withOIDC_EXCHANGE_FAILEDand forwards the Server's error, including itsrecoveryHint. --verboseprints which credential a command used and each exchange (cert id, agent, subject, expiry) as JSON lines on stderr. It never prints a key, token or cert.
When the work is done for a person or another party rather than for the agent itself, give the CLI the RFC 8693 delegation token your IdP's token exchange issued (it must carry an act claim naming the agent). The CLI sends it as AGLedger-On-Behalf-Of on every POST, beside its own credential, and the Server seals the delegation into the signed chain entry: bound when the CLI authenticates through an OIDC cert whose subject is the token's act, unbound on an API key.
| Variable | What it holds |
|---|---|
AGLEDGER_ON_BEHALF_OF_CMD |
A shell command whose stdout is the delegation token |
AGLEDGER_ON_BEHALF_OF_FILE |
A file holding the delegation token |
The command wins over the file. The Server never deduplicates a delegation token, so one is reused until shortly before its exp and then read again; one with no exp is read again for every request, and one already expired is refused before it is sent. If the Server refuses the delegation token, the source is read once more and the request retried. It never appears in output, errors or --verbose, which names only the variable in use.
export AGLEDGER_ON_BEHALF_OF_CMD='your-idp-token-exchange --subject alice@example.com --actor my-agent'
agledger api POST /v1/records -F type=notarize-generic-v1 -F criteria.summary='expense report for alice'Credential precedence (highest first), applied per command:
- Credential:
--api-keyflag →AGLEDGER_API_KEYenv →AGLEDGER_OIDC_TOKEN_CMD→AGLEDGER_OIDC_TOKEN_FILE→ stored profile (--profile <name>, else the active profile), whose API key or OIDC token source is used.AGLEDGER_OIDC_AGENT_IDoverrides a profile's stored agent id (an assertion, as above). - Timeout:
AGLEDGER_TIMEOUT, in seconds (default 30), is how long each API response is awaited. It must be a positive number up to 2147483, or the call exits 2CONFIG_ERRORnaming the variable. A POST that times out may still have been applied: see theidempotencyKeyitsTIMEOUTerror carries, above. - API URL:
--api-urlflag →AGLEDGER_API_URLenv → stored profile URL. There is no default: AGLedger is self-hosted, so a call with no URL from any of those three sources exits 2 withCONFIG_ERRORrather than guessing a host. A URL that is not an absolutehttporhttpsURL (not-a-url, orlocalhost:3000with no scheme) also exits 2 withCONFIG_ERROR, naming where it came from. The URL is checked before the credential, so a call with neither exits 2CONFIG_ERROR, not 3AUTH_REQUIRED. The one check that comes earlier is a--profilenaming no stored profile, which exits 3AUTH_REQUIREDand says the profile was not found, on every command butlogin, whose--profilenames the profile it writes.
So once you agledger login, plain agledger api ... calls authenticate from the stored profile with no flags or env. --dry-run echoes the resolved auth (URL, source, masked key, or the name of the OIDC token source, which it does not run) so you can confirm which credentials a call would use without sending it; when no URL is configured it reports apiUrl: null and names the error the real call would raise.
Agent keys (agl_agt_*) and admin keys (agl_adm_*) are both accepted; the API routes them appropriately.
- Node.js >= 24.0.0
- A running AGLedger API instance
Proprietary. Copyright (c) 2026 AGLedger LLC. All rights reserved. See LICENSE.