@ezecutin/secrets-cli
v0.1.0
Published
A local-first CLI for managing encrypted .env secrets.
Maintainers
Readme
secrets-cli
A local-first CLI for managing encrypted .env secrets. Store secret values in an AES-256-GCM encrypted vault that lives next to your project, commit the vault to git, and keep the key safe in your home directory.
No vendor accounts. No network calls. No magic.
Install
npm install -g secrets-cliRequires Node 18 or later.
Quickstart
# 1. Create a vault in your project root
secrets init
# 2. Store a secret (prompts for the value — keeps it out of shell history)
secrets set DATABASE_URL
# 3. Pull secrets to .env.local for local development
secrets pull
# 4. See what changed between environments
secrets diff --env stagingHow it works
secrets init generates a random 32-byte key and saves it to ~/.secrets/keys/<project>.key. Secret values are encrypted with AES-256-GCM and stored in .secrets.vault alongside your project. The vault is safe to commit — it is unreadable without the key.
your-project/
├── .secrets.vault ← encrypted vault, commit this
├── .secrets/
│ └── config.json ← project metadata, commit this
└── .env.local ← decrypted output, git-ignoredThe key lives at ~/.secrets/keys/<project>.key and never enters the repository. Back it up.
secrets init also adds safe local-secret patterns to .gitignore (.env, .env.local, .env.*.local, .secrets/tokens.json, and vault lock/tmp files). It does not ignore .env.example, so sanitized examples can stay in git.
Commands
secrets init [--force]
Create a new encrypted vault in the current directory. Writes:
.secrets.vault— the encrypted vault.secrets/config.json— project name, used to locate the key file~/.secrets/keys/<project>.key— your 32-byte encryption key
Use --force to overwrite an existing vault (generates a new key — old vault becomes unreadable).
secrets set <key> [--env <env>] [--value <value>]
Store a secret. Prompts for the value by default so it does not appear in shell history.
secrets set DATABASE_URL
secrets set STRIPE_KEY --env staging
secrets set API_KEY --value "sk-..." --env production # scripting onlyUse --value only in automation where you control the environment. The value may appear in CI logs and process lists.
secrets get <key> [--env <env>]
Print the decrypted value to stdout. Safe to pipe.
secrets get DATABASE_URL
secrets get DATABASE_URL | pbcopy
DATABASE_URL=$(secrets get DATABASE_URL) node server.jsExits with a non-zero status if the key is not found.
secrets list [--env <env>] [--show-values]
List all key names in an environment. Values are hidden by default.
secrets list
secrets list --env staging --show-valuessecrets import <file> [--env <env>] [--force] [--allow-empty] [--dry-run]
Import all secrets from an .env file into the vault. Comments and blank lines are stripped. Prompts before overwriting existing keys unless --force is given.
secrets import .env
secrets import .env.production --env production --force
secrets import .env --dry-run # preview without writingFlags:
--force— overwrite existing keys without prompting--allow-empty— include keys with empty string values (skipped by default)--dry-run— show what would be written without touching the vault
secrets pull [--env <env>] [--output <file>] [--dry-run]
Decrypt all secrets from an environment and write them to a .env file (default: .env.local).
secrets pull
secrets pull --env staging --output .env.staging
secrets pull --dry-run # list keys that would be writtensecrets push [--env <env>] [--input <file>] [--force] [--allow-empty] [--dry-run]
Read a .env file and merge its values into the vault (default input: .env.local). Prompts before overwriting existing keys unless --force is given.
secrets push
secrets push --env staging --input .env.staging --force
secrets push --dry-runsecrets diff [--env <env>] [--env-a <env>] [--env-b <env>]
Compare two environments. Defaults to default vs the target env.
secrets diff --env staging
secrets diff --env-a default --env-b productionOutput:
KEY DEFAULT STAGING STATUS
─────────────────────────────────────────────
API_KEY ✓ ✓ identical
DATABASE_URL ✓ ✓ values differ
STRIPE_KEY – ✓ missing in defaultsecrets delete <key> [--env <env>] [--force]
Remove a key from the vault. Prompts for confirmation unless --force is given.
secrets delete OLD_KEY
secrets delete OLD_KEY --forceGlobal flags
| Flag | Description |
| ------------------- | ---------------------------------------- |
| -q, --quiet | Suppress all non-error output |
| --non-interactive | Fail immediately if a prompt would occur |
| --version | Print the version number |
Environment variables
| Variable | Description |
| ------------------ | ---------------------------------------------------------- |
| SECRETS_KEY_PATH | Override the key file location (useful in CI) |
| CI | When set to any truthy value, disables interactive prompts |
CI usage
In CI, set SECRETS_KEY_PATH to the path of a key file injected as a secret, or use --value and --force to avoid prompts:
# GitHub Actions example
secrets pull --env production --output .env- name: Write key file
run: printf '%s\n' "$SECRETS_KEY" > /tmp/project.key
env:
SECRETS_KEY: ${{ secrets.SECRETS_KEY }}
- name: Pull secrets
run: secrets pull --env production --output .env
env:
SECRETS_KEY_PATH: /tmp/project.key.env file format
secrets-cli uses dotenv to parse .env files. Supported syntax:
KEY=value
KEY="value with spaces"
KEY='single quotes'
# comments are stripped
export KEY=value # export prefix is ignoredMultiline values, unicode, values containing = and #, and quoted strings are all supported. Key ordering is not preserved — keys are written alphabetically.
Vault format
The vault is a YAML file with per-value encryption:
version: 1
project: my-app
environments:
default:
DATABASE_URL: enc:v1:<iv_b64>.<ciphertext_b64>.<tag_b64>
API_KEY: enc:v1:<iv_b64>.<ciphertext_b64>.<tag_b64>
staging:
DATABASE_URL: enc:v1:<iv_b64>.<ciphertext_b64>.<tag_b64>Key names are stored in plaintext so the vault is git-diffable. Only values are encrypted. Each value has a unique 12-byte IV so encrypting the same value twice produces different ciphertext.
The enc:v1: prefix identifies the format version. A future breaking change would use enc:v2:.
Security model
See SECURITY.md for the full threat model. The short version:
- Protects secrets if someone can read the repository (vault without key = unreadable)
- Does not protect against malware running as your user, or someone who has both the vault and the key
- Key names are intentionally visible in the vault
- AES-256-GCM provides authenticated encryption — tampered ciphertext is detected
Back up your key file. If ~/.secrets/keys/<project>.key is lost, the vault cannot be recovered.
License
MIT
