@1dot5/smonoenv
v0.3.1
Published
SOPS + age secret management CLI for monorepo environment files
Readme
smonoenv
Secret management CLI for monorepos using SOPS + age.
Manage all app environment variables in a single .env.monorepo.<env> file and automatically distribute them to each app's .env. Encryption is handled by SOPS + age.
Installation
npm install -g @1dot5/smonoenv
# or as a devDependency
npm install -D @1dot5/smonoenv
# use npx
npx @1dot5/smonoenvPrerequisites
brew install sops ageSetup
smonoenv setupOn first run, this will:
- Verify that
sops/ageare installed - Check for an age secret key at
~/.config/sops/age/keys.txt - If no key is found, guide you through retrieving it from 1Password or generating a new one
Placing the age key
Obtain the shared team key and save it to:
~/.config/sops/age/keys.txtFor new projects:
age-keygen -o ~/.config/sops/age/keys.txtMonorepo env file format
.env.monorepo.<env> files use section delimiters to define environment variables for each app:
#<<< ENV BEGIN PATH=apps/web
DATABASE_URL=postgres://localhost:5432/mydb
NEXT_PUBLIC_API_URL=http://localhost:3000
#>>> ENV END
#<<< ENV BEGIN PATH=apps/api
DATABASE_URL=postgres://localhost:5432/mydb
PORT=3001
JWT_SECRET=dev-secret
#>>> ENV END
#<<< ENV BEGIN PATH=packages/shared
API_KEY=test-key
#>>> ENV ENDPATH= takes a relative path from the repository root. Running sync generates a .env file at each path.
Environment-suffixed output
Append :<env-name> to the path (e.g. PATH=apps/web:production) to output as .env.production:
#<<< ENV BEGIN PATH=apps/web:production
NEXT_PUBLIC_API_URL=https://api.example.com
#>>> ENV ENDOutputs to apps/web/.env.production
Commands
smonoenv local
One-command local development setup. Internally runs decrypt then sync.
smonoenv localBehavior:
- Decrypts
.env.monorepo.local.sopsif it exists - Distributes
.env.monorepo.localto each app's.env - If no encrypted file exists but
.env.monorepo.local.exampleis found, copies it and provides guidance
smonoenv encrypt <env>
Encrypt a plaintext file with SOPS.
smonoenv encrypt local
smonoenv encrypt staging
smonoenv encrypt production.env.monorepo.<env> → .env.monorepo.<env>.sops
Encrypted files (.sops) can be safely committed to Git.
smonoenv decrypt <env>
Decrypt a SOPS-encrypted file.
smonoenv decrypt staging
smonoenv decrypt production.env.monorepo.<env>.sops → .env.monorepo.<env>
smonoenv edit <env>
Edit an encrypted file directly with $EDITOR. Automatically handles decryption before editing and re-encryption after.
smonoenv edit stagingsmonoenv sync [env] [options]
Distribute monorepo env variables to each app's .env. Defaults to local if env is omitted.
smonoenv sync # distribute local
smonoenv sync staging # distribute stagingOptions
| Flag | Description |
|------|-------------|
| --check | Check sync status only. Exits with code 1 if drift is detected (for CI) |
| --dry | Dry run. No files are written |
| --clean | Delete existing .env files before syncing |
| --quiet | Suppress informational output |
Common workflows
New team member onboarding
# 1. Install tools
brew install sops age
# 2. Get the age key from 1Password
mkdir -p ~/.config/sops/age
# Save keys.txt
# 3. Verify setup
smonoenv setup
# 4. Set up local environment
smonoenv localAdding or changing environment variables
# 1. Edit directly
smonoenv edit local
# 2. Or edit the plaintext file and re-encrypt
smonoenv decrypt local
vi .env.monorepo.local
smonoenv encrypt local
# 3. Distribute to apps
smonoenv syncCI sync check
# GitHub Actions
- run: smonoenv decrypt local
- run: smonoenv sync --checkGenerating .env files in GitHub Actions
When your CI/CD pipeline needs actual .env files (e.g. Next.js builds, E2E tests, Docker builds), register the age secret key as a GitHub Actions Secret and use decrypt → sync to generate them.
1. Register the age secret key
# Check your local age secret key
cat ~/.config/sops/age/keys.txtGo to your GitHub repository's Settings → Secrets and variables → Actions and add it as SOPS_AGE_KEY (paste the entire key file contents).
2. Workflow example
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install sops and age
run: |
curl -Lo /usr/local/bin/sops https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
chmod +x /usr/local/bin/sops
curl -Lo age.tar.gz https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-amd64.tar.gz
tar xf age.tar.gz
mv age/age /usr/local/bin/
mv age/age-keygen /usr/local/bin/
- name: Setup age key
run: |
mkdir -p ~/.config/sops/age
echo "${{ secrets.SOPS_AGE_KEY }}" > ~/.config/sops/age/keys.txt
- run: pnpm install --frozen-lockfile
- name: Generate .env files
run: |
npx smonoenv decrypt staging
npx smonoenv sync staging
- run: pnpm build
- run: pnpm test3. Self-hosted runners
If sops / age are pre-installed on self-hosted runners, only the key setup step is needed:
steps:
- uses: actions/checkout@v4
- name: Setup age key
run: |
mkdir -p ~/.config/sops/age
echo "${{ secrets.SOPS_AGE_KEY }}" > ~/.config/sops/age/keys.txt
- name: Generate .env files
run: |
npx smonoenv decrypt production
npx smonoenv sync production
- run: pnpm build4. Per-environment configuration
Combine with GitHub Actions environment to generate the appropriate .env for each deploy target:
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
# ... checkout, setup omitted ...
- name: Generate .env files
run: |
ENV_NAME=${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
npx smonoenv decrypt $ENV_NAME
npx smonoenv sync $ENV_NAME5. Security: Cleanup on self-hosted runners
GitHub-hosted runners are destroyed after each job, but files persist on self-hosted runners. Use always() to clean up regardless of success or failure:
jobs:
build:
runs-on: [self-hosted]
steps:
- uses: actions/checkout@v4
- name: Setup age key
run: |
mkdir -p ~/.config/sops/age
echo "${{ secrets.SOPS_AGE_KEY }}" > ~/.config/sops/age/keys.txt
- name: Generate .env files
run: |
npx smonoenv decrypt production
npx smonoenv sync production
- run: pnpm build
- run: pnpm test
- name: Cleanup secrets
if: always()
run: |
rm -f ~/.config/sops/age/keys.txt
rm -f .env.monorepo.*
find . -name '.env' -not -path './node_modules/*' -delete
find . -name '.env.*' -not -name '.env.example' -not -path './node_modules/*' -deleteEnvironments
| Name | Purpose |
|------|---------|
| local | Local development |
| staging | Staging |
| production | Production |
File structure
.env.monorepo.local # Plaintext (recommended in .gitignore)
.env.monorepo.local.sops # Encrypted (tracked in Git)
.env.monorepo.staging # Plaintext
.env.monorepo.staging.sops # Encrypted
.env.monorepo.production # Plaintext
.env.monorepo.production.sops # Encrypted
.sops.yaml # SOPS encryption configLibrary usage
Can also be imported directly from Node.js:
import { parseMono, normalize, parseEnvFile } from "@1dot5/smonoenv";
import { sync, decrypt, encrypt } from "@1dot5/smonoenv";License
MIT
