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-admin | vault | |
|---|---|---|
| Runs | in the server image, against /data | on a keyholder's own machine |
| Holds | nothing secret | the private key |
| Can unwrap a secret | no | yes |
| Installed by | docker exec | curl -fsSL https://agentics.dk/install/vault.sh | bash |
vault-admin — the operator toolShips 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>
| Command | Purpose | Server may be running |
|---|---|---|
owners | List owners | yes |
vaults <owner> | List an owner's vaults | yes |
items <owner> <vaultId> | List a vault's items | yes |
audit verify <owner> | Verify the whole audit hash chain | yes |
audit head <owner> | Print the current sequence number and chain head | yes |
audit dump <owner> | Print every audit entry as JSON lines | yes |
version | Print the build version | yes |
owner add <owner> --subs a,b [--label L] | Create an owner bound to OIDC subjects | no |
reindex <owner> | Rebuild index.json from the records on disk | no |
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 clientThe 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.
| Command | Purpose |
|---|---|
vault init | Claim 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 / whoami | Sign in to a server, forget tokens, show the enrolment |
vault verify --recovery | Re-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.
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
--secret-key to type the old one instead and keep
both.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.
--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.
| Command | Purpose |
|---|---|
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=PATH | Store a new version |
vault item versions <vault> <item> | List an item's history, purged entries included |
vault keyholders | List the keys this device trusts |
vault keyholder show | Print 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.
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.
| Command | Purpose |
|---|---|
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
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.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.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.These are the commands that remove something, and they are shaped so that a removal always leaves a record behind.
| Command | Purpose |
|---|---|
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…> --yes | Destroy 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.--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.
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.
| Command | Where | Purpose |
|---|---|---|
vault agent add --name "<what it is>" [--subject <sub>] | owner | Enrol 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 host | Pin the owner, claim the token, write the identity file |
vault agent approve <agentId> --fingerprint <fp> | owner | Confirm the fingerprint and activate |
vault agent grant <vault> <item> --agent <id> --purpose "<why>" | owner | One item, with bounds |
vault agent whoami | agent host | What this host is enrolled as |
vault agent grants | agent host | What this host may read, why, and until when |
vault agent run --vault <id> --item <id> --env NAME=FIELD -- <cmd> | agent host | Run a command with the fields in its environment — the value never reaches the caller |
vault agent read <vaultId> <itemId> --field <k> | agent host | Read it — ids only, an agent may not list a vault |
vault agent skill init | either | Write the agent-facing instructions into this repository |
vault agent policies <vault> <item> | owner | What each agent may do, and how much of it is left |
vault agent revoke --agent <id> [<vault> <item>] | owner | Withdraw one policy, or the whole agent |
What that ceremony is actually doing:
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.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.)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.401 on every call that would have
used it. An older server answers the release carried no grant.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.--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.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.--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.