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
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.
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 connectalso carry connector metadata: provider, mode, account ID, and a maskedabc…xyzfingerprint of the credential.
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:
.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 ofiv || ciphertext || tagfrom 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
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- runscapy statusafter you switch git branches, so you notice drift immediately.post-merge- same, aftergit pull/git merge.
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
.envafter any edits you’ve made. - Pinned - the value hashes
keep.lockrecords for this branch, written on the last sync. - Remote - what’s currently in the service’s blob for this branch.
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.