Skip to main content
Capy has two moving parts: the CLI on your machine, and the service that brokers the cloud and BYOC co-decrypt handshake. The CLI handles syncing, encrypting, invites, and deploy setup. capy run then gives decrypted values to the child app process. The service stores ciphertext and membership data for the normal client protocol.

The two components

Two-component cloud and BYOC architecture: the CLI syncs and encrypts, and a child app launched through capy run receives decrypted environment values. The service brokers co-decrypt and stores ciphertext with membership records.Two-component cloud and BYOC architecture: the CLI syncs and encrypts, and a child app launched through capy run receives decrypted environment values. The service brokers co-decrypt and stores ciphertext with membership records.
Plaintext can exist in the CLI while it encrypts or decrypts, in the child process that capy run launches, and in other local processes that a workflow explicitly opens. The --web option adds a local browser UI. The service does not receive plaintext secret values on the normal Capy data path.

On-disk state

Inside a Capy-managed project: Gitignored: .env, .env.pre-capy.old, and .env.*.decrypted. keep.lock is committed. .capy/ contains local state, but .capy/deploy.json is a committed deploy-target configuration with no secret values.
And globally, in your home directory:
key.enc holds a double-wrapped organization master key. Its inner layer uses HKDF(K_local), where K_local is 32 random bytes stored beside it as local.key; the service adds the outer layer. In normal co-decrypt, the CLI presents key.enc, the service checks membership and removes only its outer layer, and the client completes the authenticated access operation. The service does not keep a usable long-lived key.enc copy for ordinary co-decrypt. Transport v4 is the exception in storage form: it stores a short-lived encrypted package that contains key.enc and local.key. The package is encrypted before upload with a one-time key carried in the transport-link fragment, so the service cannot open the stored package. K_local therefore leaves the original machine only inside encrypted transport and pairing envelopes, not as service-readable key material. Both files are long-lived secrets, and both are needed: without local.key the blob cannot be opened through co-decrypt. Add another machine with capy transport and capy pair, or recover with a fresh invite or the owner’s recovery phrase.

keep.lock

keep.lock is a small JSON file that tells Capy which project this directory belongs to and what its current state is. It contains:
  • Org ID, project ID, and project name - which org and project this directory maps to.
  • Schema version - for format evolution.
  • Variable manifest - an alphabetically sorted list of variable names, each with per-branch resource IDs and value hashes. Hashes, not plaintext, not ciphertext, not keys. Variables provisioned by capy connect also carry connector metadata: provider, mode, account ID, and a masked abc…xyz fingerprint of the credential.
It doesn’t contain any keys, any values, or any ciphertext. Committing keep.lock is what lets a teammate clone your repo, run capy, and sync the same secrets you’re working with. The active branch is tracked outside it - in the .env header and in .capy/branch (local state).

.env after Capy

Your .env after capy has run looks like:
The header records the org, project, and branch these values were encrypted for, so Capy can tell when a .env belongs to a different project. Each capy:… snippet is:
  • capy: literal prefix
  • {resourceId} - a 5-character hash of the branch name and the variable name, used to diff without leaking plaintext
  • {blob} - base64 of iv || ciphertext || tag from AES-256-GCM under the project key, with a short plaintext preview spliced around it (for a value longer than 24 characters, its first four and last six characters) so you can tell values apart at a glance
The ciphertext is inert without the project key, and two projects that hold the same value still produce completely different ciphertext. The resourceId identifies the variable rather than the value: the same variable name on the same branch derives the same ID in every project. The preview is the one part of the line that is plaintext, which is why .env stays gitignored.

Git hooks

On first-run Capy installs two hooks:
  • post-checkout - runs capy status after you switch git branches, so you notice drift immediately.
  • post-merge - same, after git pull / git merge.
No pre-push hook. If an older Capy version installed one, capy cleans it out on the next run.

Sync engine

The sync engine is a three-way merge between:
  • Local - what’s currently in .env after any edits you’ve made.
  • Pinned - the value hashes keep.lock records for this branch, written on the last sync.
  • Remote - what’s currently in the service’s blob for this branch.
For each variable, Capy picks automatically when only one side changed. When both sides changed (conflict), it prompts interactively. See Syncing secrets.

Cloud, BYOC, and local-only profiles

The two-share description on this page applies to cloud and BYOC profiles. A local-only profile does not contact a service: it stores a passphrase-protected local key and unlocks it on demand. capy lock clears its cached unlocked session; capy logout does not.

The wire

Every CLI request to the service is a plain HTTPS call - GET, POST, PATCH, or DELETE - carrying a JSON body where there is one and a bearer auth token. Secret payloads on the normal data path are already-encrypted blobs; HTTPS still protects the request metadata and credentials in transit.

What’s next

Zero trust

Why two shares, and what each share holds.

Cryptography

Every client-side algorithm, key, and parameter.
Last modified on October 2, 2026