Security

What is encrypted, what our server can still see, and where the guarantee stops. The last part is the part most pages leave out, so it is on this page too.

Encrypted

Your values are encrypted on your Mac, with age, before they leave it.

Unreadable to us

Encrypted to the public key of every current member. Our relay stores the result and cannot decrypt it: no private key, no decrypt-capable dependency, no admin override that could become one.

Your key

Generated on your Mac and kept in the macOS Keychain. Never written to a file by us, never transmitted.

Nothing invented

We did not invent any cryptography. age, X25519 recipients only.

We are not going to call this unhackable, military-grade, or zero-risk. Everything below is the shape of what it actually does.

How does push-to-Vercel work if you can't read the values?

This is the obvious contradiction to point at, and it deserves a straight answer rather than a deflection. There is a real trade-off here. It is not the one the question implies.

What the claim actually is

“End-to-end encrypted” is a statement about our servers. It has never meant that your values exist nowhere in plaintext. They obviously do. They are plaintext in your .env file. They are plaintext in your editor. They are plaintext in your Mac's memory the moment you click the eye icon to look at one. Decrypting values on your machine is the entire purpose of the app.

The guarantee is about who else can. Our relay cannot, and the Vercel integration does not change that, because our relay is not in the path.

What actually happens when you push

  1. The app decrypts the values you ticked, on your Mac, using the private key in your Keychain.
  2. It sends them to https://api.vercel.com directly from your machine, over Vercel's own API.
  3. Our relay is not involved in any part of that. Its source contains no reference to Vercel at all: no endpoint, no token handling, no code path.
  4. The Vercel token that authorises the call is one you create in your Vercel account and paste into the app. It goes into your Keychain and is never sent to us.

Why you paste a token instead of clicking “Connect with Vercel”

Because a Connect button would have to route your Vercel token through our server, and we are not willing to hold it.

Vercel's OAuth token exchange requires a client_secret. A client_secret cannot live inside a desktop app that anybody can download and unpack, so the exchange would have to happen on our infrastructure. And a Vercel access token reads your environment variables in plaintext.

Our relay must never hold a credential that reads the values it is built to be unable to read. Being unable to decrypt your ciphertext would mean nothing if we held a key that fetched the same secrets from somewhere else.

The consequence is a worse first run: you go to Vercel, create a token, and paste it. That is a deliberate trade of convenience for a claim we can actually make. We will revisit it if Vercel ships PKCE or a device-authorisation grant, neither of which exists today.

The part that is a genuine trade-off

Once you push to Vercel, Vercel has your values, and Vercel can read them.

That is not a leak or a loophole. It is what pushing means: your builds need those values, so they have to arrive in a form Vercel can use. No amount of encryption on our side changes what a third party can do with a secret you deliberately sent them.

So the honest framing is this. MiniEnv's guarantee covers our infrastructure. It does not, and cannot, extend to a service you choose to hand values to. Anyone claiming otherwise about any integration is selling something.

What we can do is make it a decision rather than a background process:

  • Nothing syncs automatically, ever. A push happens because you opened the dialog and pressed the button.
  • You see a diff first: what Vercel currently holds for each variable, and what your push will do about it, row by row.
  • Every row has a tick box, and the suggestion is overrulable in both directions. The button says how many will be written.
  • Values that already match are unticked by default, so a push touches as little as possible.
  • It writes to one Vercel environment. The other two are not read and not written.

If you never use the integration, none of this applies to you. Your values leave your machine only as ciphertext.

Import, and the thing Vercel will not give back

Import is the same path in reverse: your Mac reads from Vercel directly and encrypts on arrival. It has one asymmetry worth knowing before you rely on it.

A Vercel variable marked Sensitive can be written but never read back, by anyone, through their API. That is Vercel's design, not a permission we lack. Those variables come back as a name with no value. The app lists them as unreadable and asks you to paste those few by hand. It will not import them as empty strings that later overwrite something real, and it will not report “imported 24” when six of them arrived blank.

Which means import works least well for people who followed Vercel's own security advice. We would rather say that than quietly write empty values.

What our server can see if it is breached tomorrow

A full compromise (database dump, disk image, hostile operator) yields this and nothing more.

Every variable's valueEncrypted, unreadable
Your private keyNever leaves your Keychain
Variable names, e.g. STRIPE_SECRET_KEYVisible
Email addresses and public keysVisible
Project and team names, who belongs to whatVisible
When a value last changed, and how many existVisible

Names being visible is a real trade-off, not an oversight: the app has to show you a list of your variables without decrypting them. The practical advice is short. Do not put a secret in a variable name.

What this does not protect against

Stated plainly, because a security model that oversells itself is worse than none.

A compromised laptop

MiniEnv protects secrets in transit and at rest on the server. The moment you pull a value it exists in plaintext in a .env file on your disk. We write that file mode 600, but it is readable by you and by anything running as you. Malware on an endpoint reads the .env and the Keychain. Nothing here changes that.

Anything already downloaded

Removing someone is not retroactive, and cannot be. It re-encrypts the project so they cannot read future values. The copy already on their laptop keeps working, forever.

If a departing person had a secret, that secret is compromised. Rotate it at the source: the database, the provider, not just here. The app says this at the moment you remove someone, and no part of the product is allowed to soften it.

A stolen access token

A stolen token gets an attacker your project's ciphertext and metadata. It does not get them a single plaintext value: that needs the private key, which stays in the Keychain and is never transmitted. Token theft is a real incident. It is not a disclosure of your values.

Traffic analysis

The relay learns who belongs to which project, how many variables exist, what they are named, and when they change.

How the pieces work

Adding and removing people

Both re-encrypt the whole project, on your Mac, before the change is accepted. There is no window where a removed person can still read new values, and no background job that gets to it later. A membership change that cannot be re-encrypted is refused rather than half-applied.

Only someone who can already decrypt a project can produce its re-encryption. That is physics, not policy: you cannot re-encrypt what you cannot read.

If you lose your key

Two different things get asked here, and only one of them is refused on principle.

Recovery we can perform on your behalf is the one we will not build. No escrow, no backup we hold, no admin override, and there is not going to be one. A support-friendly recovery mechanism is a decryption mechanism, and the moment it exists everything else on this page stops being a property and becomes a promise.

A copy of your own key, held by you, is a different question, and we should be straight about where we are on it. Your private key is an age identity: one short line of text. Letting you keep a copy in your own password manager would give us nothing, would never touch our relay, and would not weaken anything above by one bit. We do not offer it today. The key is generated straight into the macOS Keychain and there is no export anywhere in the app or the command-line tool. That is a decision we are revisiting, not a property we are defending, and this page will say so on the day it changes.

So, as things actually stand: if other people are on the project, you do not need a backup. Any current member can re-encrypt it for your new key, on their Mac. If you lose the key and nobody else is on the project, the data is gone and we cannot get it back.

Rotating your key

Recording a new public key grants no access on its own. It records a key. Access resumes only when an existing member re-encrypts for it.

Being on a team is not access

Team membership is a roster entry. Reading a project requires membership of that project, because that is what the ciphertext is encrypted to. Neither is a substitute for the other.

The web dashboard cannot decrypt

It is read-only and cannot become otherwise. A browser cannot reach your Keychain, so the page holds no private key: it cannot show you a value, and it cannot re-encrypt, which is what every membership change requires. It will never ask you to paste a private key, and there is no server-side re-encryption endpoint behind it.

Known limitations we have not fixed

These are real, known, and written down rather than discovered. They are the reasons this is a small-team tool today.

  1. Membership changes have a narrow race. Our storage layer has no compare-and-swap, so two membership changes issued in the same instant can both pass the version check and the later write wins, losing the earlier one's re-encryption. The version check narrows the window; it does not close it. The fix is a per-project single-threaded writer, and the storage layer is shaped to take one.
  2. The command-line tool cannot wipe plaintext from memory. JavaScript strings are immutable and garbage-collected, so a decrypted value may sit in the heap until collected. The Rust core, which is where values live longest, zeroizes properly.
  3. The command-line tool's Keychain item is scoped by service and account only. A tighter scope needs a signed binary with entitlements, and the CLI runs under unsigned node. Any process running as you can read it.
  4. The relay verifies the recipient count, not the recipient set. age headers deliberately do not contain recipient public keys, so the relay can check that a blob has exactly one recipient per current member, catching a client that forgot someone. It cannot catch a client that encrypted to the wrong people. That guarantee lives in the client.
  5. Every member can read every value in their projects. There is no per-variable access control. Roles are owner and member, and nothing finer.
  6. Apple Silicon Macs only. The key lives in the macOS Keychain, so there is no Windows or Linux build.

Don't take our word for it

The claim that our relay cannot decrypt is enforced by a check that runs on every test run: it walks the relay's dependency tree and fails the build if anything decrypt-capable is linked in. It has been verified to actually catch a violation rather than merely existing.

The revocation guarantee is asserted end to end, in both directions: that a removed member cannot open the re-encrypted values, and that the copy they already downloaded still works. If only the first were tested, the test would pass on a product that quietly lied about the second.

# crypto invariants and revocation
cd core  && cargo test --features conformance

# the relay's API, plus the dependency audit
cd relay && npm test

# the Rust and Node implementations agree on the wire format
cd cli   && npm test

# two real users, a real revocation, end to end
bash scripts/e2e.sh

Two independent implementations of one documented protocol, one in Rust and one in Node, sharing no code. They are tested against each other, which is how a wire format stays honest.

Found something?

Write to support@minienv.dev. If it is a vulnerability, say so in the subject line and we will come back to you before doing anything else.

©2026 MiniEnv