Skip to content

Repository files navigation

cvlt

npm

cvlt is the official CLI for Circles Vault, with two parallel address surfaces:

  • op://<vault>/<item>/<field> — 1Password-style. The cvlt read, inject, and run workflows 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 OIDC repository claim).

cvlt read, cvlt inject, and cvlt run accept both schemes anywhere a reference appears.

Install

# 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/latest

Authentication

cvlt resolves credentials in this order:

  1. OP_CONNECT_HOST + OP_CONNECT_TOKEN — drop-in for op CLI; useful when you already have a token.
  2. OP_CONNECT_HOST + GitHub Actions OIDC — if ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN are present (workflow has id-token: write), cvlt fetches a short-lived OIDC token automatically. No stored secrets.
  3. Shared Circles credentials (~/.crcl/config + ~/.crcl/credentials) — interactive user. The first crcl login establishes the shared current profile; later identities are selected explicitly with crcl use. Legacy ~/.config/crcl profiles 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.

Common commands

Read a secret

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

Inject secrets into a template

# 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 > .env

Run a command with secrets injected as env vars

DB_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/password

cvlt run resolves op:// / vlt:// references found in --env-file entries and the process env, then exec's the command with the actual values.

Use the 1Password op CLI

Run the localhost-only Connect bridge in one terminal:

cvlt connect

Copy 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_PASS

The bridge decrypts locally with this installation's protected key and stops accepting requests when cvlt connect exits.

Manage vaults

cvlt vault list
cvlt vault create "production"
cvlt vault edit "production" --name "prod-secrets"
cvlt vault delete "old-vault"

Manage items

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-secrets

Documents

cvlt 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.pem

Client encryption

Vault 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 state

First 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.

Using two machines (cvlt client)

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 machine

Revoking 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.

Emergency recovery

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 circlesac

The 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.

GitHub-coordinate secrets (vlt://)

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:

  1. Coordinate names contain /. An op://<vault>/<item>/<field> reference splits on /, so it can't name a vault like github.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.
  2. 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 absent
  • vlt://github.com/<owner>/<NAME> — owner-global
  • NAME charset is GitHub-isomorphic ([A-Z0-9_], no digit start, no GITHUB_ 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/api

Scope: 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-owners

Development enrollment harness

npm 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-clients

npm 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.

Registering repos for CI access (operator-only)

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 remain

Owner-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.

GitHub Actions workflow

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.sh

cvlt 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.

Composite action

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.sh

With 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 job

cvlt run keeps secrets scoped to the child process (narrower exposure, recommended); export-env trades that for job-wide convenience.

Profile / org overrides

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 slug

Node SDK

Install the SDK independently from the CLI:

npm install @circlesac/vault

The 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.

About

1Password-compatible secrets CLI for Circles Vault

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages