cvlt is the official CLI for Circles Vault, with two parallel address surfaces:
op://<vault>/<item>/<field>— 1Password-style. Thecvlt read,inject, andrunworkflows preserve the familiar command and reference shape while encrypting locally.vlt://<provider>/<owner>[/<repo>]/<NAME>— flat GitHub-Secrets-style key→value secrets, addressed by GitHub coordinates. The repo segment selects the scope: present → project secret, absent → owner-global. Designed to replace GitHub Actions secrets (the coordinate is identical to the OIDCrepositoryclaim).
cvlt read, cvlt inject, and cvlt run accept both schemes anywhere a reference appears.
# macOS / Linux via Homebrew
brew install circlesac/tap/cvlt
# Any Node.js environment (works on GitHub Actions ubuntu-latest)
npm install -g @circlesac/vlt-cli
# Static binaries (no Node required)
# Download from https://github.com/circlesac/vault/releases/latestcvlt resolves credentials in this order:
OP_CONNECT_HOST+OP_CONNECT_TOKEN— drop-in foropCLI; useful when you already have a token.OP_CONNECT_HOST+ GitHub Actions OIDC — ifACTIONS_ID_TOKEN_REQUEST_URL/_TOKENare present (workflow hasid-token: write),cvltfetches a short-lived OIDC token automatically. No stored secrets.- Shared Circles credentials (
~/.crcl/config+~/.crcl/credentials) — interactive user. The firstcrcl loginestablishes the shared current profile; later identities are selected explicitly withcrcl use. Legacy~/.config/crclprofiles remain readable through automatic migration.
cvlt whoami shows the resolved host + account (personal by default, or org:<slug> with --org/CRCL_ORG).
Use cvlt --profile prod:user@example.com auth token when another local client needs the selected profile's short-lived Circles credential. The command refreshes the shared profile when needed and prints only the access token.
cvlt read "op://my-vault/db-credentials/password"
cvlt read -n "op://my-vault/db-credentials/password" # no trailing newline
cvlt read -o /tmp/password "op://..." # write to file# template.env
DB_HOST={{op://my-vault/db-credentials/host}}
DB_PASS={{op://my-vault/db-credentials/password}}
# Inject and write
cvlt inject -i template.env -o .env
# Or pipe
cat template.env | cvlt inject > .envDB_PASS="op://my-vault/db-credentials/password" cvlt run -- ./deploy.sh
# op run idiom: keep references in a committed env file (references are not secrets)
cvlt run --env-file=.cvlt.env -- ./deploy.sh# .cvlt.env — safe to commit; values are fetched at runtime
DB_PASSWORD=vlt://github.com/acme/api/DB_PASSWORD
OPENAI_KEY=vlt://github.com/acme/OPENAI_KEY
ACCOUNT_PASSWORD=op://my-vault/db-credentials/passwordcvlt run resolves op:// / vlt:// references found in --env-file entries and the process env, then exec's the command with the actual values.
Run the localhost-only Connect bridge in one terminal:
cvlt connectCopy the printed OP_CONNECT_HOST and OP_CONNECT_TOKEN exports into another terminal. The standard 1Password CLI can then read Circles Vault without a separate account key argument:
op read "op://my-vault/db-credentials/password"
op inject --in-file config.yml.tpl
DB_PASS="op://my-vault/db-credentials/password" op run --no-masking -- printenv DB_PASSThe bridge decrypts locally with this installation's protected key and stops accepting requests when cvlt connect exits.
cvlt vault list
cvlt vault create "production"
cvlt vault edit "production" --name "prod-secrets"
cvlt vault delete "old-vault"cvlt item create --vault prod-secrets --category login --title "DB" username=admin password=secret
cvlt item list --vault prod-secrets
cvlt item get "DB" --vault prod-secrets --format json
cvlt item edit "DB" --vault prod-secrets password=newpass
cvlt item edit "DB" --vault prod-secrets 'token[password]' # change type, preserve value
cvlt item delete "DB" --vault prod-secrets
cvlt item move "DB" --current-vault staging --destination-vault prod-secretscvlt document create ./cert.pem --vault prod-secrets --title "TLS Cert"
cvlt document list --vault prod-secrets
cvlt document get "TLS Cert" --vault prod-secrets -o ./cert.pemVault content is encrypted locally before upload. The service receives the encrypted representation required by the public client protocol.
cvlt doctor # account encryption, installation key, and GitHub OIDC KMS stateFirst use. The first interactive write initializes account encryption and prints a high-entropy recovery code once. Store it outside Vault. The installation private key is saved in macOS Keychain, Windows DPAPI, Linux Secret Service, or a passphrase-encrypted local file when no OS store exists, and never leaves the machine.
One key per Vault origin. A machine holds a single installation key per Vault deployment, shared by your personal account and every organization you select there. Enrolling the same machine into another account reuses that key, so registrations you already have keep working.
Enrollment adds a second installation without touching the recovery code and without revoking the first. On the new machine (B):
crcl login
cvlt client request --name "example-workstation"client request prints the enrollment request on stdout and its fingerprint on stderr. The request is public registration material — a versioned, base64url-encoded document holding the Vault origin, the account, the client ID, the public-only JWK, the platform, and the optional name. It contains no private key, no account key, no recovery code, and no access token. Repeating the command with the same account and arguments prints the identical request and never replaces the existing key.
On an already registered machine (A):
cvlt client approve <request>A prints the request's Vault origin, account, display name, platform, and public-key fingerprint, then asks for confirmation. Check that the fingerprint matches the one B printed before typing yes. Approval only proceeds when the origin and the canonical account match A's own, and A must prove it holds an active installation's private key: the service issues a single-use challenge encrypted to A's registered public key, bound to the account, A's client ID, the HTTP method, the pathname, and the exact request-body bytes.
After approval, B works normally:
cvlt doctor
cvlt read "op://example-vault/example-item/password"Manage installations explicitly:
cvlt client list # active installations; the current one is marked
cvlt client revoke <client-id> # revoke a lost or retired machineRevoking an installation leaves your Vault content untouched; the revoked machine gets the "installation is not registered" failure on its next read. An installation cannot revoke itself. A revoked machine can be re-enrolled later with the same stored key — run cvlt client request and approve it again; no recovery is needed.
cvlt client commands need a signed-in user. GitHub Actions OIDC callers cannot enroll, list, or revoke installations, and their read behavior is unchanged.
cvlt recover is the lost-all-installations path. It is destructive: it revokes every previously registered installation and rotates the recovery envelope. Use cvlt client request / approve for a normal second machine.
On a new machine with no access to any registered installation, run a fresh crcl login, then:
cvlt recover start --org circlesac
cvlt recover complete 12345678 --org circlesacThe second command prompts for the recovery code without placing it in shell history. A successful recovery replaces the previous installation credentials and prints a new recovery code once.
A secret is just an op:// item (there's no separate "secret" store or verb). What's special about a vault named like a GitHub coordinate — github.com/<owner>[/<repo>] — is that it's addressed by the vlt:// reference scheme, which exists for two reasons op:// can't cover:
- Coordinate names contain
/. Anop://<vault>/<item>/<field>reference splits on/, so it can't name a vault likegithub.com/acme/api(the slashes collide).vlt://github.com/<owner>[/<repo>]/<NAME>knows the structure —<NAME>is the last segment, the leading github coordinate is the vault — so it parses unambiguously, no escaping. - Inheritance. Reads cascade
project > global(repo→owner), like GitHub Actions repo/org secrets.
vlt://github.com/<owner>/<repo>/<NAME>— project; falls back to the owner if absentvlt://github.com/<owner>/<NAME>— owner-global- NAME charset is GitHub-isomorphic (
[A-Z0-9_], no digit start, noGITHUB_prefix)
The item itself is still managed with the op item/vault verbs — those take the coordinate as a --vault name (a flag value, not an op:// reference, so the slashes are fine).
An owner-global Vault may contain a small number of content_mode: plain items managed by a private service binding. cvlt read vlt://… resolves those items in the same project-before-global order as encrypted items; their public item mutation remains blocked by the Vault service.
# Register the coordinate vault (+ CI grant for a repo coordinate; org-scoped → --org)
cvlt vault create github.com/acme/api --org acme
# Write a secret = create/edit an item in that vault (--vault takes the name)
cvlt item create --vault github.com/acme/api --title DB_PASSWORD 'value[password]=s3cret'
cvlt item edit DB_PASSWORD --vault github.com/acme/api 'value[password]=rotated'
# Read by reference — vlt:// handles the coordinate + inherits (project→owner)
cvlt read "vlt://github.com/acme/api/DB_PASSWORD" # infers accessible org acme; cascades to github.com/acme if absent
# List / delete = op item verbs (coordinate as --vault name)
cvlt item list --vault github.com/acme/api
cvlt item delete DB_PASSWORD --vault github.com/acme/apiScope: personal by default, except vlt://. General commands and op:// references target your personal account unless you select an org with --org <slug> (or CRCL_ORG). A vlt://github.com/<owner>/... read automatically targets <owner> when it is an org accessible to the current Circles credential; otherwise it keeps the personal fallback. An explicit org must match the reference owner, and run/inject resolve mixed owners independently. CI via GitHub OIDC already has its org fixed by OP_CONNECT_HOST.
To exercise two accessible owners against a live development Vault without printing either value, provide distinct fixture references and run:
CVLT_E2E_PROFILE=dev-profile \
CVLT_E2E_REF_A=vlt://github.com/example-org-a/example-repo/MIXED_OWNER_PROBE \
CVLT_E2E_REF_B=vlt://github.com/example-org-b/example-repo/MIXED_OWNER_PROBE \
npm run test:e2e:mixed-ownersnpm run test:e2e:two-clients drives the candidate CLI through enroll, list, revoke, re-enroll, and audit readback against a live development deployment. It compares canary values by hash only, never prints one, and removes its test installation and canary item on exit.
It needs two genuinely independent key stores. The provided single-host harness is for Linux without a running Secret Service: it uses two encrypted-file stores with distinct XDG_CONFIG_HOME values and distinct passphrases. The harness fails when either store has no key file of its own, so a shared OS credential slot cannot masquerade as a second installation. For the rollout gate, the equivalent cross-device sequence can instead use two machines or OS user accounts; two processes under one macOS user do not count because they share one Keychain slot per Vault origin.
Store A must already hold a registered installation; store B must start with none.
CVLT_E2E_PROFILE=dev-profile \
CVLT_E2E_ORG=example-org \
CVLT_E2E_VAULT=example-vault \
CVLT_E2E_STORE_A=/tmp/cvlt-store-a \
CVLT_E2E_STORE_B=/tmp/cvlt-store-b \
CVLT_E2E_PASSPHRASE_A=example-store-a-passphrase \
CVLT_E2E_PASSPHRASE_B=example-store-b-passphrase \
npm run test:e2e:two-clientsnpm run test:e2e runs the mixed-owner harness and this one; both must pass before a release. The recovery-envelope comparison in the rollout checklist is service-side — no client route exposes the envelope.
cvlt vault create <coordinate> creates the op:// vault that stores the secrets; for a repo coordinate it also records the OIDC grant that lets that repo's CI read it (creating it is the consent). Grants are org-scoped, so pass --org <owner>. Once per repo:
cvlt vault create github.com/circlesac/my-app --org circlesac
cvlt vault create github.com/circlesac/my-app --org circlesac --ci-write --env production
cvlt vault get github.com/circlesac/my-app --org circlesac # registration + secret count
cvlt vault delete github.com/circlesac/my-app --org circlesac # revokes CI access; items remainOwner-global (github.com/circlesac) needs no grant — every registered repo of that owner reads it via project > global, and org members write to it with cvlt item create --vault github.com/circlesac --org circlesac ….
The advanced cvlt oidc grant create|list|get|edit|delete commands remain for op://-vault-scoped or org-wildcard (owner/*) grants.
vault create / edit / delete, oidc grant *, and whoami require operator (user JWT) auth. OIDC tokens from GitHub Actions are scoped to data-plane operations (read secrets/items, write if allowed) and cannot manage vaults or grants regardless of role.
After registering the repo once, a workflow needs zero stored secrets:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
env:
OP_CONNECT_HOST: https://vault.circles.ac/<your-org>
steps:
- uses: actions/checkout@v4
- run: npm install -g @circlesac/vlt-cli
- run: cvlt run -- ./deploy.shcvlt detects the runner's ACTIONS_ID_TOKEN_REQUEST_URL / _TOKEN env vars, mints a GitHub OIDC token with the right audience, and sends it to Vault. The server verifies GitHub's signature, matches the claims (repository, environment, ref) against the grant ACL, and serves the request.
For vlt:// secrets the grant's repository doubles as the coordinate: a granted workflow can read its own project secrets plus that owner's globals — no other coordinate, regardless of what it asks for.
The repo ships a composite action that installs cvlt and sets the endpoint:
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: circlesac/vault/action@main
with:
host: https://vault.circles.ac/<your-org>
- run: cvlt run --env-file=.cvlt.env -- ./deploy.shWith export-env: true the action resolves env-file entries into $GITHUB_ENV (each value masked via ::add-mask:: first), so later steps can use ${{ env.NAME }} — one word away from GitHub-native ${{ secrets.NAME }}:
- uses: circlesac/vault/action@main
with:
host: https://vault.circles.ac/<your-org>
env-file: .cvlt.env
export-env: "true"
- run: ./deploy.sh # $DB_PASSWORD available to the whole jobcvlt run keeps secrets scoped to the child process (narrower exposure, recommended); export-env trades that for job-wide convenience.
cvlt vault list # shared current profile, personal account
cvlt vault list --profile dev # explicitly selected Circles profile
cvlt vault list --org other-org # different org slugInstall the SDK independently from the CLI:
npm install @circlesac/vaultThe SDK package exposes the same E2EE and OS credential-store implementation
used by the CLI. Applications can read Vault values without spawning cvlt:
import { createVaultClient } from "@circlesac/vault"
const vault = createVaultClient()
const password = await vault.read("op://personal/example-service/password")The device key remains in macOS Keychain, Linux Secret Service, or Windows DPAPI. Account credentials and decrypted Vault values are not copied into the application's configuration directory.
The language-neutral client contract is documented in SDK.md, and
the native Go SDK is published from go/.
Installing @circlesac/vault never downloads the native CLI. The separate
@circlesac/vlt-cli package installs the cvlt shim and platform binary.