capy run. The service holds only ciphertext and membership records.
The two components
On-disk state
Inside a Capy-managed project: Gitignored:.env, .env.pre-capy.old, .env.*.decrypted, .capy/. Committed: keep.lock.
key.enc holds your org master key, and it lives only on the client. It’s double-wrapped: an inner AES-256-GCM layer under HKDF(K_local) - where K_local is 32 random bytes minted per machine and stored beside it as local.key - and an outer layer added by the service’s /orgs/{orgId}/wrap endpoint. The service never stores a copy - to use key.enc the CLI posts it to /orgs/{orgId}/co-decrypt, where the service checks your membership and strips only its outer layer. K_local never leaves your machine, so what co-decrypt hands back is still opaque to the service.
Both files are long-lived secrets, and both are needed: without local.key the blob is inert, and you get back in with capy redeem or capy recover.
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.
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. The bodies are small and the payloads are already-encrypted blobs - compromising the transport tells an attacker nothing they couldn’t get by compromising the service itself.
What’s next
Zero trust
Why two shares, and what each share holds.
Cryptography
Every client-side algorithm, key, and parameter.