Git for secrets.
One encrypted file for every secret your app refuses to start without — env vars, TLS certificates, service-account JSON, kubeconfigs. Follows your branch. Merges like code. No cloud. No accounts. No shared master password.
If you've built anything with more than one contributor, you've probably lived one of these:
a teammate adds a key to their local .env and forgets to send it. Someone pastes a
database URL into a Discord DM. You check out a branch and accidentally run it against production.
A sleepy git add . scoops up a raw key and it sits in Git history forever.
And the one nobody warns you about: half your secrets aren't strings. The TLS
certificate in certs/. The service-account JSON someone sent you at 11pm. The
kubeconfig with production cluster access. They don't fit in .env, so they end up on
a shared drive, in a wiki attachment, or in a "temporary" commit that quietly became permanent.
EnvSeal seals the files too — and injects them at runtime as a private temp path.
None of these are exotic. They're what happens when secrets live outside version control and the code that needs them doesn't.
The fix isn't a better .env file. It's putting your vault somewhere Git already
knows how to move. EnvSeal makes your secrets a file in your repo — encrypted
for the people who need them, switched by branch, and reviewed in pull requests like anything
else you commit.
Two columns. That's the whole pitch.
You set up the vault, add your teammate as a recipient, commit, and push. They pull, run their app, and it works. Same keys, same certificates, correct branch, zero DMs.
Ten decisions that shape everything else.
git blame, and history apply to secrets the same way they apply to code.envseal run reads git rev-parse --abbrev-ref HEAD and applies the tag with the same name, if one exists. No manual swapping.rotate..env files and their variants before they reach Git history. Examples like .env.example and the encrypted vault pass through.ours or theirs.envseal edit opens the vault in your $EDITOR, writes only the diff back, and zeroes the temporary plaintext on exit. Bulk edits without dozens of set calls.--override KEY for one key, --with-global for everything.-e staging or -e prod — no extra repos, no extra setup..p12 keystores, SSH keys, and kubeconfigs are sealed in the same vault.
envseal run decrypts each one to a private temp file, points an env var at it, and overwrites and deletes it when the process exits.
envseal get KEY -o PATH writes it back out — new files get 0600 (or 0700 if executable) — when a tool insists on a fixed path.
No shared drive. No wiki attachment. No stale copy with production access.
No secrets tool is perfect for every setup.
Each tool below solves the same problems — accidental commits, environment switching, team access,
and the certificate that never fit in a .env — just through a different mechanism.
This table shows how each one addresses the same concern, so you can judge which trade-offs
fit your team. If you need a dashboard or audit log, use Doppler or Infisical. If you're deep in
Kubernetes and cloud KMS, SOPS fits better. EnvSeal is for the team in between.
| Plain .env | dotenvx | Mozilla SOPS | EnvSeal | Doppler / Infisical | |
|---|---|---|---|---|---|
| Cost | Free | Free; Member $5/mo, Business $90/mo | Free | Free, no account needed | Doppler: free for 3 users, $8/mo per additional user. Infisical: free up to 5 identities, Pro $23/identity/mo ($20 annual) |
| Works offline | Yes | Yes — local CLI | Yes with age. Cloud KMS modes need connectivity | Yes, fully | Cloud-first; offline use needs a prior cache. Infisical's MIT core is free to self-host; supported air-gapped is Enterprise |
| Environment / branch switching | Manual file swap (.env, .env.staging) |
Manual -f .env.production at invocation |
Per-file, driven by CI env vars or shell aliases | envseal run reads the current Git branch and applies a matching tag |
--config flag at runtime, or a project setting in the dashboard |
| Preventing accidental commits | Relies on .gitignore discipline |
Optional dotenvx precommit hook |
.gitignore + decrypt only in CI; nothing plaintext is committed |
Built-in pre-commit shield blocks staged .env files (examples pass) |
Nothing to commit — secrets never touch the repo |
| Team onboarding | Manual file sharing | Share .env.keys (one key per environment) |
Add each age key to .sops.yaml, run sops updatekeys |
Add a GitHub handle, or paste an age key | Invite via dashboard; SSO available on paid tiers |
| Merge behavior | Git text merge | Git text merge on the encrypted file | Git text merge on YAML/JSON; conflicts need decrypt → merge → re-encrypt | Native merge driver with ours / theirs |
N/A — cloud-managed, no file to merge |
| Personal / local overrides | .env.local (gitignored) |
.env.local overrides .env |
Separate .sops.yaml per developer, or shell env vars |
Global vault + --override / --with-global |
Personal configs in dashboard, or --personal flag |
| Files / certificates | Strings only | Strings only | Encrypts any file, but you wire injection yourself | Seals files and injects a private temp path at runtime | File/mount support varies by plan and platform |
| Best suited for | Solo hackathon, no real secrets | Solo/small teams wanting encrypted .env |
K8s / GitOps teams on cloud KMS | Small-to-mid teams who want env vars and file secrets in Git | Orgs wanting GUI, audit logs, and a budget |
Verified November 2026. dotenvx does more than a table row can capture — it has real encryption, rotation, and git-diff audit. The architectural difference: dotenvx uses a single shared private key per environment, while EnvSeal seals to individual recipients. SOPS encrypts files well; the gap is that recipient management, merge behavior, and runtime injection are left for you to wire up.
Read this before you trust any encryption tool.
Most encryption tools hide their limits in the fine print. We put them on the landing page. If any of them is a dealbreaker, EnvSeal is not for you — and that's fine.
envseal recipient rm re-wraps the vault key for everyone else, but it keeps the
same data key. Anyone who unlocked the vault before still holds that key and could decrypt
future commits. Run envseal rotate immediately after removing
someone — that generates a new data key. And Git history is permanent: anyone who cloned
earlier can still decrypt old commits with the old key. If someone leaves under bad terms,
rotate the actual credentials too. Removing their access is taking away the
house key after they've already made a copy.
envseal rotate protects the future, not the past.--exp flag is a runtime check performed by EnvSeal itself. Because everything
is offline, there's no server refusing an expired token — a sufficiently determined attacker
with the raw payload could bypass the clock. Treat expiry as a guardrail, not a guarantee.
If a CI runner is ever compromised, rotate the vault and the underlying secrets immediately.
- Git-native by design. The vault is a file in your repo, not a parallel system you have to remember to sync.
- Free and offline. No accounts, no cloud, no telemetry. Works on a plane or behind a corporate firewall.
- Branch-aware. Run
envseal runonstagingand it applies thestagingtag automatically — no flag, no copy-paste. - Per-recipient access. Adding a teammate is a Git commit, not a shared-key redistribution. Removing one needs a follow-up
rotate. - Files, not just strings. Certificates, service-account JSON, and kubeconfigs are sealed and injected as private temp paths — not shared as attachments.
- Global + local vault mixing. Personal tokens stay in a system vault; projects only pull what they need.
- No web dashboard, no audit log. If you need to see who accessed what and when, use a cloud tool.
- No non-technical UI. Everyone on the team touches a terminal. If that's a dealbreaker, look at Doppler or Infisical.
- You can lock yourself out permanently. Zero-trust means zero recovery. Back up your key.
- Kubernetes / cloud-KMS shops. SOPS fits your existing pipeline better than bolting EnvSeal on.
The whole CLI, at a glance.
The commands and flags you'll actually use. If you've used Git, this will feel familiar — every command is a single verb, and most have a natural Git analogue you already know.
Core
-r adds an initial recipient.--file.-o.--override or --with-global.eval. Prefer run.--force skips confirmation.Sealed files
run injects $VAR as a private temp path; the variable defaults to KEY.0600 (or 0700 if executable); overwriting keeps the existing file's mode. Pipe stdout for raw bytes.Global vault override
--with-global.--override.base.Access & CI
@user fetches GitHub SSH keys. Falls back to a manual age key if they have none.rotate to change the data key..env file. Original left in place..env file with strict 0600 permissions.Global flags
-e staging loads .staging.envseal.envseal --global init.ENVSEAL_IDENTITY and ENVSEAL_IDENTITY_FILE.
Your secrets already live in the repo.
They're just not encrypted yet.
One vault. One command. No more "does anyone have the new key — or the certificate?"
envseal run -- docker compose up,
or inject only the keys you need into the container environment.
.envseal at the root, or put a separate .envseal in
each package — EnvSeal uses the nearest one. Use the -e profile mechanism when
you want multiple vaults side by side.
--override and --with-global?--override KEY1,KEY2 pulls only the named keys from the global
vault. --with-global overlays all keys from the global vault,
with global values winning on collision. They are mutually exclusive.
envseal set TLS_CERT --file ./certs/server.pem --env-var TLS_CERT_PATH.
During envseal run, the file is decrypted to a private temp path, the env var points
at it, and it's overwritten and deleted when the process exits. Need a fixed path? Use
envseal get TLS_CERT -o ./server.pem. export and load
only handle text variables: export warns for every skipped file secret,
load warns when you explicitly name one.
git -c envseal.merge.strategy=ours merge … or theirs.
envseal recipient add @handle will fail. Ask them to run
envseal recipient id, which creates or reuses their local identity. They send you
the output, and you add it with envseal recipient add "age1...". Same
result, one extra paste.