envsec
v1.1.2
Published
Secure environment secrets management using native OS credential stores
Maintainers
Readme
envsec
Secure environment secrets management using native OS credential stores.
Demo

Features
- Store secrets in your OS native credential store (not plain text files)
- Cross-platform: macOS, Linux, Windows
- Organize secrets by context (e.g.
myapp.dev,stripe-api.prod,work.staging) - Track secret metadata (key names, timestamps) via SQLite
- Search contexts and secrets with glob patterns
- Run commands with secret interpolation
- Save and rerun commands with
cmd(search, list, run, delete) - Export secrets to
.envfiles (with generation tracking viaaudit) - Export secrets as shell environment variables (
eval $(envsec env)) - Load secrets from
.envfiles (with conflict detection) - Rescue every plaintext
.envfile in a directory tree into the keychain (envsec rescue ~/projects --import) - Share secrets encrypted with GPG for team members
Requirements
- Node.js >= 22.13
macOS
No extra dependencies. Uses the built-in Keychain via the security CLI tool.
Linux
Requires libsecret-tools (provides the secret-tool command), which talks to GNOME Keyring, KDE Wallet, or any Secret Service API provider via D-Bus.
# Debian / Ubuntu
sudo apt install libsecret-tools
# Fedora
sudo dnf install libsecret
# Arch
sudo pacman -S libsecretA running D-Bus session and a keyring daemon (e.g. gnome-keyring-daemon) must be active. Most desktop environments handle this automatically.
Windows
No extra dependencies. Uses the built-in Windows Credential Manager via cmdkey and PowerShell.
Installation
Homebrew (macOS / Linux)
brew tap davidnussio/homebrew-tap
brew install envsecnpm
npm install -g envsecnpx (no install)
npx envsecmise
mise use -g npm:envsecUsage
Most commands require a context specified with --context (or -c). A context is a free-form label for grouping secrets — e.g. myapp.dev, stripe-api.prod, work.staging.
Custom database path
By default, metadata is stored at ~/.envsec/store.sqlite. You can override this with --db or the ENVSEC_DB environment variable:
# Use a project-local database
envsec --db ./local-store.sqlite -c myapp.dev list
# Or via environment variable
export ENVSEC_DB=/shared/team/envsec.sqlite
envsec -c myapp.dev listThe --db flag takes precedence over ENVSEC_DB. Use cases include per-project databases, team-shared databases on network drives, and CI/CD with ephemeral storage.
Add a secret
# Store a value inline
envsec -c myapp.dev add api.key --value "sk-abc123"
# Or use the short alias
envsec -c myapp.dev add api.key -v "sk-abc123"
# Omit --value for an interactive masked prompt
envsec -c myapp.dev add api.key
# Set an expiry duration with --expires (-e)
envsec -c myapp.dev add api.key -v "sk-abc123" --expires 30d
# Supported duration units: m (minutes), h (hours), d (days), w (weeks), mo (months), y (years)
# Combinable: 1y6mo, 2w3d, 1d12h
envsec -c myapp.dev add api.key -v "sk-abc123" -e 6moGenerate a secret
Generate a cryptographically secure random secret and store it in one step. When used without --context or without a key name, it works as a standalone password generator.
# Generate a 32-char alphanumeric secret (default)
envsec -c myapp.dev secret api.key
# Custom length
envsec -c myapp.dev secret api.key --length 64
envsec -c myapp.dev secret api.key -l 64
# Add a prefix (e.g. sk_, pk_, whsec_)
envsec -c myapp.dev secret api.key --prefix "sk_" --length 48
# Character set options:
# --alphanumeric (-a) Only [a-zA-Z0-9] (default)
# --special (-s) Alphanumeric + !@#$%^&*
# --all-chars All printable ASCII for maximum entropy
envsec -c myapp.dev secret db.password --special --length 64
envsec -c myapp.dev secret master.key --all-chars --length 128
# With expiry
envsec -c myapp.dev secret api.key --prefix "sk_" -l 48 --expires 90d
# Combine everything
envsec -c myapp.dev secret api.key --prefix "sk_" --length 64 --special --expires 1y
# Standalone password generator (no context, no key — just prints the value)
envsec secret --length 32
envsec secret --special --length 64
envsec secret --all-chars --length 128 --prefix "pk_"When both --context and a key name are provided, the generated value is stored and printed. Without either, it prints the raw value to stdout — perfect for piping or clipboard.
Get a secret
envsec -c myapp.dev get api.key
# Print only the raw value (no warnings or extra output)
envsec -c myapp.dev get api.key --quiet
envsec -c myapp.dev get api.key -qDelete a secret
envsec -c myapp.dev delete api.key
# or use the alias
envsec -c myapp.dev del api.keyRename a secret
Rename a secret key within the same context. The value and expiry metadata are preserved.
# Rename a key
envsec -c myapp.dev rename old.key new.key
# Overwrite target if it already exists
envsec -c myapp.dev rename old.key existing.key --forceList all secrets in a context
envsec -c myapp.dev listList all contexts
# Without --context, lists all available contexts with secret counts
envsec listSearch secrets
# Search secrets within a context
envsec -c myapp.dev search "api.*"
# Search contexts by pattern (without --context)
envsec search "myapp.*"Move secrets between contexts
Move secrets from one context to another. The source secrets are removed after moving.
# Move a single secret
envsec -c myapp.dev move api.token --to myapp.prod
# Move secrets matching a glob pattern
envsec -c myapp.dev move "redis.*" --to myapp.prod -y
# Move all secrets from one context to another
envsec -c myapp.dev move --all --to myapp.prod -y
# Overwrite existing secrets in the target context
envsec -c myapp.dev move "redis.*" --to myapp.prod --force -yCopy secrets between contexts
Copy secrets from one context to another. The source secrets remain intact.
# Copy a single secret
envsec -c myapp.dev copy api.token --to myapp.staging
# Copy secrets matching a glob pattern
envsec -c myapp.dev copy "redis.*" --to myapp.staging -y
# Copy all secrets from one context to another
envsec -c myapp.dev copy --all --to myapp.staging -y
# Overwrite existing secrets in the target context
envsec -c myapp.dev copy "redis.*" --to myapp.staging --force -yRun a command with secrets
# Placeholders {key} are resolved with secret values before execution
envsec -c myapp.dev run 'curl {api.url} -H "Authorization: Bearer {api.token}"'
# Any {dotted.key} in the command string is replaced with its value
envsec -c myapp.prod run 'psql {db.connection_string}'
# Inject ALL context secrets as environment variables (KEY.NAME → KEY_NAME)
envsec -c myapp.dev run --inject 'node server.js'
envsec -c myapp.dev run -i 'docker compose up'
# Combine --inject with placeholders
envsec -c myapp.dev run --inject 'curl {api.url} -H "Authorization: Bearer $API_TOKEN"'
# Save the command for later use with --save (-s) and --name (-n)
envsec -c myapp.dev run --save --name deploy 'kubectl apply -f - <<< {k8s.manifest}'
# If you use --save without --name, you'll be prompted interactively
envsec -c myapp.dev run --save 'psql {db.connection_string}'If any placeholder references a secret that doesn't exist, the command won't execute and you'll see a clear error:
❌ Missing secrets in context "myapp.dev":
- api.url
- api.token
Add them with: envsec -c myapp.dev add <key>Saved commands
Saved commands live under the cmd subcommand, keeping them separate from secret operations.
# List all saved commands
envsec cmd list
# Run a saved command (uses the context it was saved with)
envsec cmd run deploy
# Run quietly (suppress informational output like "Resolved N secret(s)")
envsec cmd run deploy --quiet
envsec cmd run deploy -q
# Run in a different context than the one it was saved with
# (envsec warns, showing both; ENVSEC_CONTEXT is ignored by cmd run)
envsec -c myapp.prod cmd run deploy
# Inject all context secrets as env vars when running a saved command
envsec cmd run deploy --inject
envsec cmd run deploy -i
# Search saved commands (searches both names and command strings)
envsec cmd search psql
# Search only by name
envsec cmd search deploy -n
# Search only by command string
envsec cmd search kubectl -m
# Delete a saved command
envsec cmd delete deployGenerate a .env file
# Creates .env with all secrets from the context
envsec -c myapp.dev env-file
# Specify a custom output path
envsec -c myapp.dev env-file --output .env.localKeys are converted to UPPER_SNAKE_CASE (e.g. api.token → API_TOKEN).
Export secrets as environment variables
# Output export statements for eval (bash/zsh)
eval $(envsec -c myapp.dev env)
# Specify target shell syntax
envsec -c myapp.dev env --shell fish
envsec -c myapp.dev env --shell powershell
# Output unset commands to clean up exported variables
eval $(envsec -c myapp.dev env --unset)
# Combine shell and unset
envsec -c myapp.dev env --unset --shell fishSupported shells: bash (default), zsh, fish, powershell. Keys are converted to UPPER_SNAKE_CASE (e.g. api.token → API_TOKEN). Output goes to stdout so it can be piped to eval or sourced directly — no file is written to disk.
Load secrets from a .env file
# Import secrets from .env into the context
envsec -c myapp.dev load
# Specify a custom input file
envsec -c myapp.dev load --input .env.local
# Overwrite existing secrets without warning
envsec -c myapp.dev load --forceKeys are converted from UPPER_SNAKE_CASE to dotted.lowercase (e.g. API_TOKEN → api.token). If a key already exists, it is skipped with a warning unless --force (-f) is provided.
Rescue every .env file in a directory tree
Scan a directory recursively, find every plaintext .env file and move its secrets into the OS keychain. By default rescue only reports what it found and changes nothing: you review the report, then run it again with --import and, when you are ready, --remove-plaintext.
# Report what is there (changes nothing)
envsec rescue ~/projects
# Import the secrets into the keychain
envsec rescue ~/projects --import
# Delete the plaintext files whose secrets are verified in the keychain
envsec rescue ~/projects --remove-plaintext
# Or both in one go
envsec rescue ~/projects --import --remove-plaintext--import,-i: Import the secrets into the keychain (without it,rescueonly reports)--remove-plaintext: Delete each.envfile whose secrets are all verified in the keychain--force,-f: With--import, overwrite secrets already in the keychain with a different value--no-gitignore: Do not add the.envfiles to.gitignore--depth: Maximum directory depth to scan (default:8)
How it works:
- Finds
.env,.env.local,.env.<mode>and.env.<mode>.localfiles. Templates (.env.example,.env.sample,.env.template…) and directories such asnode_modules,.git,distandvendorare skipped, and so are files generated byenvsec env-file. - Groups by project: the nearest directory with a
.git,package.json,pyproject.toml,go.mod,Cargo.toml… is the project. - Proposes contexts as
<project>.<mode>:.envand.env.localgo to<project>.dev,.env.productionto<project>.prod,.env.stagingto<project>.staging. Files of the same mode are layered like Vite and Next.js do (.env<.env.local<.env.<mode><.env.<mode>.local). - Detects duplicates: the same value used in more than one place (e.g. one API key copied into five projects) is reported by name, never by value. Files committed to git are flagged too: their secrets are in the history and should be rotated.
- Imports with the same key mapping as
load(API_TOKEN→api.token). Secrets already in the keychain with the same value count as secured, so runningrescueagain is safe; a different value is reported as a conflict and kept unless you pass--force. - Updates
.gitignoreat the root of each git repository (with--importor--remove-plaintext) so the files cannot be committed later. - Deletes only with
--remove-plaintext, and only files whose every value is in the keychain exactly as written, checked by reading it back. A file is kept (with the reason printed) when a value is not imported yet, a variable name cannot be stored, or a value is overridden by another file.
✔ 143 secrets secured · 26 plaintext files removedShare secrets (GPG encrypted)
# Encrypt all secrets from a context for a team member
envsec -c myapp.dev share --encrypt-to [email protected]
# Save encrypted output to a file
envsec -c myapp.dev share --encrypt-to [email protected] -o secrets.enc
# Use JSON format inside the encrypted payload
envsec -c myapp.dev --json share --encrypt-to [email protected] -o secrets.encThe recipient can decrypt with gpg --decrypt secrets.enc and pipe the result into envsec load. By default the encrypted payload uses .env format (KEY="value"); with --json it uses a structured JSON object. Requires GPG to be installed and the recipient's public key to be in your keyring.
Audit secrets for expiry
# Check for expired or expiring secrets in a context (default window: 30 days)
envsec -c myapp.dev audit
# Specify a custom window
envsec -c myapp.dev audit --within 7d
# Show only already-expired secrets
envsec -c myapp.dev audit --within 0d
# Audit across all contexts (omit --context)
envsec audit
# JSON output
envsec -c myapp.dev audit --jsonSecrets with an --expires duration set via envsec add are tracked in metadata. The audit command scans for secrets that are already expired or will expire within the specified window. The get and list commands also display expiry warnings inline.
The audit command also tracks generated .env files. Every time env-file is used, the output path, context, and timestamp are recorded. The audit output includes a second section listing these files. If a tracked .env file no longer exists on disk, audit automatically removes it from the metadata and reports the cleanup.
Diagnose your setup
# Run all health checks
envsec doctor
# JSON output for scripting
envsec --json doctorThe doctor command verifies your envsec installation is working correctly. It checks:
- Platform support and Node.js version
- Credential store availability (macOS Keychain, Linux secret-tool, Windows cmdkey)
- Keychain read/write access
- Database path, permissions, and schema integrity
- Orphaned secrets (metadata without keychain entry)
- Expired secrets
- Environment variables (
ENVSEC_DB,ENVSEC_CONTEXT) - Current shell
Shell completions
envsec supports dynamic tab completion for bash, zsh, and fish. Completions are context-aware: they suggest your actual context names, secret keys, and saved command names in real time by querying the metadata database.
# Bash (add to ~/.bashrc)
eval "$(envsec --completions bash)"
# Zsh (add to ~/.zshrc)
eval "$(envsec --completions zsh)"
# Fish (add to ~/.config/fish/config.fish)
envsec --completions fish | sourceWhat gets completed dynamically:
--context/-c— lists all your contexts- Secret key arguments (
get,add,delete) — lists keys for the current context cmd run/cmd delete— lists saved command names- Subcommands, flags, and static choices (shells, etc.) are also completed
How it works
Secrets are stored in the native OS credential store. The backend is selected automatically based on the platform:
| OS | Backend | Tool / API |
| ------- | -------------------------- | -------------------------------- |
| macOS | Keychain | security CLI |
| Linux | Secret Service API (D-Bus) | secret-tool (libsecret) |
| Windows | Credential Manager | cmdkey + PowerShell (advapi32) |
Metadata (key names, timestamps) is kept in a SQLite database at ~/.envsec/store.sqlite (configurable via --db or ENVSEC_DB). Keys must contain at least one dot separator (e.g., service.account) which maps to the credential store's service/account structure.
Security
envsec is built around a simple principle: your secrets belong in your OS, not in dotfiles. Every design decision starts from that foundation.
How envsec protects your secrets
OS-native encryption, zero custom crypto. Secret values are stored directly in macOS Keychain, GNOME Keyring / KDE Wallet, or Windows Credential Manager. envsec never invents its own encryption — it delegates to the battle-tested credential stores your operating system already provides, protected by your user session and (on macOS) the login keychain.
Full Unicode support. Secret values can contain any Unicode characters, including emoji and accented letters. Values are base64-encoded before being stored in the OS credential store, avoiding platform-specific encoding quirks (e.g. macOS security CLI hex-encoding non-ASCII output). Legacy plaintext secrets are read transparently for backward compatibility.
Secrets never touch disk as plaintext. Values go straight from your terminal into the OS credential store. They are never written to config files, logs, or intermediate storage.
No secrets in terminal output. The list and search commands display key names only — values are never printed. This keeps secrets out of scrollback buffers, screen recordings, and shoulder-surfing range.
Safe command execution. The run command injects secrets as environment variables of the child process rather than interpolating them into the command string. This means secret values don't appear in ps output or shell history. If any referenced secret is missing, the command is blocked entirely — no partial execution with incomplete credentials.
Input validation and injection prevention. Context names are validated against a strict allowlist (alphanumeric, dots, hyphens, underscores) with path traversal and prototype pollution checks. All SQLite queries use prepared statements with bind parameters, preventing SQL injection. PowerShell arguments on Windows are escaped to guard against command injection.
Restrictive file permissions. The metadata directory (~/.envsec/) is created with 0700 permissions and the SQLite database with 0600, limiting access to the owning user.
Known limitations and areas for improvement
We believe in being upfront about what envsec does not yet cover. These are real trade-offs, not bugs — and understanding them helps you make informed decisions.
Metadata is visible. The SQLite database at ~/.envsec/store.sqlite stores key names, context names, and timestamps — never secret values, but enough to reveal what secrets exist. Saved command templates (with {key} placeholders) are also stored there. If metadata confidentiality matters to you, ensure your home directory is on an encrypted volume.
env-file exports are plaintext. The env-file command writes secret values to a .env file on disk. This is inherently sensitive — treat the output file accordingly and never commit it to version control. Consider it a convenience bridge, not a storage mechanism.
Shell execution carries inherent risk. The run command passes your command template through /bin/sh (or cmd.exe on Windows). If the template itself comes from untrusted input, shell injection is possible. Only run command templates you wrote or trust.
No cross-context access control. Any process running as your OS user can read all secrets across all contexts. envsec relies on OS-level user isolation — it does not add its own authorization layer between contexts.
Linux headless environments. On Linux, envsec depends on an active D-Bus session and a keyring daemon (e.g. gnome-keyring-daemon). In containers or headless servers without a graphical session, the keyring may be unavailable or may store secrets with weaker protection.
Encryption depends on your OS. envsec adds no additional at-rest encryption beyond what the native credential store provides. On systems without full-disk encryption, an attacker with physical access could potentially extract secrets from the keychain. We recommend enabling full-disk encryption (FileVault, LUKS, BitLocker) for the strongest protection.
Programmatic Usage
Need to load secrets from code instead of the CLI? Check out @envsec/sdk — a Node.js / Bun SDK that reads secrets directly from the OS credential store.
import { loadSecrets } from "@envsec/sdk";
const secrets = await loadSecrets({ context: "myapp.dev", inject: true });
// process.env.API_KEY is now setDevelopment
Build
From the monorepo root:
pnpm buildOr build only the CLI package:
pnpm --filter envsec buildTest the local build
Option 1 — Global link
Link the CLI globally so the envsec command points to your local build:
cd packages/cli
pnpm link --globalNow envsec runs your local dist/main.js from anywhere. After each code change, rebuild with pnpm build and the alias picks it up automatically.
To unlink:
pnpm unlink --global envsecOption 2 — Shell alias (no link required)
Add a shell alias that runs the TypeScript source directly via tsx (useful for rapid iteration without rebuilding):
# Add to your ~/.bashrc or ~/.zshrc
alias envsec='npx tsx $(pwd)/packages/cli/src/main.ts'Or if you prefer running the compiled output:
alias envsec='node $(pwd)/packages/cli/dist/main.js'Option 3 — Run directly
node packages/cli/dist/main.js --helpRun e2e tests
The default suite uses temporary SQLite and file-backed credential fixtures, so it does not access your native credential store:
pnpm --filter envsec testTo exercise the real macOS or Linux credential-store adapter, build first and opt in explicitly:
ENVSEC_E2E_CLI="$PWD/packages/cli/dist/main.js" \
ENVSEC_E2E_ISOLATED=0 \
pnpm --filter envsec testNative E2E tests use dedicated test.e2e* contexts and remove them afterward.
Contributing
See the GitHub repository for development setup, architecture details, and contribution guidelines.
License
MIT
