@scalingworks/uenv
v0.2.0
Published
Load environment variables from secret vaults and run commands — keep secrets out of your repo
Maintainers
Readme
uenv
Load secrets from vaults and run commands — keep
.envfiles out of your repository entirely.
@scalingworks/uenv is a CLI tool that bridges your secret vault (1Password today, others in future) and your local development workflow. Instead of committing .env files or copying secrets manually, uenv fetches them at runtime and injects them into your process.
Features
- Three modes — envless (no local files at all), ref (
.envwithop://references), plain (standard dotenv) - Push & pull — sync
.envfiles to/from your vault with full section-level diffing - Monorepo-aware — each
.envfile in your repo maps to a named section inside a single vault item - Auto-inference — item name is derived from
git remote get-url origin(e.g.scalingworks/my-app) - Provider-agnostic — 1Password ships first; the interface is open for AWS Secrets Manager, HashiCorp Vault, etc.
Requirements
- Node.js ≥ 20
- 1Password CLI (
op) installed and signed in
Authentication
uenv supports two authentication modes depending on the environment.
Developer machines (interactive)
Set your account once with uenv config --account:
uenv config --account team.1password.comWhen account is configured, uenv strips any OP_SERVICE_ACCOUNT_TOKEN from the environment and passes --account to every op call. The 1Password desktop app handles authentication — it will prompt for Touch ID or your password on the first call, then cache the session for subsequent calls in the same shell session.
This covers all three operations: run, push, and pull.
CI/CD (service account)
Leave account unconfigured. Set OP_SERVICE_ACCOUNT_TOKEN in your pipeline environment:
# No uenv config needed — token is picked up automatically
OP_SERVICE_ACCOUNT_TOKEN=ops_... uenv run node server.jsuenv passes the token through to op without modification. The service account needs at least read access to the vault. push and pull require a service account with write access (or run those from a developer machine instead).
Why OP_SERVICE_ACCOUNT_TOKEN is stripped on developer machines
If you have OP_SERVICE_ACCOUNT_TOKEN set in your shell (e.g. for another tool) and also have account configured in uenv, the token would normally override --account and authenticate as the service account. Service accounts often have restricted item access and are read-only — items created after the service account was provisioned may not be visible to it at all. Stripping the token forces op to use the interactive user session, which has full access to the vault.
Installation
npm install -g @scalingworks/uenv
# or
npx @scalingworks/uenv --helpQuick start
1. Configure your vault (one-time)
uenv config --vault envs
# optionally pin your 1Password account
uenv config --account team.1password.comConfig is saved to ~/.uenv/config.json.
2. Push your existing .env files to the vault
Run this from your repo root. uenv discovers every .env file and stores each one as a named section inside a single vault item whose title matches your repo name.
uenv push
# Pushing to vault "envs" › item "scalingworks/my-app"
# ✔ .env → 12 vars
# ✔ apps/web/.env → 5 vars
# ✔ apps/api/.env → 8 varsYou can now delete your local .env files and add them to .gitignore.
3. Run commands with secrets injected
uenv run npm start
uenv run go run ./cmd/server
uenv run docker-compose upuenv fetches the .env section for this repo from the vault and injects it as environment variables before spawning your command. Nothing touches the filesystem.
Commands
uenv run
Load environment variables and execute a command.
uenv run [options] <command> [args...]| Option | Default | Description |
|--------|---------|-------------|
| --mode | envless | envless | ref | plain — see Modes |
| --vault | config / envs | Vault name |
| --item | (repo name) | Item name inside the vault |
| --section | .env | Which section (.env file path) to load |
| --env-file | (section value) | Path to local file for ref/plain modes |
| --provider | 1password | Secret provider |
# envless (default) — no local files needed
uenv run npm start
# target a non-root section (monorepo)
uenv run --section apps/web/.env next dev
# explicit item
uenv run --vault envs --item scalingworks/my-app node server.js
# ref mode — .env holds op:// URIs, resolved by op run
uenv run --mode ref npm run dev
# plain mode — standard dotenv, no vault involved
uenv run --mode plain --env-file .env.local jestuenv push
Push local .env files to the vault. Each file becomes a section in the vault item. Removed keys are deleted from the vault (full sync).
uenv push [options] [files...]# push every .env file in the repo
uenv push
# push specific files only
uenv push .env apps/api/.env
# override vault/item
uenv push --vault envs --item scalingworks/my-appuenv pull
Pull vault sections back to local .env files. Creates directories as needed.
uenv pull [options] [sections...]# pull all sections from the vault item
uenv pull
# pull a single section
uenv pull .env
# pull a nested section
uenv pull apps/web/.envuenv config
View or update global configuration stored at ~/.uenv/config.json.
uenv config [options]uenv config # show current config
uenv config --vault my-vault # set default vault
uenv config --account team.1password.comModes
envless (recommended)
uenv loads variables from the vault and injects them into the spawned process. If a local .env file exists at the section path, its values are merged on top — local file wins.
process.env < vault < local .env fileuenv run npm startThis means you can delete .env files entirely and rely on the vault, or keep a local override file for dev-specific tweaks without pushing them to the vault.
ref
.env files contain op:// secret references instead of actual values. The file is safe to commit.
# .env (safe to commit)
DATABASE_URL=op://envs/scalingworks-my-app/DATABASE_URL
STRIPE_SECRET_KEY=op://envs/scalingworks-my-app/STRIPE_SECRET_KEY
PORT=3000uenv delegates to op run --env-file which resolves references on the fly.
uenv run --mode ref node server.jsplain
Standard dotenv behaviour. Reads a local .env file and injects variables. No vault interaction.
uenv run --mode plain jest
uenv run --mode plain --env-file .env.test pytestVault structure (1Password)
uenv stores all secrets for a repository in a single item whose title matches the inferred repository name:
Vault: envs
└── Item: scalingworks/my-app
├── Section: .env
│ ├── DATABASE_URL (concealed)
│ ├── API_KEY (concealed)
│ └── NODE_ENV (text)
├── Section: apps/web/.env
│ ├── NEXT_PUBLIC_API_URL (text)
│ └── NEXT_PUBLIC_GA_ID (text)
└── Section: apps/api/.env
├── PORT (text)
└── JWT_SECRET (concealed)- Vault — defaults to
envs, configurable viauenv config --vault - Item title — inferred from
git remote get-url origin; falls back to the current directory name - Section — the relative path of the
.envfile from the repository root - Field type — keys matching
*SECRET*,*TOKEN*,*_KEY,*PASSWORD*,*PRIVATE*, etc. are stored asconcealed; everything else istext
Adding a new secret
# 1. Edit your local .env
echo 'NEW_SERVICE_KEY=sk-...' >> .env
# 2. Push the change
uenv push .env
# 3. Other team members pull it
uenv pull .envOr use op directly to edit the vault item and run uenv pull to sync locally.
Onboarding a new developer
# 1. Clone the repo (no .env files present)
git clone [email protected]:scalingworks/my-app.git
cd my-app
# 2. Pull all env sections from the vault
uenv pull
# 3. Start developing
uenv run npm startExtending with a new provider
Implement the EnvProvider interface from src/providers/interface.ts:
import type { EnvProvider } from '@scalingworks/uenv';
export class MyProvider implements EnvProvider {
readonly name = 'my-provider';
async getEnv(vault: string, item: string, section: string): Promise<Record<string, string>> {
// ...
}
async setEnv(vault: string, item: string, section: string, env: Record<string, string>): Promise<void> {
// ...
}
async listSections(vault: string, item: string): Promise<string[]> {
// ...
}
}Then register it in src/providers/index.ts.
License
MIT © Scaling Works
