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 stateThe first interactive initialization 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.
On a new machine, 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).
# 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-ownerscvlt 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/Modusign/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.