What envsec does not protect you from
Every secrets tool has a threat model, whether or not it writes one down. This is envsec's, including the parts that don't flatter it. I checked each claim against the code, and I cite the file where it matters.
What envsec is for
envsec moves secret values out of plaintext files and into the OS credential store: the macOS Keychain through Apple's security tool, the Secret Service on Linux through secret-tool, and Windows Credential Manager through PowerShell. Key names, timestamps and expiry dates go into a SQLite file. Values leave the store when you run a command that needs them: get, run, shell, env, env-file, share, or the reveal key in the TUI.
The assets are the values. The threat it is designed for is accidental exposure: a commit, a grep, a backup of the project folder, a container build context, a coding agent opening the wrong file. It is not designed to stop code that already runs as your user.
| Protected | |
|---|---|
| Accidental commit of a secrets file | yes |
| grep, editor indexing, project backups | yes |
| Other local users (default permissions) | mostly |
| Processes running as you | no |
| Children of a process you gave secrets to | no |
| Root, or a stolen unencrypted disk | no |
What it does not protect you from
Anything running as you can read every secret
On macOS, envsec creates items with security add-generic-password and reads them with find-generic-password -w (packages/core/src/implementations/mac-os-keychain-access.ts). The tool's own help says that, by default, the application that creates an item is trusted to read it without a warning. So any process running as you can call security and get the value while the login keychain is unlocked, and in my tests no dialog appears.
On Linux, a keyring unlocked at login typically answers any client in your session over D-Bus. On Windows, generic credentials are readable by any process running as your user; that is how envsec itself reads them.
Contexts don't change this. A context is a naming scheme: the key db.password in myapp.prod becomes the service envsec.myapp.prod.db and the account password (packages/core/src/domain/secret-key.ts). There is no authorization between contexts.
Environment variables are not a vault
run --inject, shell and {key} placeholders all end the same way: the value sits in the environment of a child process. On Linux, /proc/<pid>/environ holds the environment a process started with, and it is readable by the same user and by root:
$# Linux: the environment of a process you own is one file read away$envsec -c myapp.dev run --inject 'sleep 60' &$tr '\0' '\n' < /proc/$(pgrep -n sleep)/environ | grep DATABASE_URLChildren inherit the environment, so the values reach everything the process starts. envsec run --inject 'npm install' would hand them to every dependency's install script. Inject into the command that needs the secrets, not into a whole toolchain. shell --no-inherit drops the parent environment except PATH, but the secrets are still there by design.
eval lasts as long as your shell
eval "$(envsec env)" doesn't put values in your history: the history records the eval line, not its output. It does put them in your interactive shell for the rest of the session, inherited by every command you start, including an editor or a coding agent. envsec env --unset prints the matching unset lines. For anything but a short session, prefer run, which scopes the values to one process.
The value passes through process arguments on macOS and Windows
To store a value on macOS, envsec runs security add-generic-password -U -s … -a … -w <value>. The value is base64-encoded with an envsec:b64: prefix, which is encoding, not encryption. While security runs, it is in that process's arguments, visible at least to other processes running as you. Apple's help for the command calls the -w option insecure for exactly this reason. On Windows, the value is embedded in the script passed to powershell.exe -Command, with the same exposure. On Linux, envsec writes it to secret-tool on stdin, which avoids it.
The window is short, but a same-user process polling the process list could catch it. If that is in your threat model, so is everything else in this section.
add --value puts the secret in your history
An inline value is saved by your shell's history and is part of envsec's own command line while it runs. On Linux, /proc/<pid>/cmdline is readable by every local user unless /proc is mounted with hidepid. Use the prompt, or pipe the value in on stdin from another program:
$# The value lands in shell history and in envsec's own arguments$envsec -c myapp.dev add stripe.key --value 'the-actual-secret'$# Masked prompt: one * per character, nothing in history$envsec -c myapp.dev add stripe.key$# ◆ Enter secret value: *****************$# ✔ Secret "stripe.key" stored in context "myapp.dev"The prompt echoes one * per character on a terminal, so a screen recording shows the length of the secret, not its content.
run goes through a shell
run and cmd run execute the command with /bin/sh, or cmd.exe on Windows (packages/cli/src/cli/execute-command.ts). A command template is code: only run templates you wrote. Saved templates live in the metadata database, so whoever can write that file can change what cmd run deploy executes.
Placeholder values are not pasted into the command text. envsec replaces each one with a reference to a variable it sets in the child's environment (packages/cli/src/cli/resolve-command.ts):
$envsec -c myapp.dev run 'psql "{database.url}"'$# /bin/sh runs: psql "$ENVSEC_0_DATABASE_URL"$# with ENVSEC_0_DATABASE_URL set in its environmentWith /bin/sh, a value containing ; or $(…) is expanded as data, not run. An unquoted reference is still split on whitespace, so quote your placeholders. On Windows, since envsec 1.1.3, the reference is !VAR! and the command runs under cmd /v:on: delayed expansion happens after cmd.exe has parsed the line, so & and | in a value stay literal. Earlier versions used %VAR%, which cmd.exe expands before it looks for those operators; if you are on one of them, prefer --inject and read the variable in your program.
The metadata database tells a story
~/.envsec/store.sqlite holds context names, key names, creation and update times, expiry dates, saved command templates and the paths of .env files you exported. No values, but enough to tell someone which services you use and where to look. A completion cache next to it lists contexts and keys too. envsec creates the directory with mode 0700 and both files with 0600, and resets the permissions of the database file and of ~/.envsec every time it opens them (packages/core/src/implementations/sqlite-metadata-store.ts). That keeps other local users out. It does nothing against processes running as you, or a backup tool that copies your home directory.
env-file and share write files
env-file writes a plaintext .env, which is what envsec exists to avoid; it is there as a bridge for tools that only read files. Since envsec 1.1.3 it creates the file with mode 0600 and tightens an existing file it overwrites; earlier versions used the default permissions from your umask, typically 0644, which other local users can read. envsec records the path, and envsec audit lists generated files and forgets the ones you deleted. It cannot track copies.
share encrypts with GPG using --trust-model always, so it encrypts to whichever key in your keyring matches the recipient, without checking its trust level. Verify the fingerprint before you send anything.
Expiry is a reminder, not a lock
--expires stores a date in the metadata. get prints a warning after it and audit lists the secret, but the value stays readable, and run and shell still inject it. Revoking a credential happens at the provider.
Root, malware, physical access and headless Linux
Root can read anything on the machine, including your keyring once it is unlocked. Malware running as you is the same as any other process running as you. Against a stolen laptop, envsec adds no encryption of its own: at rest, you rely on the credential store and on full-disk encryption. On a Linux server or container without a D-Bus session and a keyring daemon, the Secret Service may be missing, and envsec can't store anything there.
envsec has no clipboard feature. get prints to stdout, so the value ends up in your scrollback, in any terminal logging you have on, and in a screen share.
What the code does defend against
Inside that scope, these are the protections I could point to in the source:
- No shell between envsec and the credential store.
security,secret-toolandpowershell.exeare started withexecFileand an argument array, so names and values are never parsed by a shell. - Allowlisted names. A context matches
^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$, is at most 128 characters, has no path separators and is not one of.,..,__proto__,constructororprototype(packages/core/src/domain/context-name.ts). Each dot-separated key segment matches^[a-zA-Z0-9][a-zA-Z0-9_-]*$, up to 256 characters in total. - Prepared statements. Every query that takes input binds it as a parameter.
- PowerShell quoting. Values go into single-quoted strings with
'doubled and NUL bytes stripped, and writes callCredWriteWdirectly instead of going throughcmdkey. - No values in the command text of run. As shown above; and if any placeholder is missing, the command does not run at all.
- Saved commands keep their context.
cmd runignoresENVSEC_CONTEXT, so a command saved for one context doesn't silently run against another insideenvsec shell. - Quiet by default.
listandsearchprint names only, and debug logging never logs the arguments, script or stdin passed to the credential store tools.
Recommendations
- Turn on full-disk encryption (FileVault, LUKS, BitLocker) and lock your screen. That is what protects the keychain at rest.
- Prefer
runovershellandeval, and inject into the one command that needs the secrets. - Add secrets with the prompt, not
--value. Quote{key}placeholders. - Keep production in its own context, and don't load it on a machine or in a session where you run code you haven't read.
- Treat
env-fileoutput as a secret, delete it when done, and runenvsec auditto see what is still on disk. - If you use coding agents, read Your coding agent can read your .env. The short version: envsec stops the accidental reads, not an agent that runs commands as you.
If you find something this post misses, or a claim the code doesn't back, open an issue. A threat model is only useful while it is accurate.