A JSON-first command surface.

skarbiec is a JSON-first CLI: finite commands print one JSON value to stdout. Only get, acquisition read, and totp return credential values; list, status, grant list, and bond list return metadata only.

Argument grammar#

One command name, then positional values, then options. An option is written either as two words or joined by an equals sign, and a bare option with no value that follows it is simply present. Long-running process interfaces — serve, mcp, native-host, and the sync daemon — are the exception to the one-JSON-value rule; every finite command prints one JSON value to stdout.

Options are single-valued: repeating one keeps the last value rather than accumulating a list. Commands that take several values use a comma-separated option or several positional values. In particular, the fields written by set are positional name and value pairs, not a repeated field option.

Shell
skarbiec <command> [positionals...] [--key value | --key=value] [--flag]

Vault items#

init creates the vault sealed to a fresh gpg owner key and a recovery key. set writes a schema-validated typed item; get returns one item's decrypted fields as JSON; list returns metadata only — id, item_uid, kind, state, revision, management, tags, recipients, and timestamps — never values. delete moves an item to a recoverable trash; restore brings it back, and restore --at rolls an item back to the revision recorded at that time.

rename changes an item's id and keeps everything else: the envelope moves whole, so the identifier, history, revision, tags, recipients, management, and the ciphertext are the same bytes under the new name, and nothing is decrypted on the way. It is owner-controlled items only. `skarbiec upgrade --apply` stamps a permanent identifier onto every item written before that field existed, together with the v2 envelope and the payload fingerprint; it touches the cleartext envelope only and is idempotent.

An identity is the account a person or robot signs in as: email, phone, password, totp_secret, recovery_codes, requiring email or phone. A platform login names it in context.identity, and its second factor is then read from that identity instead of being copied into every login. That is the difference between one Google account carrying one authenticator and a dozen platform rows each declaring an empty totp_secret field, which is what a vault looks like without this kind. A reference to an item that is not an active identity is refused at the write.

An exact duplicate is refused. Every item carries a fingerprint of its canonical payload, taken under a salt this vault minted for itself, so a second item holding a payload an active item already holds is refused and names that item, while rewriting one item with its own payload stays a rotation. duplicates reports the pairs that predate the refusal, comparing fingerprints without decrypting anything; the fingerprint is never printed, because it is a digest of secret content, and it means nothing outside the vault that minted the salt.

Shell
printf '%s' '{"schema":"skarbiec.item.v2","kind":"identity","fields":{"email":"operator@example.com","phone":"+48000000000","totp_secret":"JBSWY3DPEHPK3PXP"},"context":{}}' \
  | skarbiec set-json google-operator --type identity --tags dev
printf '%s' '{"schema":"skarbiec.item.v2","kind":"login","fields":{"username":"operator@example.com","password":"platform"},"context":{"identity":"google-operator"}}' \
  | skarbiec set-json claude-sso --type login
skarbiec totp-seed-state claude-sso
skarbiec duplicates
skarbiec get github
skarbiec list --all
skarbiec rename github github-primary
skarbiec restore github-primary --at 2026-07-01T12:00:00Z

Access: the declared consumer grant and acquisition#

One group answers the whole declared consumer grant. grant issue registers exact structured capabilities for a consumer; grant list reports consumers, capabilities, expiry, audience, and whether each identity is workload-bound; grant verify checks one exact action/resource/field binding, its --action stated, for a bearer presented through an owner-only --token-file; grant keep makes a direct grant live until revoked without rotating its bearer; grant revoke drops a grant or workload identity; grant capability issues one use-counted redemption of a declaration against a routed resource, with the lifetime (--ttl) and use count (--max-uses) the caller states; both are required and neither has a default or a ceiling.

acquisition request verifies an Ed25519 workload proof and issues the opaque short-TTL bearer; acquisition read returns only the bound field and atomically consumes the bearer.

Grants are edited, not mutated in place. Re-issuing the same consumer with the same capabilities rotates the bearer: a new value is shown once and the old one stops verifying. Changing the capability set is refused unless the call states --replace-capabilities, which rewrites the scope and rotates the bearer. grant ensure widens an existing direct grant by one exact field read without rotating anything — the owner proves possession through a 0600 --token-file that must hash to the recorded bearer. Every grant issue states its lifetime, --ttl-seconds or --until-revoked; there is no default. grant keep turns a direct grant issued with an end, lapsed or not, into one that lives until revoked and keeps its bearer, so a consumer whose bearer lives on another machine stays authorized with nothing delivered. The vault stores only the bearer's hash, so no listing or backup can leak the value.

Shell
skarbiec grant issue ci --capabilities acquire:github#password \
  --workload-public-key-file ci.pub.pem
skarbiec acquisition request ci github password \
  --workload-id ci-1 --workload-timestamp "$EPOCH" \
  --workload-nonce "$NONCE" --workload-signature "$SIG_HEX"
# write the one-use token to an owner-only file; it never goes in argv
skarbiec acquisition read ci github password --token-file "$PWD/one-use-token"

# Edit an existing grant
skarbiec grant issue ci --capabilities read:github#username \
  --replace-capabilities true   # rewrite scope, rotate bearer
skarbiec grant ensure ci github --field password \
  --token-file bearer.txt       # widen by one field, keep bearer

Servers: serve, mcp, native-host#

serve starts the loopback HTTP API — it binds the IPv4 loopback address only, on the port --port names; the service unit declares it, and serve without --port or --no-http is refused. mcp starts the stdio Model Context Protocol server for agents. native-host runs the length-framed browser native-messaging bridge, and browser-host-install rotates the narrow browser grant and registers the installed host.

Shell
skarbiec serve --port <port>
skarbiec mcp
skarbiec native-host

Diagnostics and audit#

  • status — vault path and non-sensitive counts of items, recipients, tokens, and bonds.
  • key-doctor — whether any key on this machine can still open the vault, reading vault and keyring directly so it answers while the service is down.
  • key-doctor retains the actual canary decryption cause in canary_error. An unreadable canary does not establish that a private key is missing: consult that cause and the recipient key inventory before restoring keys or rotating the owner. recovery drill retains the same cause in both its result and audit entry.
  • doctor — vault, audit chain, GnuPG daemons, canonical endpoint, WORM receipts, and consumer grants, each pass, fail, or not_configured. The grants check tests every live grant against the vault it names and decrypts nothing. The gpg_daemons check names each running keyboxd and gpg-agent, its pid and its physical footprint against the ceiling, and fails naming the daemon over it.
  • recover-daemons — stop the selected keyring's keyboxd, gpg-agent and scdaemon through gpgconf, then end every gpg child another process of this Skarbiec executable started against them (found in the process table before the daemons stop, each verified as a GnuPG executable through its kernel-bound identity, each said on stderr), so its caller is answered and its slot given back. Every failed control operation, and a process table that cannot be read, remains a failure; recovery never signals account-wide executable-name matches. The receipt reports recovered, the daemons it addressed, and before: what each running daemon held, in bytes, so a wedge and a bloat read differently. gpg starts fresh daemons on the next read.
  • audit list — the journal, filtered by operation, consumer, item, and time window when asked, without decrypting a credential.
  • audit verify — recompute journal linkage and digests and name the file it verified.

The full inventory#

Every public command has a dedicated page below. skarbiec --help and -h print every command with one sentence saying what it does, as text. skarbiec help prints the same inventory as JSON for machines: groups, commands, and under described each command's usage, summary and page. --help or -h after any command never runs that command: a group (grant, route, credential, rotation, recovery, emergency, donation, bond) prints its own inventory, import prints its usage, formats and limits, and every other command prints its usage line, what it does and its page, so skarbiec purge <id> --help purges nothing. --help after a name that is not a command fails with unknown command and exit status 2.

Exit status: 0 when the command did what it promises; 2 when the invocation itself is wrong — an unknown command or subcommand, a missing argument or required flag, a flag value that does not parse, or flags that contradict each other — and nothing was read or changed; 1 for every other failure, such as an unauthorized token, a refused admission or a failed push. The message on standard error starts with Error: and names what was wrong.

Output: every command that prints a result prints it as JSON by default, because the broker, Brama and Stado read it. --text anywhere on the command line prints the same document for a person instead: one key: value line per field, nested fields indented under their key, list entries as - lines, null as none and an empty list or object as (none). --json names the default form and is accepted by every command. Both are taken out before the command reads its arguments, so neither consumes the word after it and no command refuses either as unknown. Commands that print one value or a file (get --field, export) print it unchanged either way.

Shell
skarbiec --help
skarbiec get --help
skarbiec help