Skip to main content
The bare capy command is the main loop. Run it any time and Capy:
  1. Authenticates you (if needed) via a browser-based login.
  2. Unlocks your master key by co-decrypting it with the Capy service.
  3. Pulls the encrypted values for the current branch from the service. keep.lock itself is git-owned - Capy only rebuilds it from the service when the file is missing entirely.
  4. Compares the hashes pinned in keep.lock, your local .env, and the remote values.
  5. Shows you every difference and asks what to do with them.
  6. Rewrites .env in place with capy:… snippets that encode the resolved values, updates keep.lock, and commits it.
Decrypted values only exist in memory during the diff. What lands back in .env is the ciphertext plus a short cosmetic preview - the first and last few characters of the value, so you can still tell entries apart at a glance. The one place full plaintext hits disk is the first run, which saves your original .env to .env.pre-capy.old.

First run vs. subsequent runs

On the first run in a project, Capy treats .env as authoritative:
  • Asks which organization and project to use (or creates them).
  • Generates a project encryption key via the zero-trust co-decrypt flow.
  • Writes keep.lock to the project root and commits it to git.
  • Asks which branch the project should start with - development by default - and creates it.
  • Asks you to confirm, then encrypts every value in .env and uploads the ciphertext.
  • Adds .env, .capy, .env.pre-capy.old, and .env.*.decrypted to .gitignore.
  • Installs git hooks (post-checkout and post-merge) that run capy status after branch switches and merges.
  • Writes a commented-out backup of your original .env to .env.pre-capy.old (gitignored) and rewrites .env with capy:… snippets.
On subsequent runs, Capy reconciles three sources: what’s on the server, what you last pinned, and what’s in your local .env. Any difference stops the run and asks what to do with it - resolve everything one way, or pick a value per variable.

Conflicts and resolution

Every capy run is a three-way compare between:
  • Pinned - hashes stored in keep.lock from the last sync.
  • Local - what’s in your .env right now (decrypted in memory).
  • Remote - what’s on the service for this branch.
For each variable, Capy classifies the state from the triple (pinned, local, remote). Here’s every case, using A for one value, B and C for others, and for “absent”. The last column is the line capy status prints for that case; the capy sync flow makes the same classification but renders it as the three values side by side, without labels. The first two rows are the only ones Capy handles without asking. Everything else lands in the diff table and waits for your decision - Capy never pushes or pulls a variable on its own. If nothing at all differs, Capy prints Everything is up to date! and exits. When something does differ, Capy prints a three-way diff table and then a bulk-action menu:
Values are shown as abc...xyz snippets - the first and last three characters of the plaintext, or the whole value when it is six characters or shorter. Which menu entries appear depends on which columns actually differ: Capy drops the options that can’t apply, so a run where only the remote moved offers Retrieve all remote values and Retrieve all pinned values but no commit option. In local-only mode there is no remote, so the first entry reads Commit all local values. Pick Individually resolve to open an interactive arrow-key table where each variable has its own row and its own Choice:
← → moves the selection across pinned, local, remote, and delete; ↑ ↓ moves between variables; Enter confirms the row and jumps to the next unresolved one. Each row starts on the pinned value when Capy can resolve it, otherwise on local, then remote. Once every row is confirmed, Capy applies the merged set and rewrites .env in place. q cancels without writing anything. Resolving individually updates your local .env and keep.lock - it doesn’t upload anything. Only Commit and push all local values pushes to the service, so run capy push afterwards if you want your resolved values shared with the team.

Remote unreachable

If the service is unreachable, Capy prints Cannot reach remote. Showing local changes only. and falls back to a two-way compare of pinned vs. local. Anything it could not read is left out of the comparison entirely, so a variable that exists only on the remote never shows up as a difference to resolve. Losing access to the branch is a different case: the run fails with a permission error, and Capy lists the branches you can still read so you can capy checkout one of them. In capy status, an unreachable or forbidden remote instead marks every variable with a ? and reports the reason.

Status and push

status only reports - it never changes your .env, your keep.lock, or the remote. push encrypts whatever is in .env right now and sends it to the branch you’re on; it never fetches or diffs first, so run capy when you want to see what a teammate changed.

What ends up in git

  • keep.lock - a versioning manifest with no keys and no plaintext. Committed - Capy commits it for you (chore(capy): pin <branch> secrets) whenever a sync, push, or edit changes it. Set CAPY_NO_AUTOCOMMIT=1 to opt out and commit it yourself.
  • .env - ciphertext snippets at rest, but gitignored. Capy syncs the content over the wire instead of through git.
  • .env.pre-capy.old - commented-out backup of your original .env from before first-run. Gitignored.
  • .capy/ - local state for this project (the active branch, sync state). Gitignored. Your session and keys live in the global ~/.capy/ instead.

Branches

Capy has its own concept of secret branches - independent sets of values for dev, staging, prod, or ephemeral environments. Secret branches are tracked separately from git branches: the post-checkout and post-merge hooks run capy status so drift surfaces right after you move around the tree, but switching secret branches is an explicit capy checkout <branch>. See Branches.

What’s next

capy (CLI reference)

Every flag on the sync command.

Branches

Isolate secrets per environment.
Last modified on August 11, 2026