CLI
Review and approve the payments a guard is holding, from a terminal.
AgentVeins never asks anyone for approval. The guard records that a payment needs a person and enforces the answer; carrying the question to someone is a separate job, because who approves what belongs to your organisation rather than to an SDK.
@agentveins/cli is a worked example of that job — a person at a terminal.
npm install -g @agentveins/cliThe loop
An agent's payment crosses the approval threshold and comes back blocked. The agent moves on to other work. Later, somebody looks:
veins pending log verified — 4,182 entries
pending approvals — 2
1 pricewatch-eu → "api.pricingdata.io"
12.000000 USDC "Q3 historical basket, DE+FR"
3 attempts, last 6m ago audit 8f21c4a9-…
2 pricewatch-eu → "api.retailfeed.com"
7.500000 USDC "competitor SKU refresh"
1 attempt, last 22m ago audit b904de17-…veins approve 1 --ttl 30mThe agent's next attempt on those exact terms settles. The second row keeps waiting.
It keeps no state
There is no queue to fall out of step with the guard. Every blocked attempt is already in the audit log with the agent, vendor, amount and reason, so the log is the queue, and each entry's auditId is the request identifier a person can quote back.
Two consequences worth knowing:
Retries fold into one row. An agent that retries on a schedule writes one entry per attempt. A person is being asked one question, not five, so the attempts collapse and the count is shown instead — 3 attempts is information about urgency, not three decisions owed.
Rows an approval already covers disappear. The same judgment the guard applies decides that: unspent and unexpired counts, anything else does not. Showing a row as satisfied when the guard would still refuse it would send an operator away believing they were done.
Verification
Approving against an unverified log
Pass --verify and the CLI checks the log's signatures before reading it, and refuses to approve against a log that fails. Without it, the tool trusts a file it cannot prove is intact.
It matters when the log and the approval store are separated — the store behind its own access control, the log a file, or a log shipped from another machine. An attacker who can forge log entries can otherwise fabricate a plausible request and have a real person authorise terms of the attacker's choosing.
It matters less when both sit on one box under the same permissions: whoever can forge the log can write the approval store directly and skip the human entirely.
The public key has to come from somewhere the attacker does not control. Reading it from the log would check the log against itself and prove nothing, so one piece of out-of-band trust is inherent rather than an oversight.
Configuration
A veins.config.json in the working directory or any parent supplies the same options, so a service directory carries its own paths and an operator types no flags:
{
"log": "./audit.jsonl",
"approvals": "./approvals.json",
"verify": "/etc/pricewatch/operator.pub.pem",
"ttl": "30m"
}Flags beat the file, which beats the defaults.
Relative paths resolve against the file that declared them, not your working directory. The file is found by walking up, so resolving against the cwd would make one config mean two different logs depending on where the operator happened to be standing.
An unknown key is an error, not something ignored. A misspelled verfiy quietly dropped would leave an operator believing every approval had been checked against a signed log when none had been — the assurance the tool exists to give, absent and unremarked. A --config pointing at a file that is not there fails for the same reason: falling back to defaults would run under a configuration nobody chose.
Commands
veins pending | list what is waiting on a person |
veins approve | pick a row and grant it |
veins approve 2 --yes | grant the second row without prompting |
veins help | the options, and what running without --verify costs |
| Option | Notes |
|---|---|
--log <path> | audit log to read. Default ./audit.jsonl |
--approvals <path> | approval store to write. Default ./approvals.json |
--ttl <duration> | how long a grant stands. Default 15m, maximum 7d |
--verify <path> | ed25519 public key, PEM. Checks the log's signatures first |
--config <path> | use this config instead of searching |
--yes, -y | skip the confirmation prompt |
--ttl refuses a bare number. 15 could mean minutes or seconds, and the two differ by a factor of sixty on how long an agent holds permission to move money. It caps at seven days, because an approval that outlives the conversation that produced it is the thing approvals exist to prevent.
--yes without a row number is refused rather than choosing for you. It costs a rerun; guessing costs a payment nobody picked.
What it grants
An approval authorises one agent, one vendor, one exact amount, once, until it expires. Granting twice authorises twice — a person who approves the same payment on two occasions has made two decisions, and the store keeps them apart.
The grant is written through ApprovalStore.grant(), the same interface the guard reads. Nothing about the CLI is privileged: an approvals UI, a Slack action, or a row inserted by an on-call script are all doing exactly what this does.
Seeing it work
The demo in the main repository stops and waits for a real person:
npm run demo -- --hold --reset # the agent is blocked, and the demo exits
cd examples/demo
veins approve 1 # veins.config.json supplies the rest
npm run demo -- --hold # the agent retries, and settlesRun it a third time and the payment is blocked again, because the approval was spent. Two processes, one store, and a person in the middle deciding.