envsec vs dotenv vs 1Password CLI vs direnv: which one, when
Your app reads a database password and an API key from environment variables. The open questions are where those values live when nothing is running, and how they reach the process when something is. dotenv, direnv, 1Password CLI and envsec all come up when you search for an answer, and they answer different parts of it.
I wrote envsec, so read this with that in mind. I've tried to be fair: for several of the cases below envsec is the wrong tool, and I say so. The comparison page has the feature grid; this is the long version, with the trade-offs.
Two questions, four tools
Every secrets setup answers two questions: where is the value stored, and how does it get into the process that needs it? The four tools split like this:
- dotenv only answers the second question. It reads a file and copies its values into
process.env. Storage is whatever file you wrote. - direnv also answers only the second question, at the shell level: it loads variables when you enter a directory and unloads them when you leave.
- 1Password CLI answers both. Values live in a 1Password vault, and
ophands them to your command at run time. - envsec answers both, locally. Values live in your operating system's credential store, and envsec hands them to a command or prints them for your shell.
That's why "dotenv or envsec" is partly a false choice, and why direnv pairs well with any of the others.
dotenv: the default, and fine for configuration
dotenv is a zero-dependency Node.js module that loads a .env file into process.env. Many frameworks read .env files on their own, and loaders exist for most languages, so the format is everywhere.
- Where secrets live: in a plaintext file in your project. The dotenv README answers "Should I commit my .env file?" with a no.
- Team sharing: out of band: someone sends you the file or its contents. Its companion project dotenvx encrypts the values so the file can be committed, and you share the private key separately.
- Cost and offline: free (BSD-2-Clause), and it's a file, so it works anywhere.
- CI: usually you don't ship a
.envto CI at all. The platform sets environment variables, and dotenv leaves them alone: by default it never modifies a variable that is already set. - Ergonomics: hard to beat. Edit a file, call one function at startup.
dotenv is the better choice for non-secret configuration (ports, log levels, local URLs, feature flags), for a committed .env.example that documents what a project needs, and in containers and CI, where something else already injects the real values. The trouble starts when the same file also holds the database password. Any process running as you can read it: editor extensions, backup tools, a coding agent indexing your project.
direnv: per-directory environments
direnv is a shell extension. Before each prompt it looks for an .envrc in the current and parent directories, runs it in a bash subprocess, and applies the resulting changes to your shell. Leave the directory and the changes are undone. It has hooks for bash, zsh, fish and several other shells.
- Where secrets live: wherever
.envrcgets them. Anexport API_KEY=…line in.envrcis a plaintext file, the same as a.env. direnv's standard library can also load a.envfile for you. - Safety: direnv refuses to run a new or modified
.envrcuntil you approve it withdirenv allow. That protects you from a cloned repository running code when youcdinto it. - Team sharing, cost, offline: no sharing (it's about your shell), free (MIT), fully local.
- CI: not its job. CI jobs don't have an interactive shell moving between directories.
- Ergonomics: excellent once set up. You
cdinto a project and the variables are there.
One thing to know before you load secrets with it: the variables end up in your interactive shell, so every command you start from that shell inherits them. direnv also keeps its own bookkeeping in a DIRENV_DIFF variable. On my machine (direnv 2.37.1) that variable contains the values it set, compressed but not encrypted.
direnv is the better choice for switching per-project settings that aren't secret, and as glue that calls a real secret store. More on that below.
1Password CLI: secrets for a team
1Password CLI (op) is the command-line client for a 1Password account. Its op run command reads environment variables whose values are secret references such as op://development/GitHub/credentials/personal_token, from your environment or an --env-file, and starts your command with the real values for the lifetime of that process.
- Where secrets live: in a vault in your 1Password account, not in the project.
- Team sharing: this is where it is strongest. Vaults are shared with teammates, access is granted and revoked centrally, and when someone changes a value, the next
op runpicks it up for everyone. - Cost: it needs a 1Password account, which is a paid subscription. The CLI isn't open source.
- Offline: I haven't tested how far
opgets without a network connection, and the pages I link here don't promise it, so I won't claim either way. - CI: service accounts scoped to specific vaults, and CI/CD integrations such as GitHub Actions. Laptops and pipelines read from the same source.
- Ergonomics: a committed file full of
op://references documents which secrets a project needs without containing any of them. Sign-in can use your fingerprint through the desktop app.
If more than a couple of people share production credentials, if you need to revoke someone's access when they leave, or if an auditor asks who can read what, 1Password CLI is the better choice. If your company already pays for 1Password, start there.
envsec: the OS keychain, from the command line
envsec stores each secret in the operating system's credential store: the macOS Keychain, the Secret Service on Linux (through secret-tool), or the Windows Credential Manager. Metadata (key names, timestamps, optional expiry dates) goes into a SQLite file, ~/.envsec/store.sqlite by default. Values never go into SQLite. Secrets are grouped by context, such as myapp.dev or stripe-api.prod.
$# Import an existing .env once$envsec -c myapp.dev load -i .env$# Run the app with every secret of the context injected$envsec -c myapp.dev run --inject 'npm run dev'$# Or pass one secret to one command$envsec -c myapp.dev run 'psql {db.url}'With run, a {db.url} placeholder becomes a reference to an environment variable that only the child process sees, so the value isn't pasted into the command line. There is also envsec shell for a subshell with the secrets loaded, envsec env for eval, and envsec env-file for tools that insist on a .env file (envsec records where it wrote them, and envsec audit lists them along with expired secrets).
- Where secrets live: in the OS credential store, encrypted by the OS and tied to your login.
- Team sharing:
envsec shareencrypts a context for one GPG recipient. You hand over a file; nothing syncs. Sharing secrets with GPG covers how it works. - Cost and offline: free (MIT), no account, and everything is local.
- CI: not designed for it. envsec needs a credential store, and a headless Linux runner doesn't have one unless you start it yourself. I do that with gnome-keyring to test envsec in CI, but for your pipeline secrets, use your CI provider's secret store.
What envsec doesn't do: sync between machines, permissions, an access log, or rotation. It tracks expiry dates you set with add --expires and reports them in audit, but it doesn't rotate anything. It also doesn't protect you from software running as your user: on my Mac, reading a secret that envsec stored doesn't show a prompt. The threat model post goes into the details.
At a glance
| envsec | dotenv | direnv | 1Password CLI | |
|---|---|---|---|---|
| Stores secrets in | OS credential store | Plaintext .env | Nothing of its own | 1Password vault |
| Encrypted at rest | Yes, by the OS | No (dotenvx: yes) | Not applicable | Yes |
| Gets values into | A command or your shell | process.env | Your shell | A command (op run) |
| Team sharing | One-off GPG file | Out of band | Out of band | Shared vaults |
| Access control | Your OS login | File permissions | File permissions | Per-vault permissions |
| Account needed | No | No | No | Yes |
| Cost | Free (MIT) | Free (BSD-2) | Free (MIT) | Paid subscription |
| Offline | Yes | Yes | Yes | Check their docs |
| CI | Not designed for it | Platform env vars | Not its job | Service accounts |
They combine better than they compete
direnv plus envsec. The lightest version uses direnv only to pick the envsec context for a directory. No secret enters your shell; envsec reads the ENVSEC_CONTEXT variable when you don't pass -c, so envsec run --inject inside that directory uses the right context:
# .envrc: choose the context for this directory, export no secrets
export ENVSEC_CONTEXT=myapp.devIf you want the secrets in your shell as plain variables, let direnv evaluate envsec env. direnv runs .envrc in bash, so the default bash syntax is the right one even if your interactive shell is fish. I tested this with direnv 2.37.1:
# .envrc: export every secret of the context into your shell
eval "$(envsec -c myapp.dev env)"The trade-off is the one above: the values now live in your shell and in DIRENV_DIFF, and direnv calls envsec again each time it reloads the directory. I prefer the first version.
dotenv plus envsec. Keep non-secret settings in .env and the secrets in envsec. Because dotenv doesn't override variables that are already set, the injected secrets win, and the file never needs to contain them:
$# .env holds PORT, LOG_LEVEL, API_BASE_URL. No secrets.$# envsec injects the secrets; dotenv won't override them.$envsec -c myapp.dev run --inject 'npm run dev'1Password plus envsec. Use 1Password for what the team shares and for CI, and envsec for what is only yours: personal tokens, side projects, the things you would otherwise keep in a .env in your home directory.
Which one, when
- dotenv for non-secret configuration, for templates like
.env.example, and wherever the platform injects variables for you (containers, CI). Keep secrets out of the file. - direnv for per-directory environments, and as glue between your shell and a secret store.
- 1Password CLI when a team shares secrets, when you need access control and revocation, or when CI and laptops should read from one source.
- envsec when you want the secrets on your own machine off disk and out of your projects, without an account or a network, and an occasional GPG file is enough sharing for you.
If you're starting from a folder of .env files, From .env to the keychain in five minutes walks through the move. The comparison page has the feature-by-feature grid.