Documentation

Everything you need to manage secrets with envsec.

Installation

Homebrew (macOS / Linux)

bash
brew tap davidnussio/homebrew-tap
brew install envsec

npm

Requires Node.js 22 or later.

bash
npm install -g envsec

Or run directly without installing:

bash
npx envsec

mise

bash
mise use -g npm:envsec

Why envsec?

After the Shai-Hulud npm attack (September 2025), I audited my old client projects and found 97 .env files with live credentials. Side projects, POCs, test apps, course exercises. Just files parked on disk, in plaintext, surviving every reboot.

Your OS already has a vault. envsec uses it.

Quick Start

Store your first secret. The -c flag sets the context — a label for grouping related secrets.

Terminal
# Store a secret
$envsec -c myapp.dev add api.key -v "sk-abc123"
# Retrieve it
$envsec -c myapp.dev get api.key
# List all secrets in the context
$envsec -c myapp.dev list
# Run a command with secret interpolation
$envsec -c myapp.dev run 'curl -H "Auth: {api.key}" https://api.example.com'

Set up shell completions and unlock the full power of tab. It's over 9000 times better — trust us, your fingers will thank you.

Requirements

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.

Terminal
# Debian / Ubuntu
$sudo apt install libsecret-tools
# Fedora
$sudo dnf install libsecret
# Arch
$sudo pacman -S libsecret

Windows

No extra dependencies. Uses the built-in Windows Credential Manager via cmdkey and PowerShell.

envsec add

Store a secret in the OS credential store.

  • <key>Secret key name (e.g. api.key, db.password)
  • --value, -vValue to store (omit for interactive masked prompt)
  • --expires, -eExpiry duration (e.g. 30m, 2h, 7d, 4w, 3mo, 1y)
Terminal
# Inline value
$envsec -c myapp.dev add api.key --value "sk-abc123"
# Interactive masked prompt (omit --value)
$envsec -c myapp.dev add api.key
# With expiry duration
$envsec -c myapp.dev add api.key -v "sk-abc123" --expires 30d
# Supported 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 6mo

envsec get

Retrieve a single secret value.

  • <key>Secret key name to retrieve
  • --quiet, -qPrint only the raw value (no warnings or extra output)
  • --jsonOutput in JSON format (includes context, key, value, expires_at)
Terminal
$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 -q

Use --quiet (-q) for scripting — it suppresses expiry warnings and outputs only the secret value.

envsec delete

Remove a secret from the credential store.

  • <key>Secret key name to delete (optional if --all is used)
  • --yes, -ySkip confirmation prompt
  • --allDelete all secrets in the context
Terminal
$envsec -c myapp.dev delete api.key
# Skip confirmation prompt
$envsec -c myapp.dev delete api.key --yes
# Alias
$envsec -c myapp.dev del api.key

envsec rename

Rename a secret key within the same context. The value and expiry metadata are preserved.

  • <old-key>Current secret key name
  • <new-key>New secret key name
  • --force, -fOverwrite target if it already exists
Terminal
# 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 --force

envsec list

List all secrets in a context, or list all contexts.

  • --jsonOutput in JSON format
Terminal
# List secrets in a context
$envsec -c myapp.dev list
# List all contexts (without -c)
$envsec list

envsec move

Move secrets from one context to another. The source secrets are removed after moving.

  • <pattern>Glob pattern or exact key to move (optional if --all is used)
  • --to, -tTarget context to move secrets to
  • --allMove all secrets from source context
  • --force, -fOverwrite existing secrets in the target context
  • --yes, -ySkip confirmation prompt
Terminal
# 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 -y

envsec copy

Copy secrets from one context to another. The source secrets remain intact.

  • <pattern>Glob pattern or exact key to copy (optional if --all is used)
  • --to, -tTarget context to copy secrets to
  • --allCopy all secrets from source context
  • --force, -fOverwrite existing secrets in the target context
  • --yes, -ySkip confirmation prompt
Terminal
# 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 -y

envsec run

Execute a command with secret interpolation. Placeholders like {key} are resolved and injected as environment variables — values never appear in ps output.

  • <command>Command to execute. Use {key} placeholders for secret interpolation
  • --inject, -iInject all context secrets as environment variables (KEY.NAME → KEY_NAME)
  • --save, -sSave this command for later use
  • --name, -nName for the saved command (prompted interactively if omitted with --save)
Terminal
# Run with secret interpolation
$envsec -c myapp.dev run 'curl {api.url} -H "Authorization: Bearer {api.token}"'
# Inject ALL context secrets as environment variables
$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
$envsec -c myapp.dev run --save --name deploy 'kubectl apply -f - <<< {k8s.manifest}'

With --inject (-i), every secret in the context is exported as an environment variable using UPPER_SNAKE_CASE (e.g. db.passwordDB_PASSWORD). Explicit {key} placeholders take precedence over injected variables.

envsec cmd

Manage saved commands.

cmd list

List all saved commands.

Terminal
$envsec cmd list

cmd run

Run a saved command (uses the context it was saved with).

  • <name>Name of the saved command to execute
  • --override-context, -oOverride the saved context at execution time
  • --quiet, -qSuppress informational output (print only command output)
  • --inject, -iInject all context secrets as environment variables
Terminal
$envsec cmd run deploy
# Run quietly (suppress informational output)
$envsec cmd run deploy --quiet
$envsec cmd run deploy -q
# Inject all context secrets as env vars
$envsec cmd run deploy --inject
$envsec cmd run deploy -i
# Override context at execution time
$envsec cmd run deploy --override-context myapp.prod

cmd search

Search saved commands by name or command string.

  • <pattern>Search pattern
  • --name, -nSearch only in command names
  • --command, -mSearch only in command strings
Terminal
$envsec cmd search psql

cmd delete

Delete a saved command.

  • <name>Name of the command to delete
Terminal
$envsec cmd delete deploy

envsec env-file

Export secrets to a .env file.

  • --output, -oOutput file path (default: .env)
Terminal
# Default output: .env
$envsec -c myapp.dev env-file
# Custom output path
$envsec -c myapp.dev env-file --output .env.local

envsec env

Export secrets as shell environment variable statements.

  • --shell, -sTarget shell syntax: bash (default), zsh, fish, powershell
  • --unset, -uOutput unset/remove commands instead of export
Terminal
# bash/zsh
$eval $(envsec -c myapp.dev env)
# fish
$envsec -c myapp.dev env --shell fish
# powershell
$envsec -c myapp.dev env --shell powershell
# Unset exported variables
$eval $(envsec -c myapp.dev env --unset)

Keys are converted to UPPER_SNAKE_CASE (e.g. api.token API_TOKEN).

envsec shell

Spawn a new shell with all secrets from a context injected as environment variables. The parent environment is inherited by default. Type exit or press Ctrl+D to leave — secrets are cleared when the session ends.

  • --shell, -sShell to spawn (bash, zsh, fish, powershell). Default: auto-detect
  • --no-inheritDo not inherit parent environment variables
  • --quiet, -qSuppress startup/exit banner
Terminal
# Start a shell with secrets loaded
$envsec -c myapp.dev shell
# Use a specific shell
$envsec -c myapp.dev shell --shell fish
# Don't inherit parent environment variables
$envsec -c myapp.dev shell --no-inherit
# Suppress startup/exit banner
$envsec -c myapp.dev shell --quiet

Keys are converted to UPPER_SNAKE_CASE (e.g. api.token API_TOKEN). On bash and zsh the prompt is prefixed with (envsec:context)so you know you're inside an envsec session.

Supported shells: bash, zsh, fish, powershell / pwsh. When no --shell flag is given, envsec auto-detects from the $SHELL environment variable.

envsec load

Import secrets from a .env file into a context.

  • --input, -iInput .env file path (default: .env)
  • --force, -fOverwrite existing secrets without prompting
  • --batch, -bBatch mode: defer database persistence until all secrets are imported
Terminal
# Import from .env
$envsec -c myapp.dev load
# Custom input file
$envsec -c myapp.dev load --input .env.local
# Overwrite existing secrets
$envsec -c myapp.dev load --force

envsec share

Encrypt secrets with GPG for team sharing.

  • --encrypt-toGPG recipient key (email, key ID, or fingerprint) to encrypt for
  • --output, -oOutput file path (default: stdout). Use - for stdout explicitly
  • --jsonUse JSON format inside the encrypted payload (default: .env format)
Terminal
# Encrypt for a team member
$envsec -c myapp.dev share --encrypt-to [email]
# Save to file
$envsec -c myapp.dev share --encrypt-to [email] -o secrets.enc
# JSON format inside encrypted payload
$envsec -c myapp.dev --json share --encrypt-to [email] -o secrets.enc

The recipient decrypts with gpg --decrypt secrets.enc and pipes the result into envsec load.

envsec audit

Check for expired or expiring secrets.

  • --within, -wShow secrets expiring within this duration (default: 30d). Use 0d for only already-expired
  • --jsonOutput in JSON format
Terminal
# Default window: 30 days
$envsec -c myapp.dev audit
# Custom window
$envsec -c myapp.dev audit --within 7d
# Only already-expired
$envsec -c myapp.dev audit --within 0d
# Audit all contexts
$envsec audit
# JSON output
$envsec -c myapp.dev audit --json

envsec secret

Generate a cryptographically secure random secret. When both a context and key name are provided, the value is stored in the credential store. Without either, it works as a standalone password generator that prints the raw value to stdout.

  • <key>Secret key name (optional; omit for standalone password generation)
  • --length, -lLength of the generated secret (default: 32)
  • --prefix, -pPrefix to prepend to the generated secret (e.g. "sk_")
  • --expires, -eExpiry duration (e.g. 30m, 2h, 7d, 4w, 3mo, 1y)
  • --alphanumeric, -aUse only alphanumeric characters [a-zA-Z0-9] (default)
  • --special, -sInclude common special characters [a-zA-Z0-9!@#$%^&*]
  • --all-chars, -AUse all printable ASCII characters for maximum entropy
Terminal
# Generate and store a 32-char alphanumeric secret
$envsec -c myapp.dev secret api.key
# Custom length and prefix
$envsec -c myapp.dev secret api.key --prefix "sk_" --length 48
# Character sets:
# --alphanumeric (-a) [a-zA-Z0-9] (default)
# --special (-s) [a-zA-Z0-9] + !@#$%^&*
# --all-chars (-A) all printable ASCII
$envsec -c myapp.dev secret db.password --special --length 64
# With expiry
$envsec -c myapp.dev secret api.key --prefix "sk_" -l 48 --expires 90d
# Standalone password generator (no store, just print)
$envsec secret --length 32
$envsec secret --special --length 64 --prefix "pk_"

When both context and key are present, the value is stored and printed. Without either, the raw value goes to stdout — perfect for piping to pbcopy, xclip, or any other tool.

envsec doctor

Run a suite of health checks to verify your envsec installation and environment. Useful when troubleshooting setup issues.

  • --jsonOutput in JSON format for scripting
Terminal
# Run all health checks
$envsec doctor
# JSON output
$envsec --json doctor

Checks performed:

  • Version — currently installed envsec version
  • Platform — OS and kernel version, confirms it is supported
  • Node.js — runtime version (22+ required)
  • Shell — active shell detected from the environment
  • Environment — detects ENVSEC_DB and ENVSEC_CONTEXT overrides
  • Credential store — verifies the OS credential backend is accessible (Keychain, Secret Service, or Credential Manager)
  • Keychain read/write — writes, reads back, and deletes a probe secret to confirm full access
  • Database — checks that the metadata directory and SQLite file exist and have correct permissions
  • Database integrity — queries the schema to confirm the database is not corrupted
  • Orphaned secrets — detects keys present in metadata but missing from the keychain
  • Expired secrets — flags secrets whose expiry date has already passed

A summary line at the end shows how many checks passed and how many failed. Use the --json flag to get structured output suitable for scripting or CI diagnostics.

Interactive TUI

envsec includes a full-screen terminal UI for managing secrets without memorizing commands. Launch it with envsec tui or optionally pass a context to start in.

Terminal
# Launch the TUI
$envsec tui
# Launch with a pre-selected context
$envsec -c myapp.dev tui

The TUI uses raw ANSI escape sequences with zero external dependencies. It runs in an alternate screen buffer so your terminal history stays clean.

Views & Screens

The main menu provides access to eight screens, each covering a core envsec workflow.

Contexts

Browse all contexts with their secret counts. Press s to set the selected context as the active context for the session, x to clear the active context, Enter to view its secrets, or d to delete all secrets in a context (with confirmation).

Secrets

Lists all secrets in the current context as a table with key, last updated, and expiry columns. Press Enter to reveal a secret value, a to add a new secret, or d to delete the selected secret.

Add Secret

Interactive form to store a new secret. Prompts for key, value (masked input), and an optional expiry duration (e.g. 30d, 1y, 6mo).

Search

Glob pattern search. With a context selected, searches secret keys. Without a context, searches context names.

Saved Commands

Lists all saved commands in a table with name, command template, and context. Press d to delete a command.

Audit

Scans for secrets expiring within 30 days. Shows expired vs. expiring status with time distance. Also lists tracked .env file exports and cleans up stale records for files that no longer exist on disk.

Import .env

Prompts for a file path (defaults to .env) and imports all key-value pairs into the current context. Keys are converted from UPPER_SNAKE_CASE to dotted.lowercase.

Export .env

Prompts for an output path (defaults to .env) and writes all secrets from the current context. The export is tracked in metadata for the audit view.

Keyboard Shortcuts

KeyAction
↑ / ↓Navigate menu items and table rows
EnterSelect / confirm
cOpen contexts view (main menu)
sSet selected as active context (contexts view)
xClear active context (contexts view)
aAdd a new secret (secrets view)
dDelete selected item
rReveal secret value (detail view)
EscGo back / cancel
qQuit the TUI
Ctrl+CQuit the TUI

Contexts

A context is a free-form label for grouping secrets — e.g. myapp.dev, stripe-api.prod, work.staging. Most commands require a context specified with --context (or -c).

Keys must contain at least one dot separator (e.g. service.account) which maps to the credential store's service/account structure.

Custom Database Path

By default, metadata is stored at ~/.envsec/store.sqlite. Override with --db or the ENVSEC_DB environment variable.

Terminal
# Project-local database
$envsec --db ./local-store.sqlite -c myapp.dev list
# Via environment variable
$export ENVSEC_DB=/shared/team/envsec.sqlite
$envsec -c myapp.dev list

Shell Completions

Tab completion for bash, zsh, fish, and sh.

Installed via Homebrew? Shell completions are configured automatically — you're already good to go.

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

SDK Overview

@envsec/sdk is a Node.js / Bun SDK that gives your applications programmatic access to envsec secrets. Load secrets at startup, inject them into process.env, or manage them with a full lifecycle client — no CLI needed.

Two API styles are available: a functional one-shot API (loadSecrets, withSecrets) for simple use cases, and a class-based API (EnvsecClient) for multi-operation workflows.

SDK Installation

Requires Node.js ≥ 22 and a working envsec setup (OS credential store accessible).

bash
npm install @envsec/sdk
# or
pnpm add @envsec/sdk

Functional API

One-shot functions for the most common use cases. No lifecycle management needed — the client is created and closed automatically.

loadSecrets

Load all secrets from one or more contexts. Optionally inject them into process.env.

bash
import { loadSecrets } from "@envsec/sdk"

// Load only
const secrets = await loadSecrets({ context: "myapp.dev" })
console.log(secrets["api.key"])

// Load and inject into process.env
await loadSecrets({ context: "myapp.prod", inject: true })
// process.env.API_KEY is now set

withSecrets

Run a callback with secrets available. When inject: true, process.env is automatically restored after the callback completes.

bash
import { withSecrets } from "@envsec/sdk"

const result = await withSecrets(
  { context: "myapp.dev", inject: true },
  async (secrets) => {
    // process.env.API_TOKEN is set here
    return fetch(secrets["api.url"])
  }
)
// process.env is restored to its original state

Client API

Full lifecycle client for multi-operation workflows. Supports get, set, delete, and bulk loading.

bash
import { EnvsecClient } from "@envsec/sdk"

const client = await EnvsecClient.create({ context: "myapp.dev" })

// Read
const apiKey = await client.get("api.key")      // string | null
const dbUrl = await client.require("db.url")     // string (throws if missing)

// Write
await client.set("api.key", "sk-new-value")
await client.set("api.key", "sk-new-value", { expires: "30d" })

// Delete
await client.delete("api.key")

// Bulk
const all = await client.loadAll()               // Record<string, string>
await client.injectEnv()                         // inject all into process.env

// Always close when done
await client.close()

Multi-Context

Pass an array of contexts to merge secrets from multiple sources. Later contexts override earlier ones (left-to-right merge).

bash
const client = await EnvsecClient.create({
  context: ["myapp.defaults", "myapp.dev"],
})

// "myapp.dev" values win over "myapp.defaults"
const secrets = await client.loadAll()
await client.close()

Write operations (set, delete) target the last (primary) context.

Options Reference

EnvsecClientOptions

OptionTypeDescription
contextstring | string[]Context(s) to operate on. Array enables multi-context merge.
dbPathstringOverride default SQLite path (~/.envsec/store.sqlite).

LoadSecretsOptions / WithSecretsOptions

Extends EnvsecClientOptions with:

OptionTypeDefaultDescription
injectbooleanfalseInject secrets into process.env after loading.

Key Transformation

When injecting into process.env, keys are converted to UPPER_SNAKE_CASE:

Secret KeyEnvironment Variable
api.tokenAPI_TOKEN
db.connectionDB_CONNECTION
redis.cache-urlREDIS_CACHE_URL

Security Model

envsec delegates encryption to your OS native credential store. It never invents its own crypto. Secret values go straight from your terminal into the OS credential store — they are never written to config files, logs, or intermediate storage.

The list and search commands display key names only — values are never printed. The run command injects secrets as environment variables of the child process rather than interpolating them into the command string, keeping values out of ps output and shell history.

The metadata directory (~/.envsec/) is created with 0700 permissions and the SQLite database with 0600, limiting access to the owning user.

Known Limitations

  • The SQLite database stores key names, context names, and timestamps — never secret values, but enough to reveal what secrets exist.
  • The env-file command writes secret values to a .env file on disk. Treat the output file accordingly.
  • The run command passes templates through /bin/sh. Only run templates you wrote or trust.
  • Any process running as your OS user can read all secrets across all contexts.
  • On Linux, envsec depends on an active D-Bus session and a keyring daemon.