All posts
7 min readDavid Nussio

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 fileyes
grep, editor indexing, project backupsyes
Other local users (default permissions)mostly
Processes running as youno
Children of a process you gave secrets tono
Root, or a stolen unencrypted diskno
The short version. Each row is explained below.

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:

Terminal
# 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_URL

Children 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:

Terminal
# 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):

Terminal
$envsec -c myapp.dev run 'psql "{database.url}"'
# /bin/sh runs: psql "$ENVSEC_0_DATABASE_URL"
# with ENVSEC_0_DATABASE_URL set in its environment

With /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:

Recommendations

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.