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

