
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
Multi-backend environment variable and secrets manager with AES-256-GCM encryption
Organizes your .env files with structure and best practices.
Powered by dotenv for parsing. Store secrets anywhere: S3, MinIO, R2, or filesystem.
curl -fsSL https://raw.githubusercontent.com/forattini-dev/vaulter/main/install.sh | sh
# or: npm install -g vaulter
vaulter init # Initialize project
vaulter key generate --name master # Generate encryption key
vaulter change set DATABASE_URL="postgres://..." -e dev # Set secret
vaulter change set PORT::3000 -e dev # Set config (plain)
vaulter change set NODE_ENV=local -e dev # Set config (sensitive=false)
vaulter change move API_KEY --from shared --to api -e dev # Move variable to service
vaulter change move API_KEY --from shared -e dev -s svc-notifications # Infer destination service
vaulter plan -e dev # Preview changes before applying
eval $(vaulter export shell -e dev) # Export to shell
web + api)This flow shows local editing, team sharing, and promotion across multiple environments.
# 0) Initialize + discover services
vaulter init --monorepo
vaulter key generate --name master
vaulter services
# 1) Create/override vars locally (offline by default)
# `local set` writes only to `.vaulter/local/*`; use `-e/--env` only for backend-aware operations.
vaulter local set NEXT_PUBLIC_APP_NAME=Portal --shared
vaulter local set NODE_ENV=local --shared
vaulter local set DATABASE_URL=postgres://... -s api
vaulter local set REDIS_URL=redis://... -s api
vaulter local set QUEUE_ENABLED::true -s api
vaulter local set WORKER_CONCURRENCY::4 -s web
vaulter local pull --all # Generates .env for local run (all outputs)
vaulter local diff # Review local overrides
# 2) Share source of truth with team (backend sync)
vaulter local push --all -e dev
# 3) Team members pull and generate local envs
vaulter local sync -e dev
vaulter local pull --all
# 4) Promote the same managed set to multiple environments
for ENV in dev stg prd; do
echo "Deploying to $ENV"
vaulter plan -e "$ENV"
vaulter apply -e "$ENV" $( [ "$ENV" = "prd" ] && echo '--force' )
done
# 5) Run your scripts with vaulter-managed variables
vaulter run -e dev -- pnpm start # Local run with local overrides
vaulter run -e dev -s web -- pnpm --dir apps/web dev
vaulter run -e dev -s api -- pnpm --dir apps/api lint
vaulter run -e stg -s api -- pnpm --dir apps/api migrate
vaulter run -e prd -- docker compose -f ./deploy/docker/docker-compose.yml up
# 6) Export service-specific artifacts per environment
# Config-like outputs
vaulter export env -e dev --service api > apps/api/.env
vaulter export env -e stg --service web > apps/web/.env
vaulter export shell -e prd --service api > /tmp/api-env.sh
# Kubernetes artifacts
vaulter export k8s-secret -e dev --service api --name api-secrets
vaulter export k8s-secret -e dev --service web --name web-secrets
vaulter export k8s-secret -e stg --service api --name api-secrets
vaulter export k8s-secret -e prd --service api --name api-secrets
# Deployment formats
vaulter export k8s-configmap -e prd --service api --name api-configmap
vaulter export helm -e prd --service api --name api-values
--forceis required onapply -e prdand other production-like environments.
Vaulter follows a backend-sync workflow where the backend is the source of truth and local overrides are for personal customization.
Backend is the source of truth. Everything syncs via backend.
| Component | Git Status | Purpose |
|---|---|---|
.vaulter/config.yaml | β Committed | Project configuration |
.vaulter/local/* | β Gitignored | Personal local overrides |
*.env files | β Gitignored | Generated outputs |
.vaulter/
βββ config.yaml # β
Committed - Project config
βββ local/ # β Gitignored - Personal overrides
β βββ configs.env # Non-sensitive overrides (DEBUG, PORT)
β βββ secrets.env # Sensitive overrides (test API keys)
β βββ services/ # Monorepo per-service overrides
β βββ api/
β βββ configs.env
β βββ secrets.env
βββ dev/ # β Gitignored - Environment data (--dir mode)
βββ configs.env # Shared non-sensitive vars
βββ secrets.env # Shared sensitive vars
βββ services/ # Monorepo service vars
βββ api/
βββ configs.env
βββ secrets.env
apps/web/.env # β Gitignored - Generated output
apps/api/.env # β Gitignored - Generated output
Directory modes:
.vaulter/local/ - Personal overrides (never synced to backend).vaulter/{env}/ - Environment data (synced with --dir mode)# Vaulter - only commit config.yaml
.vaulter/local/
*.env
.env.*
# 1. Start: Pull latest from backend + apply your local overrides
vaulter local pull
# 2. Work: Add personal overrides (not shared with team)
vaulter local set DEBUG::true # Shared override
vaulter local set PORT::3001 # Service-specific (inferred from cwd in monorepo)
# 3. Add new variable for team? Push to backend
vaulter local set NEW_VAR=value --shared # Personal scratch pad
vaulter local push # Share scratch locally with team
vaulter plan -e dev # Preview changes (recommended)
vaulter apply -e dev # Apply after approval
# 4. Check: See what's different
vaulter diff -e dev # Local vs backend diff
# 5. Promote: Clone to staging/production
vaulter clone dev stg --dry-run # Preview
vaulter clone dev stg # Execute
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DEVELOPMENT WORKFLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β LOCAL (.vaulter/local/) βββ Personal only (gitignored) β
β βββ configs.env β
β βββ secrets.env β
β β β
β β merged on `vaulter local pull` β
β βΌ β
β β
β BACKEND (S3/MinIO) βββ Source of truth (synced) β
β βββ dev/ βββββββββββββββββββββββββββββββββββββββββββββββ β
β β βββ all vars (encrypted) β β
β β β β
β βββ stg/ ββββββββ vaulter clone dev stg βββββββββββββββ€ β
β β βββ all vars (encrypted) β β
β β β β
β βββ prd/ ββββββββ vaulter clone stg prd βββββββββββββββ β
β βββ all vars (encrypted) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Team collaboration assumes one shared truth for each environment (backend) and private, local overrides per developer.
New team member setup (2 minutes):
git clone <repo> # Gets .vaulter/config.yaml
export VAULTER_KEY_DEV=<from-team> # Get key securely from team
vaulter local sync -e dev # Pull remote vars to .vaulter/local/
vaulter local pull --all # Generate .env files (offline)
Why this is stable for teams
vaulter local set is always a private, working-copy edit. It does not change what others consume by itself.vaulter local push is how you publish team-visible changes from local overrides.vaulter local sync is how others consume published changes.status, diff, and plan/apply) before merging critical updates.Recommended sharing flow (single variable):
# 1) Add locally first
vaulter local set --shared NEW_FEATURE::enabled # Shared config
vaulter local diff # Verify local change before publishing
# 2) Optional dry-run share preview
vaulter local push --shared --dry-run -e dev # Checks what would be pushed
# 3) Share to backend (explicit approval step before running)
vaulter local push --shared -e dev
# 4) Notify team
# "New var published. Run: vaulter local sync -e dev && vaulter local pull --all"
Monorepo service rule (recommended):
-s svc-*) unless explicitly cross-service..vaulter/config.yaml (policy), so mistakes are prevented early.Conflict resolution if two devs edit same key
vaulter local diff -s <service> # See your local delta
vaulter local sync -e dev # Pull latest from backend
vaulter local pull --all # Rebuild outputs
vaulter local diff -s <service> # Re-check before pushing
If divergence remains:
vaulter plan -e dev + manual review for sensitive or cross-service keys.Important: Most local commands are local-only. Passing -e/--env is only needed when publishing or syncing with backend.
| Task | Tool |
|---|---|
| Check health | vaulter_status action="scorecard" |
| Pull with overrides | vaulter_local action="pull" |
| Set shared override | vaulter_local action="shared-set" key="DEBUG" value="true" |
| Set service override | vaulter_local action="set" key="PORT" value="3001" |
| See differences | vaulter_diff |
| Compare environments | vaulter_search source="dev" target="prd" |
Vaulter is an opinionated organizer for your environment variables. It uses dotenv under the hood for parsing .env files - we don't reinvent the wheel, we just add structure.
# Install in your project
pnpm add vaulter
# or: npm install vaulter
What vaulter adds on top of dotenv:
| Feature | dotenv | vaulter |
|---|---|---|
Parse .env files | β | β (uses dotenv) |
| Organize by environment (dev/stg/prd) | β | β |
| Separate local vs deploy files | β | β |
| Auto-detect environment (local/CI/K8s) | β | β |
| Encrypted remote storage | β | β |
| Sync between team members | β | β |
| Export to K8s, Helm, Terraform | β | β |
Philosophy: Your local .env stays local (gitignored). Configs are committed. Secrets are encrypted in your own storage.
// app.ts
import { config } from 'vaulter'
config() // Loads from .vaulter/local/ (configs.env + secrets.env)
# Run commands with env vars loaded
npx vaulter run -- pnpm dev
# Or pull from backend first
vaulter local pull
That's it! For most local development, vaulter is just a structured dotenv.
Always start with vaulter status to diagnose your setup:
vaulter status -e dev
vaulter status -e dev --offline
Status performs up to 18 checks online, or a local-first subset in --offline.
| Check | What It Does |
|---|---|
| β Connection | Tests backend connectivity (skipped in --offline) |
| β Latency | Measures operation speed |
| β Permissions | Validates read/write/delete access |
| β Encryption | Tests encrypt β decrypt round-trip |
| β Sync Status | Compares local vs remote |
| β Security | Detects .env in git, weak keys |
| β Scope Policy | Checks shared vs service assignment rules |
| β Perf Config | Suggests cache/warmup/concurrency tuning |
| β +8 more | Config, project, environment, backend, keys, etc. |
Example output:
β ok: 15 | β warn: 1 | β fail: 1
β connection: connected (24 vars in dev)
β latency: read=45ms, list=67ms
β permissions: read/write/delete OK
β encryption: round-trip successful
β sync-status: 5 local-only, 3 remote-only, 2 conflicts
β security: 2 .env files tracked in git
β Add to .gitignore immediately
When to use:
scripts/vaulter-verify-dev.sh)For a quick pre-deploy validation in local/dev workflows:
VAULTER_VERIFY_ENV=dev pnpm run verify:vaulter
VAULTER_VERIFY_OFFLINE=0 VAULTER_VERIFY_REQUIRE_CONFIG=1 pnpm run verify:vaulter
The script runs:
vaulter status -e <env> -v [--offline] (offline by default)vaulter diff -e <env> --valuesvaulter list -e <env>It writes an execution log under artifacts/vaulter-health/ for auditability.
For AI Agents: Call vaulter_status action="scorecard" once at the start of a new session (or when operations fail / environments change) to understand the current state before performing sensitive operations.
See docs/DOCTOR.md for complete guide.
| Command | Description |
|---|---|
init | Initialize project config |
init --split | Initialize with split mode (configs/secrets dirs) |
| Command | Description |
|---|---|
status -e <env> | Full diagnostic report with checks and suggestions |
change)| Command | Description |
|---|---|
change set KEY=val -e <env> | Set secret (encrypted) |
change set KEY::val -e <env> | Set config (plain text) |
change set KEY:=123 -e <env> | Set typed secret (number/boolean) |
change delete <key> -e <env> | Delete variable |
change move <key> --from <scope> --to <scope> -e <env> | Move/copy variable between scopes |
change import -f <file> -e <env> | Import variables from file |
list -e <env> | List all variables |
Set syntax: = encrypted secret Β· :: plain config Β· := typed secret
In monorepo mode, when --service is resolved, one of --from or --to can be omitted and inferred from the active service.
| Command | Description |
|---|---|
plan -e <env> | Compute diff local vs backend, generate plan artifact |
apply -e <env> | Execute plan, push changes to backend |
diff -e <env> | Quick diff without plan artifacts |
plan --dir -e <env> | Plan from .vaulter/{env}/ directory |
plan [--plan-output <file>] -e <env> | Write plan artifact (.json + .md). If --plan-output is omitted, defaults to artifacts/vaulter-plans/<project>-<env>-<timestamp>.* |
vaulter local pull β vaulter local set β vaulter local push (when ready)vaulter change set β vaulter change move β vaulter plan -e <env> β vaulter apply -e <env>vaulter plan -e <env> β validate β vaulter apply -e <env>vaulter status -e <env> for quick pre-flight health check| Command | Description |
|---|---|
export shell -e <env> | Export for shell eval $(...) |
export k8s-secret -e <env> | Generate Kubernetes Secret (sensitive vars only) |
export k8s-configmap -e <env> | Generate Kubernetes ConfigMap (config vars only) |
export helm -e <env> | Generate Helm values.yaml |
export terraform -e <env> | Generate Terraform .tfvars |
export docker -e <env> | Docker env-file format |
export vercel -e <env> | Vercel environment JSON |
export github-actions -e <env> | GitHub Actions secrets |
| Command | Description |
|---|---|
services list | List discovered services |
services | Same as services list |
| Command | Description |
|---|---|
audit list -e <env> | List audit entries |
audit stats -e <env> | Show statistics |
rotation list -e <env> | Check rotation status |
rotation run -e <env> | CI/CD gate for overdue secrets |
| Command | Description |
|---|---|
key generate --name <n> | Generate symmetric key |
key generate --env <env> | Generate key for specific environment |
key generate --name <n> --asymmetric | Generate RSA/EC key pair |
key list | List all keys |
key export --name <n> | Export encrypted bundle |
key import -f <file> | Import encrypted bundle |
key backup -o <file> | Backup keys to encrypted bundle |
key restore -f <file> | Restore keys from backup bundle |
| Command | Description |
|---|---|
run -- <command> | Execute command with auto-loaded env vars |
run -e prd -- <command> | Execute with specific environment |
run -s api -- <command> | Execute with service-specific vars (monorepo) |
run --verbose -- <command> | Show which files were loaded |
run --dry-run -- <command> | Preview without executing |
Examples:
# Local development
npx vaulter run -- pnpm dev
# CI/CD build with production vars
npx vaulter run -e prd -- pnpm build
# Monorepo service
npx vaulter run -e dev -s api -- pnpm start
Use vaulter run directly in your npm scripts to keep variables centralized and explicit.
{
"scripts": {
"dev:web": "vaulter run -e dev -s web -- pnpm --dir apps/web dev",
"lint:api": "vaulter run -e dev -s api -- pnpm --dir apps/api lint",
"migrate:api:stg": "vaulter run -e stg -s api -- pnpm --dir apps/api run migrate",
"deploy:api:prd": "vaulter run -e prd -s api -- pnpm --dir apps/api build && vaulter export k8s-secret -e prd -s api --name api-secrets"
}
}
npm run dev:web
npm run lint:api
npm run migrate:api:stg
The important part is that vaulter run stays as the first command so variable resolution and scope resolution
happen before your script command.
The run command auto-detects the environment (local, CI, K8s) and loads the appropriate files before executing your command.
Run
vaulter --helporvaulter <command> --helpfor all options.
Every secret is encrypted before leaving your machine using AES-256-GCM.
vaulter key generate --name master
For CI/CD separation: public key encrypts, private key decrypts.
vaulter key generate --name master --asymmetric # RSA-4096
vaulter key generate --name master --asym --alg ec-p256 # EC P-256
# .vaulter/config.yaml
encryption:
mode: asymmetric
asymmetric:
algorithm: rsa-4096
key_name: master # ~/.vaulter/projects/<project>/keys/master[.pub]
CI/CD: Give CI only the public key (can write, can't read). Production gets the private key.
Use different encryption keys for each environment (dev, stg, prd). This provides complete isolation - production secrets can't be decrypted with dev keys.
# Generate keys for each environment
vaulter key generate --env dev
vaulter key generate --env stg
vaulter key generate --env prd
Keys are stored in ~/.vaulter/projects/{project}/keys/{env}.
Key Resolution Order (per environment):
| Priority | Source | Example |
|---|---|---|
| 1 | Env var VAULTER_KEY_{ENV} | VAULTER_KEY_PRD=my-secret |
| 2 | Config encryption.keys.{env} | See below |
| 3 | File keys/{env} | ~/.vaulter/projects/myapp/keys/prd |
| 4 | Env var VAULTER_KEY | Global fallback |
| 5 | Config encryption.key_source | Default config |
| 6 | File keys/master | Default fallback |
Per-environment config:
# .vaulter/config.yaml
encryption:
keys:
dev:
source:
- env: VAULTER_KEY_DEV
- file: ~/.vaulter/projects/myapp/keys/dev
prd:
source:
- env: VAULTER_KEY_PRD
mode: asymmetric # Optional: different mode per env
Multi-app isolation:
~/.vaulter/projects/
βββ app-landing/keys/
β βββ dev # app-landing dev key
β βββ stg # app-landing stg key
β βββ prd # app-landing prd key
βββ app-api/keys/
β βββ dev # app-api dev key (DIFFERENT from app-landing)
β βββ prd
βββ svc-auth/keys/
βββ prd
Each app has completely isolated secrets - app-landing/prd keys cannot decrypt app-api/prd secrets.
In monorepos, shared variables need a consistent encryption key. Use shared_key_environment to specify which environment's key encrypts shared vars:
# .vaulter/config.yaml
encryption:
shared_key_environment: dev # Use dev key for all shared vars
keys:
dev:
source:
- env: VAULTER_KEY_DEV
prd:
source:
- env: VAULTER_KEY_PRD
Why this matters:
__shared__ service) need ONE key to encrypt/decryptshared_key_environment, vaulter uses the current environment's keyExample flow:
# Set shared var (uses dev key because shared_key_environment: dev)
vaulter change set LOG_LEVEL=debug -e dev --scope shared
# Read shared var from prd (still uses dev key for shared vars)
vaulter list -e prd --shared # Works! Uses dev key for shared
# .vaulter/config.yaml
version: "1"
project: my-project
backend:
url: s3://bucket/envs?region=us-east-1
encryption:
key_source:
- env: VAULTER_KEY
- file: .vaulter/.key
rotation:
enabled: true
interval_days: 90
patterns: ["*_KEY", "*_SECRET", "*_TOKEN"]
environments: [dev, stg, prd]
default_environment: dev
audit:
enabled: true
retention_days: 90
scope_policy:
mode: warn
inherit_defaults: true
rules:
- name: api-keys-service
pattern: '^API_'
expected_scope: service
expected_service: svc-app
reason: 'API_* vars are service-owned'
- name: app-url-shared-default
pattern: '^APP_.*_URL$'
expected_scope: shared
reason: 'URL variables stay shared by default'
# Local development files (see "Local vs Deploy Structure" below)
# local: .vaulter/local/
# CI/CD deploy files (see "Local vs Deploy Structure" below)
# deploy: .vaulter/deploy/
| Provider | URL |
|---|---|
| AWS S3 | s3://bucket/path?region=us-east-1 |
| MinIO | http://KEY:SECRET@localhost:9000/bucket |
| Cloudflare R2 | https://KEY:SECRET@ACCOUNT.r2.cloudflarestorage.com/bucket |
| DigitalOcean | https://KEY:SECRET@nyc3.digitaloceanspaces.com/bucket |
| FileSystem | file:///path/to/storage |
Vaulter separates local development from deployment configurations:
.vaulter/
βββ config.yaml
βββ local/ # Developer machine (gitignored)
β βββ configs.env # Non-sensitive (sensitive=false)
β βββ secrets.env # Sensitive (sensitive=true)
β βββ services/ # Monorepo only
β βββ <service>/
β βββ configs.env
β βββ secrets.env
βββ deploy/ # CI/CD pipelines
βββ configs/ # Committed to git
β βββ dev.env
β βββ stg.env
β βββ prd.env
βββ secrets/ # Gitignored, pulled from backend
βββ dev.env
βββ prd.env
Why this structure:
| Location | Purpose | Git | Contains |
|---|---|---|---|
local/configs.env | Developer's machine | Ignored | Non-sensitive local vars |
local/secrets.env | Developer's machine | Ignored | Sensitive local secrets |
deploy/configs/*.env | CI/CD configs | Committed | Non-sensitive (PORT, HOST, LOG_LEVEL) |
deploy/secrets/*.env | CI/CD secrets | Ignored | Pulled via vaulter local sync |
Gitignore:
# Local development
.vaulter/local/configs.env
.vaulter/local/secrets.env
.vaulter/local/services/
# Deploy secrets (pulled in CI)
.vaulter/deploy/secrets/
Use the official Vaulter GitHub Action for seamless CI/CD integration:
- uses: forattini-dev/vaulter@v1
id: secrets
with:
backend: s3://my-bucket/secrets
project: my-app
environment: prd
outputs: env,k8s-secret
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
| Output | File | Use Case |
|---|---|---|
env | .env | Docker, Node.js |
json | vaulter-vars.json | Custom scripts |
k8s-secret | k8s-secret.yaml | kubectl apply |
k8s-configmap | k8s-configmap.yaml | kubectl apply |
helm-values | helm-values.yaml | helmfile, helm |
tfvars | terraform.auto.tfvars | terraform, terragrunt |
shell | vaulter-env.sh | source in scripts |
kubectl:
- uses: forattini-dev/vaulter@v1
with:
backend: ${{ secrets.VAULTER_BACKEND }}
project: my-app
environment: prd
outputs: k8s-secret,k8s-configmap
k8s-namespace: my-namespace
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
- run: |
kubectl apply -f k8s-secret.yaml
kubectl apply -f k8s-configmap.yaml
Helmfile:
- uses: forattini-dev/vaulter@v1
with:
backend: ${{ secrets.VAULTER_BACKEND }}
project: my-app
environment: prd
outputs: helm-values
helm-values-path: ./helm/secrets.yaml
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
- run: helmfile -e prd apply
Terraform/Terragrunt:
- uses: forattini-dev/vaulter@v1
with:
backend: ${{ secrets.VAULTER_BACKEND }}
project: infra
environment: prd
outputs: tfvars
# .auto.tfvars is loaded automatically by Terraform!
tfvars-path: ./secrets.auto.tfvars
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
- run: terragrunt apply -auto-approve
Docker Build:
- uses: forattini-dev/vaulter@v1
with:
backend: ${{ secrets.VAULTER_BACKEND }}
project: my-app
environment: prd
outputs: env
env-path: .env.production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
- run: docker build --secret id=env,src=.env.production -t app .
Export to GITHUB_ENV:
- uses: forattini-dev/vaulter@v1
with:
backend: ${{ secrets.VAULTER_BACKEND }}
project: my-app
environment: prd
export-to-env: true # Makes vars available in subsequent steps
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
- run: echo "Database is $DATABASE_URL" # Available!
| Input | Required | Default | Description |
|---|---|---|---|
backend | β | - | S3 connection string |
project | β | - | Project name |
environment | β | - | Environment (dev/stg/prd) |
service | - | Service (monorepo) | |
outputs | env | Comma-separated outputs | |
k8s-namespace | default | K8s namespace | |
export-to-env | false | Export to GITHUB_ENV | |
mask-values | true | Mask secrets in logs |
| Output | Description |
|---|---|
env-file | Path to .env file |
k8s-secret-file | Path to K8s Secret YAML |
k8s-configmap-file | Path to K8s ConfigMap YAML |
helm-values-file | Path to Helm values file |
tfvars-file | Path to .tfvars file |
vars-count | Number of variables |
vars-json | JSON array of variable names |
You can also use the CLI directly:
- name: Pull and build
env:
VAULTER_PASSPHRASE: ${{ secrets.VAULTER_PASSPHRASE }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
npx vaulter local sync -e prd
npx vaulter local pull -e prd
npx vaulter run -e prd -- pnpm build
# GitLab CI
npx vaulter run -e ${CI_ENVIRONMENT_NAME} -- pnpm build
# Docker (build with secrets)
npx vaulter run -e prd -- docker build -t myapp .
# Terraform
vaulter export terraform -e prd > secrets.auto.tfvars
# Helm
vaulter export helm -e prd | helm upgrade myapp ./chart -f -
# Direct export
npx vaulter export k8s-secret -e prd | kubectl apply -f -
When running in Kubernetes, env vars are already injected via ConfigMap/Secret. Vaulter's config() function automatically skips loading when it detects K8s:
import { config } from 'vaulter'
config() // Skips in K8s, loads files elsewhere
Detection: KUBERNETES_SERVICE_HOST environment variable is set by K8s automatically.
Auto-detects NX, Turborepo, Lerna, pnpm, Yarn workspaces, Rush.
vaulter service list # List discovered services
vaulter plan -e dev -s api # Plan changes for specific service
vaulter apply -e dev -s api # Apply planned changes
vaulter export shell -e dev -s api # Export with shared inheritance
vaulter export shell -e dev --shared # Export only shared variables
When exporting for a specific service, shared variables are automatically included:
# Shared vars: NODE_ENV=development, LOG_LEVEL=debug
# API service vars: PORT=3000, LOG_LEVEL=info
vaulter export shell -e dev -s api
# Output: NODE_ENV=development, LOG_LEVEL=info, PORT=3000
# (service vars override shared vars with same key)
# To export without inheritance:
vaulter export shell -e dev -s api --no-shared
One config β multiple .env files. Works with any framework: Next.js, NestJS, Express, NX, Turborepo, etc.
Different apps need different variables in different places:
apps/
βββ web/ # Next.js needs .env.local with NEXT_PUBLIC_*
βββ api/ # NestJS needs .env with DATABASE_*, JWT_*
βββ admin/ # Needs everything
Define outputs once, pull everywhere:
# .vaulter/config.yaml
outputs:
web:
path: apps/web
filename: .env.local
include: [NEXT_PUBLIC_*] # Only public vars
api:
path: apps/api
include: [DATABASE_*, JWT_*] # Only backend vars
exclude: [*_DEV] # No dev-only vars
admin: apps/admin # Shorthand: all vars
# Shared across all outputs (inherited automatically)
shared:
include: [NODE_ENV, LOG_LEVEL, SENTRY_*]
# Pull to all outputs at once
vaulter local pull --all
# Result:
# β web: apps/web/.env.local (5 vars)
# β api: apps/api/.env (12 vars)
# β admin: apps/admin/.env (25 vars)
# Pull only web
vaulter local pull --output web
# Preview without writing
vaulter local pull --all --dry-run
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Backend (S3) β
β DATABASE_URL, JWT_SECRET, NEXT_PUBLIC_API, LOG_LEVEL β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
vaulter local pull --all
β
βββββββββββββββββΌββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
β web β β api β β admin β
β .env.localβ β .env β β .env β
β β β β β β
β LOG_LEVELβ β LOG_LEVELβ β LOG_LEVELβ β shared (inherited)
β NEXT_* β β DATABASE_β β ALL VARS β β filtered by include/exclude
ββββββββββββ β JWT_* β ββββββββββββ
ββββββββββββ
| Pattern | Matches |
|---|---|
NEXT_PUBLIC_* | NEXT_PUBLIC_API_URL, NEXT_PUBLIC_GA_ID |
*_SECRET | JWT_SECRET, API_SECRET |
DATABASE_* | DATABASE_URL, DATABASE_HOST |
*_URL | API_URL, DATABASE_URL, REDIS_URL |
By default, shared.include vars are added to ALL outputs. Override with inherit: false:
outputs:
isolated-app:
path: apps/isolated
inherit: false # No shared vars
include: [ISOLATED_*]
vaulter local pull and local .env generation are 100% OFFLINE - no backend calls.
Works entirely from local files in .vaulter/local/. This is the primary workflow for day-to-day development: edit local overrides, run vaulter local pull, and only sync when needed.
| Command | What it does | Backend? |
|---|---|---|
vaulter local pull | Generate .env files from local | β OFFLINE |
vaulter local push --all | Send local β backend | β Backend |
vaulter local sync | Download backend β local | β Backend |
vaulter local set | Write local override to .vaulter/local/ | β OFFLINE |
vaulter local diff | Compare local overrides vs base env | β OFFLINE |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LOCAL DEVELOPMENT β
β 1. Edit .vaulter/local/*.env β
β 2. vaulter local pull β Generate .env β
β 3. Develop... β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SHARE WITH TEAM β
β vaulter local push --all β Upload to backend β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NEW TEAM MEMBER β
β 1. git clone <repo> β
β 2. vaulter local sync β Download from backend β
β 3. vaulter local pull β Generate .env β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
For monorepos, use --service <name> on local set, local delete, local diff, and local push (without --all), unless the CLI can infer the service from your current directory (or the monorepo has only one service).
.vaulter/local/
βββ configs.env # Shared configs (all services)
βββ secrets.env # Shared secrets (all services)
βββ services/ # Monorepo only
βββ web/
β βββ configs.env # web-specific configs
β βββ secrets.env # web-specific secrets
βββ api/
βββ configs.env
βββ secrets.env
Priority: shared vars < service-specific vars
For each output target, vaulter merges:
.vaulter/local/{configs,secrets}.env.vaulter/local/services/{service}/*.envExample:
# === EDIT LOCALLY ===
vaulter local set --shared DEBUG::true # shared config
vaulter local set --shared API_KEY=xxx # shared secret
vaulter local set PORT::3001 # service config (inferred from cwd in monorepo)
vaulter local set DB_URL=xxx -s api # service secret
# In service directories, `-s` is usually auto-inferred.
# If the repo has only one service, `-s` is inferred automatically too.
# === GENERATE .ENV FILES [OFFLINE] ===
vaulter local pull
# Output: "svc-auth: 23 vars (21 shared + 2 service)"
# === SHARE WITH TEAM ===
vaulter local push --all # Upload entire structure
# === GET TEAM'S CHANGES ===
vaulter local sync # Download from backend
vaulter local pull # Generate .env files
# === OTHER ===
vaulter local diff # Show differences
vaulter local status # Show summary
Vaulter uses section-aware mode by default when writing .env files. This preserves your custom variables while managing backend variables separately.
# Your variables (NEVER touched by vaulter)
MY_LOCAL_VAR=something
CUSTOM_DEBUG=true
MY_PORT_OVERRIDE=3001
# --- VAULTER MANAGED (do not edit below) ---
DATABASE_URL=postgres://...
API_KEY=sk-xxx
NODE_ENV=production
# --- END VAULTER ---
How it works:
| Location | Behavior |
|---|---|
| Above marker | Preserved - your custom vars, never modified |
| Between markers | Managed - vaulter controls this section |
| Below end marker | Preserved - any trailing content |
CLI options:
# Section-aware pull (default)
vaulter local pull
# Overwrite entire file (ignores sections)
vaulter local pull --overwrite
Programmatic API:
import {
syncVaulterSection,
getUserVarsFromEnvFile,
setInEnvFile
} from 'vaulter'
// Sync only the managed section (preserves user vars)
syncVaulterSection('/app/.env', {
DATABASE_URL: 'postgres://...',
API_KEY: 'sk-xxx'
})
// Read only user-defined vars (above the marker)
const userVars = getUserVarsFromEnvFile('/app/.env')
// { MY_LOCAL_VAR: 'something', CUSTOM_DEBUG: 'true' }
// Add var to user section (above marker)
setInEnvFile('/app/.env', 'MY_VAR', 'value', true)
Use cases:
DEBUG=true above the marker, it stays after vaulter local pullPORT=3001 locally without affecting teammatesFEATURE_X_ENABLED=true without touching backend.env.local compatible with Next.js/Vite expectationsimport { VaulterClient, loadConfig } from 'vaulter'
const client = new VaulterClient({ config: loadConfig() })
await client.connect()
await client.set({ key: 'API_KEY', value: 'sk-xxx', project: 'my-project', environment: 'prd' })
const value = await client.get('API_KEY', 'my-project', 'prd')
const vars = await client.list({ project: 'my-project', environment: 'prd' })
await client.disconnect()
The config() function auto-detects your environment and loads the appropriate files:
import { config } from 'vaulter'
// Auto-detect environment and load appropriate files
const result = config()
// With options
config({
mode: 'auto', // 'auto' | 'local' | 'deploy' | 'skip'
environment: 'dev', // Override environment (dev, stg, prd)
service: 'api', // For monorepo service-specific vars
verbose: true, // Debug output
})
Environment Detection:
| Environment | Detection | Behavior |
|---|---|---|
| Kubernetes | KUBERNETES_SERVICE_HOST set | Skip loading (vars injected via ConfigMap/Secret) |
| CI/CD | CI=true, GITHUB_ACTIONS, etc. | Load from .vaulter/deploy/ |
| Local | Default | Load from .vaulter/local/ (configs.env + secrets.env) |
Why this matters:
.env file (gitignored)import 'vaulter/load' // Auto-loads .env into process.env
Load secrets directly from the backend at application startup - no .env files, no Kubernetes ConfigMaps/Secrets needed.
// Option 1: Simple import (like dotenv/config)
import 'vaulter/load'
// Option 2: Side-effect import with full path
import 'vaulter/runtime/load'
// Option 3: With options
import { loadRuntime } from 'vaulter'
await loadRuntime({
environment: 'prd',
service: 'api', // Optional: for monorepos
required: true // Default: true in prd, false otherwise
})
// Option 4: Using config() with backend source
import { config } from 'vaulter'
await config({ source: 'backend' })
// Now process.env has all your secrets!
console.log(process.env.DATABASE_URL)
.vaulter/config.yaml to find backend URLprocess.env# .vaulter/config.yaml
project: my-app
backend:
url: s3://my-bucket/secrets
# Runtime detects environment from NODE_ENV or config
default_environment: dev
| Option | Type | Default | Description |
|---|---|---|---|
environment | string | NODE_ENV or dev | Target environment |
project | string | from config | Project name |
service | string | - | Service name (monorepo) |
required | boolean | true in prd | Throw on failure |
override | boolean | false | Override existing env vars |
includeShared | boolean | true | Include shared vars (monorepo) |
filter.include | string[] | [] | Glob patterns to include |
filter.exclude | string[] | [] | Glob patterns to exclude |
verbose | boolean | false | Enable logging |
Runtime loader automatically uses per-environment keys:
# Set env-specific keys
export VAULTER_KEY_DEV="dev-secret"
export VAULTER_KEY_PRD="prd-secret"
# Or generate key files
vaulter key generate --env prd
Kubernetes without ConfigMaps/Secrets:
# deployment.yaml - No configMapRef/secretRef needed!
env:
- name: VAULTER_KEY_PRD
valueFrom:
secretKeyRef:
name: vaulter-key
key: prd
- name: VAULTER_BACKEND
value: "s3://my-bucket/secrets"
// app.ts - Secrets loaded at startup
import 'vaulter/runtime/load'
// process.env.DATABASE_URL is now available
Lambda/Serverless:
import { loadRuntime } from 'vaulter'
export const handler = async (event) => {
await loadRuntime({ environment: 'prd' })
// Use secrets from process.env
}
import { loadRuntime, isRuntimeAvailable, getRuntimeInfo } from 'vaulter'
// Check if runtime config exists
if (isRuntimeAvailable()) {
const info = await getRuntimeInfo()
console.log(info.project, info.environment, info.backend)
}
// Load with callbacks
const result = await loadRuntime({
environment: 'prd',
onLoaded: (r) => console.log(`Loaded ${r.varsLoaded} vars in ${r.durationMs}ms`),
onError: (e) => console.error('Failed:', e.message)
})
Claude AI integration via Model Context Protocol. 17 Tools | 4 Resources | 5 Prompts.
vaulter mcp
{
"mcpServers": {
"vaulter": {
"command": "vaulter",
"args": ["mcp", "--cwd", "/path/to/project"]
}
}
}
Tool Architecture: Each tool is action-based (one tool per domain with
actionparameter).
| Category | Tool | Actions / Description |
|---|---|---|
| Mutation Flow | vaulter_change | set, delete, move, import (writes local state only) |
vaulter_plan | Compute diff local vs backend, generate plan artifact | |
vaulter_apply | Execute plan, push changes to backend | |
vaulter_run | Execute command with loaded variables | |
| Read | vaulter_get | Get single var or multi-get via keys[] |
vaulter_list | List vars with optional filter | |
vaulter_search | Search by pattern or compare environments | |
vaulter_diff | Quick diff without plan artifacts | |
| Status | vaulter_status | scorecard, vars, audit, drift, inventory |
| Export | vaulter_export | k8s-secret, k8s-configmap, helm, terraform, env, shell, json |
| Keys | vaulter_key | generate, list, show, export, import, rotate |
| Local Dev | vaulter_local | pull, push, push-all, sync, set, delete, diff, status, shared-set, shared-delete, shared-list |
| Backup | vaulter_snapshot | create, list, restore, delete |
vaulter_versions | list, get, rollback | |
| Setup | vaulter_init | Initialize project |
vaulter_services | Discover monorepo services | |
| Danger | vaulter_nuke | Preview backend deletion (CLI-only execution) |
Static data views (no input required). For actions with parameters, use tools.
| URI | Description |
|---|---|
vaulter://instructions | Read first! s3db.js architecture + tool overview |
vaulter://tools-guide | Which tool to use for each scenario |
vaulter://config | Project configuration (YAML) |
vaulter://services | Monorepo services list |
Pre-configured workflows for common tasks.
| Prompt | Description |
|---|---|
setup_project | Initialize new vaulter project |
deploy_secrets | Deploy to Kubernetes |
compare_environments | Compare dev vs prd |
rotation_workflow | Check/rotate/report on rotation |
local_dev_workflow | Manage local dev overrides (shared + service) |
Full MCP documentation: See docs/MCP.md for complete tool reference with parameters.
vaulter shell # Secrets Explorer (default)
vaulter shell menu # Menu
vaulter shell audit # Audit log viewer
vaulter shell keys # Key manager
Shows local .env sync status vs backend:
| Icon | Status | Meaning |
|---|---|---|
β | synced | Local value = backend value |
β | modified | Local value differs from backend |
β | missing | Exists in backend, not in local .env |
+ | local-only | Exists only in local .env |
Global: q quit Β· ESC back Β· ββ navigate
| Screen | Shortcuts |
|---|---|
| Menu | 1 2 3 quick access to screens |
| Explorer | r refresh Β· v toggle values Β· Tab cycle env Β· j/k vim nav |
| Audit | o filter op Β· s filter source Β· / search Β· c clear |
| Keys | r refresh Β· c toggle config |
Download from Releases:
| Platform | Binary |
|---|---|
| Linux x64/ARM64 | vaulter-linux-x64, vaulter-linux-arm64 |
| macOS x64/ARM64 | vaulter-macos-x64, vaulter-macos-arm64 |
| Windows x64 | vaulter-win-x64.exe |
MIT Β© Forattini
FAQs
Multi-backend environment variable and secrets manager with AES-256-GCM encryption
We found that vaulter demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the projectβs GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.