@quaivault/cli
v0.7.0
Published
Command-line client for QuaiVault multisig vaults on Quai Network
Maintainers
Readme
@quaivault/cli
Command-line client for QuaiVault multisig vaults on Quai Network.
npm install -g @quaivault/cliTwo surfaces, one core: one-shot commands for people and agents, and a TUI for watching.
Reading needs no key
No wallet, no account, no configuration. This works the moment you install:
qv vault show 0x005f2629A632962f4944d23686efDa5c160d535bThen tell it who you are — still no key:
qv use --as 0x<your-address>
qv inbox # what is waiting on you, across every vaultAdd a key only when you want to sign:
qv key import mykey --useqv key import reads a raw private key from your terminal, or takes an existing
Web3 Secret Storage (V3) keystore with --keystore geth.json — the format Pelagus,
MetaMask, Geth and ethers all export.
The commands
Reading (no key required)
qv status · qv doctor · qv inbox · qv vault show|ls|receive
qv tx ls|history|show|wait · qv balance · qv messages · qv addr check
qv activity deposits|token-transfers · qv recovery status|history
Acting on transactions
qv tx approve · qv tx unapprove · qv tx execute · qv tx cancel · qv tx expire
Proposing changes (every one of these asks your co-owners to act)
qv propose transfer|token|nft|erc1155|call|batch
qv propose add-owner|remove-owner|threshold|delay
qv propose module|delegatecall|sign-message|cancel-by-consensus
qv propose enable-recovery|disable-recovery|setup-recovery
Vaults, recovery, setup
qv vault create · qv vault mine-salt
qv recovery approve|unapprove|execute|cancel|expire|initiate
qv key import|ls|use|rm|rename|change-password|export
qv use · qv alias · qv contact · qv policy · qv watch · qv tuiEverything except approve/execute/cancel/expire is a proposal: it asks N−1
other people to act. The naming says so, because qv owner add would read like it adds
an owner when it actually asks two other people to.
Social recovery is deliberately enabled in two reviewed proposals. First enable the module configured for the active network, then configure its guardians:
qv propose enable-recovery <vault>
qv propose setup-recovery <vault> --guardian 0x… 0x… --threshold 2 --recovery-period 7d
qv recovery status <vault> --jsonrecovery status reports the module address, enabled/configured state, guardian set,
threshold, recovery period, and pending requests. In the TUI recovery pane, s opens
the applicable enable or guardian-setup flow; d proposes disabling an enabled module.
Terminal workspace
CLI 0.7.0 provides eight panes: inbox, history, activity, assets, vault, recovery,
policy, and proposals. Start with qv tui --profile <name> --as <address>.
The active profile and identity are preserved when a command opens for signing.
| Keys | Action |
|---|---|
| Tab / Shift-Tab, 1–8 | Change pane |
| [ / ] | Change vault |
| Arrows or j / k, PgUp / PgDn, Home / End | Navigate lists and scroll full details |
| /, then Enter; Esc | Filter inbox/transaction history; clear search |
| Enter | Open transaction details |
| ← / → in history | Transactions, deposits, token transfers, recovery history |
| m in history | Load the next 50 records |
| ← / → in recovery | Choose a pending recovery request |
| r, ?, q | Refresh, keyboard guide, back/quit |
Proposal forms support native and token transfers, raw and ABI-assisted calls, batches, vault administration, message signatures, vault creation, and recovery. Token forms require explicit decimals. Every proposal form includes expiry, execution delay, and an optional idempotency key. Paste calldata, address lists, or file paths normally; oversized inputs are rejected with a visible message.
Within a field, use arrows, Home/End, Backspace/Delete, Ctrl-U (clear), Ctrl-W (delete word), and Ctrl-K (delete to end). Tab/Shift-Tab changes fields. Enter advances until the last field, where it opens the command for review. The result stays visible until you press a key, so transaction hashes can be copied.
The display adapts to terminal resizing down to 40 columns × 12 rows. Kitty's
keyboard protocol is detected through Ink; unsupported terminals use standard
input. No special font or Kitty configuration is required. Mouse reporting is
left off so normal terminal text selection remains available. --color never
and NO_COLOR support monochrome terminals. Use a larger window for long tables.
Discovery and pending lists page beyond the SDK's defaults. Each is bounded at 1,000 records per role/vault, with a visible warning if that bound is reached. History loads on demand. Indexed lists are not an atomic chain snapshot; the one-shot command rereads chain state before signing. Failed reads stay visibly unavailable or stale, and delayed responses cannot overwrite another vault.
For the terminal regression suite on Linux/macOS, install pyte==0.8.2 and
wcwidth==0.8.3 in a Python environment, then run npm run test:tui-pty.
QV_TUI_PYTHON can select that interpreter; QV_TUI_CAPTURE_DIR saves synthetic
screen captures. The suite tests PTYs and protocol responses, not GUI terminal apps.
Before you sign
Every write prints a disclosure read from chain, not the indexer:
About to approve:
Transfer 100 QUAI to alice
0x8a3f9c21…7e6d
Decoded as verified
To 0x001f4e8a9b0c1d2e3f405162738495a6b7c8d781 (alice)
Value 100 QUAI
exactly 100000000000000000000 wei
Operation call
Data (none)
Approvals 1 of 2
[x] 0x00a1b2… (bob)
[ ] 0x001f4e… (alice)When the ABI is unknown, it shows the hex — laid out one 32-byte word per line, because ABI encoding is word-aligned and a padded address reads as 12 zero bytes then 20. You can pick out a recipient and an amount with no ABI at all:
Data unknown ABI — 68 bytes, showing raw calldata
selector 0xa9059cbb
[000] 000000000000000000000000001f4e8a9b0c1d2e3f405162738495a6b7c8d781
[032] 0000000000000000000000000000000000000000000000056bc75e2d63100000A decode the SDK cannot vouch for — a selector-shape guess, a supplied ABI, or a
delegatecall — is labelled as such and needs --i-understand-unverified to sign.
For agents and automation
qv inbox --json # structured, with affordances
qv tx show <v> <h> --json # includes a `verify` block to assert against
qv --schema --schema-version 1 # every command, flag and output shape--jsonis a versioned CLI-owned schema ({"schema": 1, …}). Every bigint is a decimal string in wei, never a number —Number()loses precision above 0.009 QUAI.- Exit codes are a contract.
0ok ·1failure ·2usage ·3precondition or policy ·4not executed (approved_only/timelock_started) ·5declined ·130interrupted.qv tx executeexits non-zero when the vault call failed even though the chain transaction succeeded. changedistrue/false/"unknown". Re-approving something you already approved is a no-op withchanged: false, exit 0 — so a retry after a timeout is safe.- Assert, don't trust prose:
--expect-data-hash,--expect-to,--expect-value,--expect-abi-sourceall fail closed against re-read chain state before signing. - Retry proposals deliberately: pass
--idempotency-key <stable-operation-id>. Successful broadcasts are journaled durably, and a retry with the same inputs returnschanged: false; reusing the key for different inputs fails closed. Keys are reserved before submission; an interrupted or unresolved submission blocks reuse until its chain history and local journal have been reconciled. Never work around this by inventing a new key.
A receipt timeout after submission reports BROADCAST_UNKNOWN, changed: "unknown",
retryable: false, and chainTxHash in both data and error. Look up that hash before
retrying. The SDK verifies the selected chain ID against both the read and signing RPCs
before each broadcast; misspelled network names are rejected.
Signing locks are never reclaimed based on age. After a crash, verify the recorded PID and pending transactions before manually removing the lock named in the error.
Agents may sign, within a policy
Non-interactive signing requires a policy file in the CLI configuration directory:
qv policy init # writes ~/.quaivault/policy.tomlmax_value_per_approval_wei = "1000000000000000000"
max_approvals_per_hour = 5
allow_to = []
deny_kinds = ["wallet_admin", "module_config", "recovery_setup"]
deny_delegatecall = true
require_abi_source = ["builtin"]
allow_recovery_actions = ["cancel", "expire"]There is deliberately no flag to relocate it and no environment override — a bound the caller can move is not a bound. An attended human at a terminal is not restricted by it.
The safest agent deployment is a UID or container that cannot read
~/.quaivault/keys/ and has no QUAIVAULT_PRIVATE_KEY*. The whole read surface still
works, by construction.
Security
- No telemetry. No usage analytics, no crash reporting, no phone-home. On a chain
where addresses are public and permanent, correlating one with an IP is a
deanonymisation primitive that cannot be walked back.
qv doctorprints a paste-able report when you choose to share one. - No
--private-key,--passwordor--mnemonicflag, ever./proc/*/cmdlineis world-readable and rewritingargvdoes not change it. Keys come from a keystore,--key-file,--key-fd, or a terminal. - Quai ledger only. QuaiVault is an EVM contract on the Quai ledger. Qi is a
separate UTXO ledger that executes no contracts, so a Qi address can never sign,
approve, or hold any role in a vault — and enough Qi owners brick a vault permanently.
Every place an address is committed to a role checks both its zone and its ledger,
because the two are orthogonal:
0x0081…sits in a valid zone and is still Qi. This is not a gap awaiting support; it is what the ledgers are. - Keys are stored as Web3 Secret Storage V3 via
quais— a decade-scrutinised standard, not something invented here. Files are0600in a0700directory, written atomically, and refused if they are symlinks or readable by others. KDF parameters are validated before derivation, which is what closes V3's unauthenticated-params gap. qv tuinever holds a key. It renders, and delegates every signature to a spawned one-shotqvprocess that reads its own password.kill -USR1opens a V8 inspector on any Node process and serves a full heap read to anything on loopback; against a long-lived TUI that is a certainty rather than a race.- Terminal-escape and bidi injection is stripped from every attacker-authored string —
token names, revert reasons, config entries. A token can be named
"\x1b[2A\x1b[KAll checks passed". - The CLI refuses to run with
NODE_OPTIONSset, since it can inject code before ours runs.
Report vulnerabilities per SECURITY.md.
Configuration
~/.quaivault/config.toml (or $XDG_CONFIG_HOME/quaivault/):
default_profile = "default"
[profiles.default]
network = "mainnet" # or "testnet"
address = "0x00…" # who you act as; no key needed
vault = "0x00…" # default vault
[aliases]
treasury = "0x005f…"
[contacts] # so you can tell who signed
bob = "0x00a1…"| Variable | Purpose |
|---|---|
| QUAIVAULT_ADDRESS | identity to act as |
| QUAIVAULT_VAULT | default vault |
| QUAIVAULT_PROFILE | profile to use |
| QUAIVAULT_PRIVATE_KEY_FILE | key file for CI (preferred over the next one) |
| QUAIVAULT_PRIVATE_KEY | raw key — supported, least preferred |
| QUAIVAULT_KEYSTORE_PASSWORD_FILE | keystore password for non-interactive use |
NO_COLOR, FORCE_COLOR and --color all work. Output to a pipe drops all chrome:
data goes to stdout, warnings and hints to stderr.
Shell completion
qv completion bash > ~/.local/share/bash-completion/completions/qv
qv completion zsh > ~/.zfunc/_qv # ensure ~/.zfunc is on $fpath
qv completion fish > ~/.config/fish/completions/qv.fishThe script is generated from the command registry, so it never goes stale. It is also deliberately static: it contains no vault aliases, contact names or paths, because a completion script lives in a dotfile that ends up in backups and dotfile repositories.
Further reading
CAPABILITIES.md— web-frontend capability parity and the intentional boundaries between one-shot and TUI workflows.docs/agent-contract.md— the full{exitCode, changed, retryable}table, theverifyblock, and how to bind an agent to bytes rather than to prose.docs/r4-ipfs-measurement.md— why on-chain ABI resolution is measured and deferred rather than built.SECURITY.md— what this tool protects against, what it does not, and every irreversible action with its guard.docs/environment-notes.md— Quai and quais behaviours observed directly against live networks: checksummed-address requirements, why a static block number proves nothing, and the expiry floor that is not what it looks like.
Development
npm run check # typecheck + lint + production build + test
npm pack # inspect the tarball before publishingRequires Node 22 or later. Releases go out through npm trusted publishing (OIDC), so
there is no NPM_TOKEN to leak and every release carries a provenance attestation —
verify with npm audit signatures.
License
MIT
Security checks in 0.6.1
Policy files reject unknown fields, malformed values, and invalid numeric limits rather than dropping those bounds. The hourly limit reserves an approval slot before signing, including attempts whose result is uncertain; reservations expire after one hour.
Keystore import bounds scrypt N, r, p and combined memory/CPU cost, plus PBKDF2 iterations,
before deriving a key. The default floor is scrypt N=131072 and r=8, or 600000 PBKDF2
iterations. --accept-weak-kdf permits a lower floor at import but never bypasses resource
limits. Imported keys are re-encrypted with the normal scrypt settings. Concurrent imports
cannot overwrite an existing key file.
