sopsy — encrypted team secrets in Git, unlocked with Touch ID

16 min read Original article ↗

A CLI for SOPS · age · Apple Secure Enclave
YubiKey Linux Support Coming Soon

Encrypted secrets you can actually commit to Git.
Decrypt with Touch ID


Plaintext is never in the repo, never in Slack. Sleep soundly at night.

sopsy encrypts the secrets in your .env, .yml, and .json files — values only, names left in the clear. The file stays diffable and git blame-able, so you can see exactly when each key appeared and who added it, then commit the whole thing without flinching. Every teammate's key is minted inside their Mac's Secure Enclave

Secrets decrypt only on Touch ID, unless you used the --without-touch-id flag when you join. The private key never leaves the chip, and any current member can grant the next one access. No server. No subscription. No plaintext in Slack — or anywhere for that matter.

$brew install kigster/tap/sopsy

$cargo install sopsy

See the 60-second setup ↓

sopsy init bootstrapping your encrypted repository ✔ git repository ✔ found sops > Generate a Secure Enclave identity? Yes ✔ created Secure Enclave identity (Touch ID) ℹ private key never leaves the Enclave ➜ public recipient age1se1q2a48h0242huw4… > Your name (key owner): Konstantin ✔ .sops.yaml ✔ .env.example ✔ .env.encrypted ✔ .gitignore ✔ .sopsy.yml ✔ .sopsy.sha > Set up a break-glass emergency key? Yes ✔ wrote break-glass.private + .public ⚠ store both offline in 1Password, then continue press ENTER once the keys are saved … ✔ removed local key files ✔ recorded break-glass in both config files ✔ repository bootstrapped

Quick start

From zero to a whole team in seven steps.

  1. Start (or enter) a git repository

    git init my-app && cd my-app

  2. Bootstrap everything

    sopsy init — tools check, Secure Enclave identity, config files.

  3. Put real secrets in

    sopsy edit .env.encrypted decrypts to your editor and re-encrypts on save.

  4. Verify hygiene before you commit

    sopsy check — the same command your CI runs.

  5. Commit the encrypted artifacts

    git add .sops.yaml .sopsy.yml .sopsy.sha .env.example .env.encrypted .gitignore — plaintext .env is already gitignored. Push the ciphertext with confidence.

  6. A teammate joins (self-service)

    The new dev clones the repo and runs sopsy join "Alan Turing" — mints their own Secure Enclave key with Touch ID and records a pending request. They commit and open a PR.

  7. An existing member approves

    Any current member runs sopsy approve "Alan Turing" — vouches for the key, promotes it into .sops.yaml, and re-keys every secret. Who approved, and when, is recorded in .sopsy.ymlsopsy recipient list shows the whole audit trail. Now Alan can decrypt.

Watch the entire process live just below…

Terminal recording of the full sopsy workflow: init, edit, check, commit, join, and approve

sopsy does not replace SOPS. It makes SOPS delightful.

sopsy join

Generate

age-plugin-se mints your keypair via Touch ID. The private key is sealed in the Secure Enclave; your public key is recorded pending in .sopsy.yml.

sopsy approve

Vouch & approve

You open a PR; any active member vouches and promotes your public key from pending into .sops.yaml’s recipient list — recording who approved you, and when, in .sopsy.yml.

sops updatekeys

Re-key secrets

Every encrypted file is re-wrapped for the new recipient set — so your key, and only listed keys, can open them.

sopsy edit

Edit & commit

git pull, then Touch ID decrypts into your editor and re-encrypts on save. Commit the ciphertext; plaintext .env stays gitignored.

SOPS stays the encryption engine. age stays the cryptography. The Secure Enclave holds the private key.

sopsy orchestrates all three with safe defaults, great diagnostics, and a scriptable interface — so the right thing is finally the easy thing.

Why not just use sops?

You already can. sopsy removes the thirteen ways you can get it wrong.

Everything here runs sops + age under the hood. The difference is the friction — the steps you have to remember, the flags you have to know, and the safety rails that aren't there until you build them.

Task

raw sops + age

with sopsy

Bootstrap a repo

Hand-write .sops.yaml creation_rules, craft .gitignore rules, build .env.example, then encrypt by hand.

One idempotent command writes every file and seeds an encrypted .env.encrypted for you.

sopsy init

Get a hardware identity

Run age-plugin-se keygen, copy the public key, paste it into config without fat-fingering it.

The Enclave identity is generated, printed prominently, and recorded in config automatically.

sopsy init

Onboard a teammate

Hand-edit .sops.yaml, run sops updatekeys on every file — and just trust on faith that the key really belongs to them.

The newcomer joins to record a pending request (no access yet); any active member approves — vouching for the key, promoting it, and re-keying every secret in one step.

sopsy join → sopsy approve

Audit who has access

git blame .sops.yaml and hope the commit history tells the story of who let whom in.

Every member carries requested_at, approved_at, and approved_by — one command prints the whole audit trail as a table.

sopsy recipient list

Rotate / offboard keys

Manually re-key each file; forget one and a departed key still decrypts new commits.

Every recipient change re-wraps the existing secrets via sops updatekeys automatically, so a departed key can't read new commits.

sopsy recipient remove

Edit an encrypted file

Pass --input-type/--output-type yourself — .env.encrypted has no extension sops recognizes.

File type is auto-detected (dotenv / yaml / json / binary); your $EDITOR just opens.

sopsy edit

Encrypt a file in one shot

Call sops --encrypt with the right --input-type, then redirect the output yourself and hope the artifact matches a creation rule.

sopsy encrypt writes a .encrypted artifact, auto-detecting the format — .env, json, yaml, ini — all encrypt values only; anything else as one blob.

sopsy encrypt [file] -o file.encrypted

Decrypt to stdout or a file

sops --decrypt with manual type flags, then redirect — easy to clobber the wrong file or leak plaintext to disk.

sopsy decrypt prints plaintext to stdout or writes it with -o file.

sopsy decrypt file.encrypted [-o file]

Know what's value-encryptable

Dig through docs to learn which formats support partial, values-only encryption versus whole-file.

sopsy list-supported-types prints exactly which extensions value-encrypt (.env / .yaml / .json / .ini); everything else is encrypted as a single binary blob.

sopsy list-supported-types

Guard CI against leaks

Roll your own grep for tracked plaintext and hope it catches every case.

Seven hygiene invariants, a clear pass/fail checklist, and a non-zero exit on any violation.

sopsy check

Give CI decryption access

Generate an age key on a laptop, paste the private half around, hand-edit .sops.yaml, and remember to delete the local copies.

A guided ceremony provisions a portable key, waits while you store one CI secret (SOPS_AGE_KEY), then deletes the local files and re-keys. A Linux runner with only sops installed can decrypt.

sopsy recipient ci

Diagnose a broken setup

Dig through $PATH, tool versions, and Enclave status by hand.

A grouped, colorful report you can paste straight into a GitHub issue — always exits 0.

sopsy doctor

Stay disaster-proof

Nothing reminds you to keep an offline recovery key. Lose the device, lose the secrets.

One guided ceremony generates a portable emergency key, waits while you store it offline, then deletes the local copy and registers it — and doctor and check nag until it exists.

sopsy recipient break-glass

Net effect: the same battle-tested crypto, minus the foot-guns — every interactive prompt also has a flag, so a human at a terminal and an unattended CI job run the exact same tool.

What's in the box

Safe defaults, sharp diagnostics, zero ceremony.

hardware

Secure Enclave identities

Your private key is generated inside Apple Silicon hardware, gated by Touch ID, and impossible to read, copy, or exfiltrate. Only the public recipient is ever shared.

init

One-command bootstrap

sopsy init writes .sops.yaml, .env.example, an encrypted .env.encrypted, .gitignore safety rules, and .sopsy.yml. Idempotent — re-running is always safe.

diagnostics

doctor health checks

macOS, Apple Silicon, Secure Enclave, Touch ID, every external tool, and repo health — a colorful report you can paste straight into an issue. sopsy deps is the remedy: it installs whatever doctor found missing.

edit

Friendlier edits

Open an encrypted file through SOPS with automatic file-type detection and nicer errors. Your $EDITOR, your workflow — none of the flag-juggling.

secrets

Scriptable encrypt / decrypt

One-shot, no-editor crypto for scripts and CI. sopsy encrypt .env -o .env.encrypted and sopsy decrypt .env.encrypted (to stdout, or -o a file). Structured .env / JSON / YAML / INI encrypt values only; everything else becomes one binary blob. Pipe decrypted values straight into your shell — via direnv in .envrc or a wrapper around your app — with no plaintext ever hitting disk:eval "$(sopsy decrypt .env.encrypted | sed -E '/^#/d; /^$/d; s/^([A-Z])/export \1/g')"

recipients

Recipient management

Add, remove, and list who can decrypt. Every change re-wraps existing secrets via sops updatekeys, so the key set never drifts from the ciphertext.

check

A CI gate that bites

Seven hygiene invariants, a pass/fail checklist, and a non-zero exit the moment a plaintext secret is tracked. Needs no key — runs on Linux runners too.

join / approve

Self-service onboarding

Newcomers sopsy join to mint a key and record a pending request — no access granted yet. Any active member sopsy approves: vouch for the key, promote it, re-key. Requests expire after 72h by default.

provenance

A built-in audit trail

Every member carries their name, username, when they requested access, and who granted it — approved_by recorded as "Konstantin Gredeskoul (kig)". sopsy recipient list shows it all in one table.

integrity

Tamper-evident config

.sopsy.sha — a SHA-256 checksum of .sopsy.yml plus the admin public key — is refreshed on every write and verified on every read, so hand edits are surfaced instead of silently trusted. sopsy doctor repairs it.

ci

One-secret CI decryption

sopsy recipient ci runs a guided ceremony that provisions a portable decryption key for your pipeline. Store one CI secret — SOPS_AGE_KEY — and a Linux runner with nothing but sops installed can decrypt.

How encryption flows

Plaintext stays local. Only ciphertext ships.

sopsy shells out to sops, which uses age recipients drawn from.sops.yaml. The primary path is dotenv: a plaintext.env becomes an encrypted .env.encrypted that's safe to commit.

Only encrypted files and public metadata belong in Git. The plaintext.env must always stay on your machine.

01

.env · plaintext

Your real KEY=value secrets. Gitignored automatically — never committed.

02

sops --encrypt --input-type dotenv

Recipients come from .sops.yaml creation_rules. sopsy never touches ciphertext itself.

03

.env.encrypted · committed

Encrypted values + sops metadata. Safe to push — only public recipients live here.

04

sopsy edit → decrypt → re-encrypt

A Touch ID prompt releases your Enclave key, opens your editor, and re-encrypts on save.

The Secure Enclave security model

The private key never leaves your Mac.

  • Generated inside the chip. On Apple Silicon the private key is created within the Secure Enclave and bound to that hardware — it cannot be read, copied, or exported by anyone or anything.
  • Released only with Touch ID. Decryption prompts your fingerprint. The Enclave hands the key to sops for one operation, then it is gone again.
  • Only the public recipient travels. The age1se1… string is safe to commit and safe to share. It is all a teammate or admin needs to grant you access.
  • Lose the device, keep the repo. Because the key is bound to one Mac, a break-glass recipient is what keeps secrets recoverable if every Enclave device is lost.

Encryption & team onboarding

Access is a pull request — and a human vouch.

Every teammate must be admitted before they can decrypt. A stranger who clones the repo, encrypted files and all, can never open them — they were never added to the recipient set. The first encryptor is the prime, and holds the offline break-glass pair in a vault for the day every laptop is lost.

# Alan Turing's MacBook — branch off maingit checkout -b sopsy/add/alan-turing# request to join, and stage the change with --gitsopsy join "Alan Turing" --git# …trade the thumb for --without-touch-id ➜ pending request written to .sopsy.yml ✔ git add .sopsy.yml .sopsy.sha — staged for you ◇ ready-to-paste commit + PR printed below ↓git commit -m "Request access" && git push

# Any already-approved engineer — same branchgit fetch && git checkout sopsy/add/alan-turing# vouch out of band, then approve + stage with --gitsopsy approve "Alan Turing" --git# …or bare `sopsy approve` to triage every request ✔ re-keyed every secret for the new recipient ✔ staged the re-keyed files — commit + PR printedgit commit -m "Approve Alan Turing" && git push ✔ merge the PR — Alan can now decrypt

Add --git to any command that changes files — init, join,approve, edit, and everyrecipient mutation — and sopsy git adds exactly the files it touched (never your plaintext .env) and prints ready-to-paste commit and pull-request commands. The secrets themselves are written with the scriptable shorthandsopsy encrypt .env -o .env.encrypted — no editor required.

Command reference

One verb per job. Every prompt has a flag.

sopsyinit

Bootstrap an encrypted repository: verify the toolchain, acquire an age recipient, and write every config file. Idempotent — keeps existing files unless --force.

sopsydoctor

Print a grouped health report — system, tools, repository, recipients — and always exit 0. Repairs a stale .sopsy.sha checksum (its only write). Safe to paste into a bug report.

sopsyjoin<name> [-t]

Self-service onboarding (alias of request-access): generate your Secure Enclave key and record a pending request in .sopsy.yml. Commit, open a PR, and ask any member to approve. Pass -t / --without-touch-id to mint the key with no Touch ID gate — handy for direnv and many-window workflows.

sopsyapprove<name>

Any active member vouches for a pending key, promotes it into .sops.yaml, and re-keys every secret — recording approved_by and approved_at for the audit trail, with atomic rollback if the re-key fails. Refuses requests older than join_request_ttl (default 72h).

sopsyedit<file>

Open an encrypted file in your editor through SOPS, with friendly errors and automatic file-type detection. Resolution: --editor → $EDITOR → $VISUAL → vi.

sopsyrecipientadd · remove · list · keygen · break-glass · ci

Manage who can decrypt. add/remove keep .sopsy.yml and .sops.yaml in sync and re-key secrets; list prints every member with their approval provenance; break-glass and ci run guided portable-key ceremonies; keygen mints an Enclave key without registering it.

sopsyencrypt · decrypt<file>

One-shot, scriptable crypto outside the editor. sopsy encrypt writes a .encrypted artifact (or stdout); sopsy decrypt prints plaintext to stdout or -o a file — ideal for direnv and CI. Structured .env/yaml/json/ini encrypt values only; anything else as one blob.

sopsytypesalias: list-supported-types

Print the file formats sopsy understands and which extensions auto-detect to each: dotenv, yaml, json, ini (value-encrypted) plus binary (whole-file). Takes no flags; the longer list-supported-types name still works.

sopsycheck

The CI gate: seven hygiene invariants, a pass/fail checklist, non-zero exit on any violation. Never decrypts — needs no key, runs anywhere.

sopsydeps

Install the external tools (sops, age, age-plugin-se) with Homebrew. Probes first, installs only what is missing. The remedy that pairs with doctor: --check fails CI if anything is absent, --dry-run previews the brew command.

sopsycompletion<shell>

Generate shell completions for bash, zsh, fish, powershell, and elvish — derived from the CLI definition, so they never drift from the real flags.

sopsy--help· global --git

A clap-based CLI: sopsy --help and sopsy <command> --help are always authoritative. Every interactive prompt has an equivalent flag. The global --git flag stages exactly the files a command changed and prints ready-to-paste commit + PR instructions — a no-op when nothing was modified.

Install

Up and running in one command.

$brew install kigster/tap/sopsy

Installs a prebuilt binary — no Rust toolchain — and pulls the tools sopsy orchestrates (sops, age, and age-plugin-se) automatically. Nothing else to set up.

From crates.io

requires Rust

$cargo install sopsy

Builds from source with Cargo. You'll still need sops, age, and age-plugin-se on your PATH — or run sopsy deps to fetch them.

macOS-first (v1.3.0). Homebrew installs the prebuilt binary and every prerequisite; the Cargo path needs the Rust toolchain.

The CI gate

Seven invariants stand between you and a leak.

sopsy check is the sops-hygiene gate for CI and your pre-commit hook — it verifies the encryption discipline itself, and pairs with content scanners like gitleaks (below). It validates encrypted files by their on-disk sops metadata — never by decrypting — so it needs no private key and no Secure Enclave. Drop it on a Linux runner and it just works.

$sopsy check && echo "secrets hygiene: OK"

And when a job must actually decrypt — deploys, integration tests — sopsy recipient ci provisions a portable key in one guided ceremony. Store a single CI secret, SOPS_AGE_KEY, and a Linux runner with nothing but sops installed can decrypt. Never reuse the break-glass key for CI — offline/few-hands and online/every-run are opposite threat models.

$sopsy recipient ci

01.env is not tracked by git

02.env is gitignored

03.sops.yaml valid + has creation_rules

04every encrypted file matches a creation rule

05no plaintext secrets tracked

06every encrypted file carries sops metadata

07a break-glass recipient is configured

all checks passed · exit 0

Defense in depth

Stop secrets before the commit ever lands.

Don't rely on remembering. sopsy setup pre-commit installs a lefthook hook that runs three complementary checks before every commit; sopsy setup github-workflow adds a CI job that scans the entire repository.

$sopsy setup pre-commit

$sopsy setup github-workflow

sopsy check

Is the encryption intact?

The structural gate. Confirms sopsy's seven invariants — plaintext .env never tracked, every managed file genuinely encrypted, a break-glass recipient present. It knows which files must be encrypted, so it's deterministic and needs no key — but it doesn't read file contents looking for secret-shaped strings.

gitleaks

Does anything look like a token?

The pattern gate. Regex + entropy, git-aware. Catches known secret shapes — cloud keys, tokens, private keys — anywhere in your staged diff, including files sopsy never manages.

detect-secrets

Anything new since we audited?

The baseline gate. Scans against a committed .secrets.baseline you triage once; only new, un-audited findings block the commit — so noise stays low and real leaks stand out.

sopsy check proves the sops workflow itself is correct;gitleaks and detect-secrets read the content for secrets that never went through sops at all. What one heuristic misses, another catches — and because the pre-commit hook is advisory (--no-verify skips it), the github-workflow is the gate that can't be bypassed.

Break-glass keys

Set up disaster recovery on day one.

A break-glass key is a separate emergency age key pair — portable by design, stored offline in a vault and shared with only a few admins. If every developer's Secure Enclave device is lost, it can still decrypt and re-key the repository. sopsy recipient break-glass runs the whole ceremony in one command: generate, hand off, wait for ENTER, delete the local copies, register and re-key.

Lose your only Enclave device with no break-glass key, and every secret becomes permanently undecryptable. That's why doctor and check nag until one exists — and sopsy refuses to remove the sole break-glass recipient. The same guided ceremony powers sopsy recipient ci for your pipeline — but the two keys must never be the same: offline/few-hands and online/every-run are opposite threat models.

# one guided ceremony: generate → store → delete → registersopsy recipient break-glass -o break-glass✔ wrote break-glass.private + break-glass.public⚠ copy both into 1Password / a vault, then continue press ENTER once the keys are saved … ✔ deleted both local key files ✔ registered break-glass recipient✔ re-keyed every secret (sops updatekeys)

Further reading

Everything you need to run it for real.

Make SOPS delightful.

Small, fast, colorful, and macOS-first. Your secrets stay in Git — encrypted — while the key that decrypts them never leaves the chip.

$brew install kigster/tap/sopsy

$cargo install sopsy