This is a complete rewrite of V1 with 100% functional compatibility and new features.
- Working on several projects, with different teams, many repos is my reality.
- I need a personal workspace, which is attached to the project, but does not become part of its official repository (e.g. patched docker-compose.yml, bespoke Java test classes, etc.)
- My environment configurations are not DRY, they share variables, often follow even a hierarchy (globlal -> company -> region -> stage)
The vault is a directory outside the project that holds a personal workspace: env files, secrets, dev overrides.
It is linked to the project via a single .envrc symlink — the only trace of rsenv in your repo.
YOUR PROJECT THE VAULT
~/projects/myapp/ ~/.rsenv/vaults/myapp-a1b2c3d4/
┌─────────────────────────┐ ┌──────────────────────────────┐
│ │ │ │
│ .envrc ───── symlink ──────────────────── dot.envrc │
│ │ │ │
│ src/ │ │ envs/ │
│ Makefile │ │ local.env │
│ docker-compose.yml │ │ prod.env │
│ config/ │ │ │
│ secrets.yaml │ │ guarded/ (secrets) │
│ database.yml │ │ swap/ (dev overrides)│
│ │ │ .rsenv.toml (vault config) │
└─────────────────────────┘ └──────────────────────────────┘
git-tracked outside project git
minimal footprint: your personal workspace
ONE symlink
How it connects: rsenv init vault creates the vault, moves the .envrc
there (as dot.envrc), and creates the symlink.
Swap temporarily replaces project files with your dev versions. Unlike guard, this is reversible — swap in when you start work, swap out when done.
┌─ NORMAL STATE (swapped out) ──────────────────────────────────────────┐
│ │
│ Project Vault/swap │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ docker-compose.yml │ │ docker-compose.yml │ │
│ │ (official version) │ │ (your dev version) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
│ rsenv swap in
▼
┌─ SWAPPED IN (working) ────────────────────────────────────────────────┐
│ │
│ Project Vault/swap │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ docker-compose.yml │ │ docker-compose.yml │ │
│ │ (your dev version) │ │ .rsenv_original │ │
│ │ ◄── moved here │ │ ◄── backup of official │ │
│ └──────────────────────┘ │ │ │
│ │ docker-compose.yml │ │
│ │ .<hostname>.rsenv_active │
│ │ ◄── sentinel (who did it) │
│ └──────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
│ rsenv swap out
▼
back to normal state
(your changes to dev version are PRESERVED)
Hostname tracking: The sentinel .<hostname>.rsenv_active records which
machine swapped the file in, preventing conflicts when sharing vaults.
Key commands:
rsenv swap init <files>— set up files for swapping (first time)rsenv swap in— replace project files with vault versionsrsenv swap out— restore originals (no args = all files)rsenv swap status— show what's swapped in, by which hostrsenv swap status --silent— exit code only: 0=clean, 1=dirty, 2=unmanaged
Env files form a tree using the # rsenv: parent.env directive.
Children inherit all parent variables and can override them.
File contents: Resulting tree:
┌─ base.env ─────────────────┐ base.env
│ export DB_HOST=localhost │ / \
│ export DB_PORT=5432 │ local.env cloud.env
│ export LOG_LEVEL=info │ / \
└────────────────────────────┘ staging.env prod.env
┌─ cloud.env ────────────────┐
│ # rsenv: base.env │ ◄── links to parent
│ export DB_HOST=rds.aws.com │ ◄── overrides parent
└────────────────────────────┘
┌─ prod.env ─────────────────┐
│ # rsenv: cloud.env │ ◄── links to parent
│ export LOG_LEVEL=error │ ◄── overrides grandparent
└────────────────────────────┘
Build result — rsenv env build prod.env merges the chain:
prod.env ──inherits──► cloud.env ──inherits──► base.env
Merged output (child wins):
┌────────────────────────────────────────────┐
│ export DB_HOST=rds.aws.com ◄ cloud.env │
│ export DB_PORT=5432 ◄ base.env │
│ export LOG_LEVEL=error ◄ prod.env │
└────────────────────────────────────────────┘
Key commands:
rsenv env tree— visualize the hierarchyrsenv env select— fuzzy-pick an env, write to.envrcrsenv env build <file>— merge and output variablesrsenv env envrc <file>— update the vars section ofdot.envrc
Guard permanently moves sensitive files to the vault and leaves a symlink behind. Git sees the symlink, not the secret.
BEFORE rsenv guard add config/secrets.yaml
═══════ ═══════════════════════════════════
Project Project Vault/guarded
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ config/ │ │ config/ │ │ config/ │
│ secrets.yaml │ ──guard──► │ secrets.yaml ──────► │ secrets.yaml │
│ (real file) │ │ (symlink) │ │ (real file) │
└───────────────────┘ └───────────────────┘ └───────────────────┘
git tracks: real file git tracks: symlink safe, outside git
(dangerous) (harmless)
Dotfile neutralization: Dotfiles are renamed in the vault to prevent
side effects: .gitignore → dot.gitignore, .envrc → dot.envrc.
Key commands:
rsenv guard add <file>— move to vault, create symlinkrsenv guard list— show all guarded filesrsenv guard restore <file>— move back to project
Vault contents can be encrypted at rest using SOPS (with GPG or Age). rsenv uses content-addressed filenames to detect staleness.
Plaintext Encrypted
secrets.env ──encrypt──► secrets.env.a1b2c3d4.enc
^^^^^^^^
SHA-256 hash prefix of plaintext
Modify secrets.env → hash changes → rsenv detects "stale"
Re-encrypt → new hash → secrets.env.f9e8d7c6.enc
Status categories:
┌──────────────────┬──────────────────────────────────┬──────────────┐
│ Status │ Meaning │ Action │
├──────────────────┼──────────────────────────────────┼──────────────┤
│ current │ Hash matches, up-to-date │ None │
│ stale │ Plaintext changed since encrypt │ Re-encrypt │
│ pending_encrypt │ No encrypted version exists │ Encrypt │
│ orphaned │ .enc exists but plaintext gone │ Can delete │
└──────────────────┴──────────────────────────────────┴──────────────┘
A pre-commit hook (rsenv hook install) blocks commits when files are
stale or unencrypted. Plaintext files are auto-added to .gitignore.
┌─────────────────────────────────────────────────────────────────────────┐
│ rsenv workflow │
│ │
│ 1. rsenv init vault create vault, link via .envrc symlink │
│ 2. rsenv env select pick environment, export variables │
│ 3. rsenv guard add .env move secrets to vault (permanent) │
│ 4. rsenv swap in swap in dev overrides (temporary) │
│ 5. rsenv sops encrypt encrypt vault at rest │
│ │
│ ... work ... │
│ │
│ 6. rsenv swap out restore originals, no traces │
│ 7. rsenv sops encrypt re-encrypt if changed │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Defense in depth:
├── Vault location ──── secrets live outside project directory/git
├── Symlinks ────────── git commits harmless symlinks, not secrets
├── SOPS encryption ─── vault contents encrypted at rest
└── .gitignore sync ─── plaintext auto-ignored by git
# macOS (Homebrew)
brew tap sysid/rsenv
brew install rsenv
# Or via Cargo
cargo install rsenv
rsenv init vault # Create vault for project
rsenv guard add .env # Move .env to vault, create symlink
rsenv env tree # View environment hierarchy
rsenv env select # Interactive environment selectionGetting Started: Installation · Quick Start · Core Concepts
Features: Environment Variables · Vault Management · File Swapping · SOPS Encryption
Reference: Commands · Configuration · Troubleshooting · Migration Guide
BSD-3-Clause
