Agent Vault CLI reference

The vault-admin operator tool and the vault keyholder client.

Agent Vault has two command-line surfaces, and the split between them is the product's security boundary rather than a packaging decision.

vault-adminvault
Runsin the server image, against /dataon a keyholder's own machine
Holdsnothing secretthe private key
Can unwrap a secretnoyes
Installed bydocker execcurl -fsSL https://agentics.dk/install/vault.sh | bash

vault-admin — the operator tool

Ships inside the server image and reads the same volume the server writes, so it can never drift from the on-disk format. It contains no cryptography: it can list what exists and prove the audit chain is intact, and it cannot read a secret.

docker exec vault vault-admin --data /data <command>
CommandPurposeServer may be running
ownersList ownersyes
vaults <owner>List an owner's vaultsyes
items <owner> <vaultId>List a vault's itemsyes
audit verify <owner>Verify the whole audit hash chainyes
audit head <owner>Print the current sequence number and chain headyes
audit dump <owner>Print every audit entry as JSON linesyes
versionPrint the build versionyes
owner add <owner> --subs a,b [--label L]Create an owner bound to OIDC subjectsno
reindex <owner>Rebuild index.json from the records on diskno

The last column is not a caution, it is enforced. Reading opens the volume without its process lock — atomic renames mean a reader always sees a whole file — so inspecting a live deployment never requires taking it down. Writing takes the lock the running server holds, because two writers would fork the audit chain; a writing command run against a live volume fails with a message telling you to stop the server.

Since docker exec needs a running container, a writing command runs in its own:

docker run --rm -v vault-data:/data <image> \
  vault-admin --data /data owner add acme --subs <oidc-sub>

--subs is required on owner add. An owner with no bound subject is unreachable through the OIDC plane and would exist only to be adopted by whoever asked for it first.

reindex is the repair path after a crash between a record write and its index update. The index is a cache; the records are the truth, and repair only ever runs in that direction.

Nothing in this table can read a secret, including audit dump: the log records that an item was created or released, never what it contained.

vault — the keyholder client

The only component that can unwrap a secret, and deliberately the last thing built: everything before it exists so that the server never needs to be trusted with what this client holds.

curl -fsSL https://agentics.dk/install/vault.sh | bash     # macOS, Linux
irm https://agentics.dk/install/vault.ps1 | iex            # Windows

Two flags are global and are read from the environment as well: --server (VAULT_SERVER) and --owner (VAULT_OWNER).

Neither is usually needed. --server resolves flag → environment → the server this device is enrolled at → https://vault.agentics.dk, and the fallback is announced on stderr. --owner resolves flag → environment → this device's enrolment → whichever namespace GET /api/owners says the signed-in account is a member of. Both refuse to guess when there are two: a wrong server means talking to a vault you did not name, and a wrong namespace means enrolling a device into someone else's tenant.

On the hosted vault, an account with no namespace is asked to name one and vault init claims it — signing in and installing the client is the whole of setup. A self-hosted server has that off by default and says so instead: there, namespaces are created by whoever runs the box.

GET /api/owners is the only route that says an owner name without being given one, and it lists memberships, never tenants: a namespace appears only if your own subject is written into its owner.json. Being in VAULT_ADMIN_SUBS is deliberately not enough — an operator enumerates the deployment with vault-admin against the volume, not over HTTP. So an unauthenticated or unrelated caller still cannot learn that a tenant exists, which is the property the uniform 404 everywhere else exists to protect.

Enrolment

CommandPurpose
vault initClaim a namespace if you have none, enrol this device, print the Emergency Kit, run the recovery drill
vault device add --holder <id>Restore an account onto a new device from the Emergency Kit
vault login / logout / whoamiSign in to a server, forget tokens, show the enrolment
vault verify --recoveryRe-run the recovery drill against the card in the drawer
vault recover --holder <id>Forgot the passphrase: open with the Recovery Key Card and set a new one

vault init prints two artefacts — an Account Card and a Recovery Key Card — and they are meant to be stored apart. The Account Card plus the passphrase is the everyday way back onto a new machine (vault device add).

The Recovery Key Card is not a second half of that pair — it is the break-glass path, and it is sufficient on its own. Store it accordingly.

A forgotten passphrase

The passphrase itself cannot be recovered: nothing stores it, only a key derived from it. The account key can be, because init wraps it twice — once under the passphrase and once to the Recovery Key Card. vault recover walks the second path and rewrites the first:

vault recover --owner acme --holder kh_…      # recovery key read without echo
  • The old passphrase stops working when this succeeds. The new entry is written at a higher epoch, and the server refuses to go back down, so the forgotten passphrase is dead rather than merely unknown.
  • Every secret survives — the account key is unchanged, so nothing has to be re-wrapped.
  • On a device that is still enrolled, the Secret Key is reused and the Account Card stays valid. On a fresh machine a new Secret Key is minted and printed as a new Account Card: the old card is void and other devices on that holder stop unlocking. Pass --secret-key to type the old one instead and keep both.

Naming things

The binary is named after its top noun, so that noun is implicit: vault list and vault create are about vaults. Everything below a vault keeps the full noun verb form — vault item create, vault invite list, vault agent grant.

Every command that works on a vault or an item takes them as arguments, in the order its usage line shows, and by the name you gave them:

vault create work
vault item create work oauth-client --title "Ford Connect"
vault item show work oauth-client --reveal

--vault and --item still work and mean the same thing, so the two spellings mix freely. Ids (vlt_…, itm_…) work anywhere a name does, and are what a script should use: a name is resolved by asking the server, an id is not. Two vaults may share a name; a command that finds two refuses rather than picking one. The prefixes are what tell the two apart, so a vault cannot be named vlt_… and an item cannot be named itm_….

One place the difference is more than convenience. vault grant and vault agent grant wrap an item's key for somebody else, and an item's cleartext name is not covered by any signature — the manifest binds the id. So when either of those is given a name, it prints the item's sealed title, which is encrypted to your account and not the server's to forge, and waits for you to confirm. With no terminal there is nobody to show it to, and the command asks for the id instead. vault agent read runs as an agent, whose token cannot list a vault, so it takes ids and nothing else.

Keeping secrets out of your shell history

--field key=value puts the value on the command line, where your shell records it and a screen recording keeps it. --field key — with no = — asks for the value when the command runs, without echoing it:

vault item create work oauth-client --field client_id --field client_secret

Both forms mix, prompts come in the order the flags were given, and the values are asked for before anything is created — so a typo costs a retry rather than a half-made item. It reads from a terminal only: piping into a prompted field is refused, because a value taken from whatever happened to be on stdin is not a value anyone chose. Scripts pass key=value.

A prompt is one line by construction, so it cannot carry the secrets that are most worth vaulting: a PEM private key, an SSH key, a service-account JSON. --field-file key=PATH reads one from a file instead, bytes as they are — trailing newline included, because a PEM's final newline is part of what every tool expects to be handed back:

vault item create work github-app \
  --title "GitHub App si14-x" \
  --field app_id \
  --field-file private_key=./si14-x.2026-09-06.private-key.pem

The file is read at the same moment a prompt would be asked — before the account is unlocked — so a mistyped path also costs a retry rather than a passphrase. It refuses an empty file, a file over 1 MiB, and anything that is not valid UTF-8 (base64-encode a binary secret first: a field is text on the wire, and the JSON encoder would silently replace the bytes it cannot represent). - is not stdin here either.

The same applies to vault deposit, which is the command someone outside your organisation runs — and it is the case the file form was most needed for, since a certificate or key is exactly what a stranger gets sent a link to deposit.

Everyday use

CommandPurpose
vault list / vault create <name>List and create vaults
vault item list <vault>List a vault's items
vault item create <vault> <name> [--title T] [--field k] [--field-file k=PATH]Create an item and store a secret
vault item show <vault> <item> [--reveal]Read the newest secret
vault item set <vault> <item> --field k=v | --field-file k=PATHStore a new version
vault item versions <vault> <item>List an item's history, purged entries included
vault keyholdersList the keys this device trusts
vault keyholder showPrint this device's three public values, to send to a vault owner
vault keyholder add --id … --recipient … --signpub …Vouch for someone else's key on your vault
vault grant <vault> <item> --to <keyholder>Give another keyholder access
vault verify <vault> <item>Verify the item's signed history

Nothing prints a secret without --reveal. The command that gets run on a shared screen is vault item show.

Asking someone else for a secret

The invite flow is the reason this product exists. It lets a person with no account, no key and none of our software put a credential into a vault they cannot read.

CommandPurpose
vault invite create <vault> <item> --prompt "…"Mint a link
vault invite list <vault> <item>List an item's invites and their state
vault invite revoke <vault> <item> --invite <id>Stop the server accepting a deposit
vault deposit <link> --field password=…Fill in a secret you were sent a link for
vault item pending <vault> <item> [--reveal]Review what was deposited
vault item accept <vault> <item> --version <id>Make a deposit the current secret

Three properties are worth knowing before you send one of these links:

  • The link's fragment never reaches the server. Everything after # — the invite secret, the item's public key, its fingerprint — stays in the depositor's client. The server holds only public halves derived from that secret, so a dump of its volume yields nothing that can redeem an invite or forge a deposit.
  • Read the fingerprint out separately. invite create prints it. A depositor who compares it against what their client shows has verified that the key they are sealing to is yours, over a channel the link did not travel on.
  • A deposit is not the secret until you accept it. Anyone holding the link can write, so deposits land as versions the item's signed history does not name. vault item pending verifies each one against a key committed inside the item's own sealed metadata; vault item accept is what makes one current. An unverified deposit is never printed, with or without --reveal.

Taking access away, and taking data away

These are the commands that remove something, and they are shaped so that a removal always leaves a record behind.

CommandPurpose
vault item state <vault> <item>Key generation, revocation count, whether a rotation is due
vault revoke <vault> <item> --from <keyholder>Tombstone, remove their grant, rotate the key behind them
vault item rekey <vault> <item>Rotate an item's key with nobody removed
vault item rm <vault> <item>Mark an item deleted — nothing is destroyed
vault item restore <vault> <item>Undo the above
vault item purge <vault> <item> <versionId…> --yesDestroy version bytes for good

What each of them really does:

  • vault revoke is three operations, and the first one is a record. A signed revocation tombstone is written; only then will the server delete the grant; only then does the key rotate. The tombstone names the keyholder and the key epoch they were removed at, and it is never rewritten — so a year later there is something on disk saying that access existed and was withdrawn, which a bare deletion would not leave.
  • A rotation is not a retraction. It gives the item a new key, re-seals its metadata and current secret under it, re-wraps for everyone who stays, and closes every live invite — because an invite minted under the old key would otherwise seal tomorrow's secret to the key you just retired. What it cannot do is take back a key someone already unwrapped, and it does not change the password in the system the secret is for. Do that too. The command says so.
  • --no-rekey is allowed and visible. The item is then in the state the server stamps X-Vault-Rotation-Required on, and vault item state says rotation REQUIRED until you finish the ceremony.
  • vault item rm is a flag. Every version, grant and manifest link stays exactly where it was; the item is hidden from vault item list, closed to writes, and its invites stop working. restore is one call, and brings the invites back with it.
  • vault item purge is the only irreversible command in this CLI. Without --yes it prints what it would destroy and stops. With it, a signed purge record naming the versions is stored before a byte is removed, and afterwards those version ids still list and still resolve — as a 410 naming the purge, not as a 404. That is the difference between this product destroying a secret and this product losing one.

Nothing in this vault is ever deleted as a side effect of anything else.

vault deposit is the one command that needs no account, no passphrase and no enrolment — only the link. Pass --server when the link points at a deposit page rather than at the API.

Invites expire (seven days by default, thirty at most), carry a use budget (--max-uses, ten at most), and can be revoked. A revoked invite is indistinguishable from one that never existed, to everyone including its holder.

Giving an agent access

An agent is a keyholder like any other — the same registry, the same owner-signed record, the same manifest. What differs is that nobody is watching the machine it runs on, so it never gets the item key outright.

CommandWherePurpose
vault agent add --name "<what it is>" [--subject <sub>]ownerEnrol an agent, print a one-shot token
vault agent enrol --agent <id> --owner-holder <id> --owner-sign-pub <k> --owner-recipient <age1…> --anchor <v> --token <t> [--client-id <id> --client-secret <s>]agent hostPin the owner, claim the token, write the identity file
vault agent approve <agentId> --fingerprint <fp>ownerConfirm the fingerprint and activate
vault agent grant <vault> <item> --agent <id> --purpose "<why>"ownerOne item, with bounds
vault agent whoamiagent hostWhat this host is enrolled as
vault agent grantsagent hostWhat this host may read, why, and until when
vault agent run --vault <id> --item <id> --env NAME=FIELD -- <cmd>agent hostRun a command with the fields in its environment — the value never reaches the caller
vault agent read <vaultId> <itemId> --field <k>agent hostRead it — ids only, an agent may not list a vault
vault agent skill initeitherWrite the agent-facing instructions into this repository
vault agent policies <vault> <item>ownerWhat each agent may do, and how much of it is left
vault agent revoke --agent <id> [<vault> <item>]ownerWithdraw one policy, or the whole agent

What that ceremony is actually doing:

  • The agent's grant holds half a key. The owner splits a fresh key-encryption key in two, wraps one half to the agent and hands the other to the server, and seals the item key under both. An agent that keeps its grant forever has kept 32 bytes that open nothing; the server that holds the other half has 32 bytes that open nothing either. Every read is an online call the server can count, rate-limit, refuse, and ask a human about — which is exactly what a plain wrap to the agent's own key could never be.
  • An agent finds its own grants. vault agent grants lists every policy that names this host: the ids, the owner's --purpose in the owner's own words, how many reads are left, when it expires, and — when it is not usable — why. It is metadata, so it spends no release and no budget. This is what lets a human say "the credentials are in the vault" and have an agent work out the rest; without it every grant has to be dictated to the machine by hand, and dictated again when it expires an hour later.
  • vault agent run is the form to prefer for an LLM agent. read prints the value, which for an agent means the value is in its transcript, its summary and its logs — a leak with nobody attacking. run spends the same release and puts the fields in one child process's environment instead, filtering the child's output so an accidental echo prints [redacted by vault]. Same flags as the owner-side vault run: --env NAME=FIELD, --file NAME=FIELD, --prefix.
  • vault agent skill init writes the instructions down. It drops a SKILL.md into .claude/skills/vault-secrets/ (or --dir anywhere else) telling an agent to check whoami, discover with grants, use run, never print a value, and hand its human the exact vault agent grant line when it has no access. The file is compiled into the binary, so the instructions and the grammar they name cannot drift apart.
  • Revoking destroys the server's half. Anything the agent already fetched under that policy stops opening, including copies. That is the property the split exists for, and it is the one thing a normal grant cannot offer: a wrap that has been read once has been read forever.
  • The fingerprint confirmation is not optional. The server refuses to activate an agent that no owner has signed for, so a host cannot enrol itself. An agent cannot look at a screen and say "that is not the key I was shown", so the owner does it on its behalf.
  • The agent host pins you from the paste, not from the server. vault agent add prints the whole enrol line, including the owner's two public halves (--owner-sign-pub, --owner-recipient) and the --anchor that covers both. The host pins those and asks the server for nothing: it has a one-shot token and no account, so there is no authenticated call it could make, and a key the server handed out would be a key the server chose — whoever can serve a forged grant can serve the record that makes it verify. The anchor is the checksum that catches a half-pasted key before anything is written. (Before 0.1.7 enrol fetched the record instead, and answered 401 against any authenticated server. An enrol line without the two key flags came from an older owner CLI: upgrade it and mint a fresh token.)
  • Discovery needs 0.1.11 on both sides. vault agent grants is a route the server has to serve; an older server answers 404 and the CLI reports it as such. Everything else in this section works from 0.1.8.
  • Server and agent host must both be 0.1.8 or newer. A release now carries the agent's own grant, and the four routes a read needs after it — the manifest chain, the sealed metadata, the versions — accept the agent's proof. Before that the agent could obtain a key and then 401 on every call that would have used it. An older server answers the release carried no grant.
  • An agent needs a service account as well as a key. The identity file proves which host is calling; the bearer token proves which account, and a real server requires both. Create a confidential Keycloak client with service accounts enabled, pass its service-account subject to vault agent add --subject, and give the host its credentials — --client-id/--client-secret on enrol, or the VAULT_CLIENT_ID/VAULT_CLIENT_SECRET environment variables, which win when both are present because an orchestrator that rotates a secret expects the fresh one to be used. The client secret is stored inside the sealed section of the identity file; the client id is outside it, so vault agent whoami can answer "which account does this host authenticate as" without the passphrase — which is the question you ask when a read returns 401. A host that gets its token from elsewhere can set VAULT_TOKEN and store no credentials at all.
  • vault agent read takes ids, never names. Names are resolved by listing, and an agent cannot list a vault — its token opens exactly the item its policy names. Two items with one name would also need a human to say which was meant, and there is no human on that host. The owner's commands take names; the agent's do not.
  • Every policy has an expiry (≤30 days), a uses budget and a rate limit, and covers exactly one item. There is no field for a set of items. The friction of writing N policies is the feature.
  • --consent never is only accepted for a policy that dies within the hour. Anything longer goes through pks-agent-consent, one approval per release, and the approval is spent when it is used. If the consent service is unreachable, erroring, unconfigured, or too old to support single-use approvals, nothing is released.
  • A rotation closes every agent. vault item rekey and vault revoke re-wrap the humans and drop the agents, and say so. Re-wrapping an agent would hand it the new item key with no second share — every bound on its policy would go quiet at the moment you were rotating because you wanted access to stop. Re-grant deliberately afterwards.
  • The identity file is the honest limit. --protection passphrase (the default) means a copy lifted from a backup or a decommissioned VM image is useless. It does nothing about someone who has the running host: they can call the vault as the agent. The whole model is blast-radius reduction — one item, short expiry, spendable budget, a human per release — not host trust.

The install script is generated by agentics.dk from the published release record, so it always resolves the current version for your platform. Binaries are also attached to each GitHub release with a sha256sum checksums file if you prefer to verify and place them yourself.