Commit the reference, keep the secret in the vault
A plaintext .env file is one careless git add away from a leaked API key. Here is how a committed file of references works instead, and where its protection stops.
13 Oct 2026 · T. Krause · 5 min read
Most projects start with a .env file and one line in .gitignore. That line is the only thing between your Stripe key and the public history of the repository. It holds until the day someone sets up a new repo, forgets the ignore file and runs git add -A.
The usual fix is a template. You commit .env.example with empty values and trust everyone to fill in their own copy. That keeps the secret out of git, and it keeps the knowledge out too. Half a year later nobody remembers which key the staging server uses, and the example file has quietly drifted from the real one.
2Password takes a different route. The project commits a file that names every secret precisely and contains none of them.
What a reference file looks like
In a project that uses 2Password, the root holds a .env.2pw file. It reads like a normal env file, except that the values are addresses:
OPENAI_API_KEY=2pw://Dev/OpenAI/credential
STRIPE_SECRET_KEY=2pw://Dev/Stripe/credential
DATABASE_URL="postgres://app:{{ 2pw://Dev/Postgres/password }}@localhost:5432/app"
NODE_ENV=development
A reference has the form 2pw://<vault>/<item>/<field>. Here Dev is the vault, OpenAI the item and credential the field. Leave out the field and you get the item's primary secret. Leave out the vault and 2Password searches all vaults, which works as long as the item title is unique. Spaces and a few special characters are URL encoded, so an item called "My Key" becomes My%20Key.
Two details make the file usable in real projects. Plain values like NODE_ENV=development sit right next to references, so configuration that isn't secret needs no second file. And the {{ ... }} form lets a reference live inside a longer string. Connection URLs need that, because the password is only one piece of the value.
This file is safe to commit. Anyone who clones the repo learns that the project needs an OpenAI key stored under Dev/OpenAI. They don't learn the key. A new teammate reads the file and knows which items to create in their own vault. The file can't drift from the real configuration, because it is the real configuration.
How 2pw run puts the values back
Your code still reads process.env.OPENAI_API_KEY or os.environ["OPENAI_API_KEY"]. Nothing in the application changes. What changes is how you start it:
2pw run -- npm run dev
2pw run -- python manage.py runserver
2pw run --env-file .env.2pw.prod -- ./deploy.sh
2pw run looks for .env.2pw in the current directory and its parents. It asks the local vault agent to resolve every reference and starts your command with the real values in its environment. When the process exits, the values go with it. They never land in a file on disk.
The lookup has guard rails. 2pw run only uses a .env.2pw that you own and that nobody else can write to, and the search stops at your home directory. A file someone drops into a shared parent folder won't be picked up. The command also resolves environment variables that already hold a 2pw:// reference, so references exported by a parent shell work the same way.
Some tools don't fit the run pattern, and for those there are other ways in:
2pw read 2pw://Dev/OpenAI/credential # print one value
eval "$(2pw env --format shell)" # export into the current shell
2pw inject -i config.tpl.yaml -o config.yaml # fill {{ 2pw://... }} in any file
The eval line is also what goes into a direnv .envrc. Files written by 2pw inject -o always get mode 600. Node.js and Python have small SDKs with a loadEnv() or load_env() call that reads .env.2pw into the process environment.
Moving an existing .env over
You probably already have a .env with real keys in it. Retyping them into a vault is the kind of chore that never happens, so the CLI does it in one command:
2pw import-env .env --item my-project --vault Dev
rm .env
import-env reads the file and picks out what looks secret. That means variables ending in _KEY, _SECRET or _TOKEN, passwords, and URLs with credentials in them. Those go into one vault item called my-project, with fields named after the variables. Then it writes a .env.2pw with references where the secrets were and leaves the non-secret values as they are.
Read the generated file before you run rm. The import works on names and patterns, so a secret stored under an odd variable name can stay in plaintext. Move those by hand with 2pw edit, check again, then delete the original.
One thing the import can't repair is history. If the old .env was ever committed, the keys still sit in past commits. Rotating them at the provider is the only real fix, and it is worth doing the same afternoon.
Where the protection stops
This is easy to oversell, so here is the limit in plain words. While the vault is unlocked, any program running as your macOS user can ask the agent for secrets. A plaintext .env has the same exposure, since any process running as you can read it. 2Password doesn't change that part of the threat model, and I'd distrust any local tool that claimed it did.
What it changes is everything around that moment. The secret stops living in a file inside each project folder and inside every backup and archive of those folders. The vault is one encrypted file at ~/.2password/vault.2pw. The agent's socket accepts only your user. By default the agent locks after four hours of inactivity, and the Mac app locks the vault when the Mac sleeps or the screen locks. A locked vault answers nobody.
You also get a record. 2pw log lists recent secret access, showing which references were read and from which directory. Values are never logged. If a script you don't recognise turns up reading your production database password, you will see it there. A plaintext file can't tell you anything of the sort.
The habit that follows is short. Lock the vault with 2pw lock or ⌘L when you step away, and look at 2pw log now and then.
Pick a project whose .env you'd hate to see in a public repo. Install 2Password, create a vault, run 2pw import-env on that file and start the dev server through 2pw run. If the app behaves exactly as before, commit the new .env.2pw and delete the old file. The download, the Node.js 22 requirement and the full CLI reference are on the 2Password page.