{"id":33185819,"url":"https://github.com/sysid/rs-env","last_synced_at":"2026-02-24T19:04:18.717Z","repository":{"id":188248138,"uuid":"678398444","full_name":"sysid/rs-env","owner":"sysid","description":"Developer Environments Evolved","archived":false,"fork":false,"pushed_at":"2026-02-04T18:35:45.000Z","size":7623,"stargazers_count":41,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-05T05:49:45.247Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://sysid.github.io/hierarchical-environment-variable-management/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sysid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-08-14T13:12:01.000Z","updated_at":"2026-02-04T18:35:49.000Z","dependencies_parsed_at":"2026-01-07T06:02:13.136Z","dependency_job_id":null,"html_url":"https://github.com/sysid/rs-env","commit_stats":{"total_commits":80,"total_committers":1,"mean_commits":80.0,"dds":0.0,"last_synced_commit":"b0a071d958e0fe75a083970e3112844174a10e81"},"previous_names":["sysid/rs-env"],"tags_count":72,"template":false,"template_full_name":null,"purl":"pkg:github/sysid/rs-env","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sysid%2Frs-env","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sysid%2Frs-env/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sysid%2Frs-env/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sysid%2Frs-env/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sysid","download_url":"https://codeload.github.com/sysid/rs-env/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sysid%2Frs-env/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29796854,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T16:37:37.581Z","status":"ssl_error","status_checked_at":"2026-02-24T16:37:37.074Z","response_time":75,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-11-16T05:00:20.102Z","updated_at":"2026-02-24T19:04:18.711Z","avatar_url":"https://github.com/sysid.png","language":"Rust","funding_links":[],"categories":["\u003ca name=\"system\"\u003e\u003c/a\u003eSystem tools"],"sub_categories":[],"readme":"\u003cimg src=\"doc/logo.png\" alt=\"rsenv logo\" width=\"240\"\u003e\n\n\n[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)\n[![Rust](https://img.shields.io/badge/rust-2021-orange.svg)](https://www.rust-lang.org/)\n\n\u003e This is a complete rewrite of V1 with 100% functional compatibility and new features.\n\n## Why?\n\n- Working on several projects, with different teams, many repos is my reality.\n- I need a personal workspace, which is attached to the project, but does not become part of its\n  official repository (e.g. patched docker-compose.yml, bespoke Java test classes, etc.)\n- My environment configurations are not DRY, they share variables, often follow even a hierarchy\n  (globlal -\u003e company -\u003e region -\u003e stage)\n\n\n---\n\n## 1. The Vault\n\nThe vault is a directory **outside the project** that holds a personal\nworkspace: env files, secrets, dev overrides. \n\nIt is linked to the project via a single `.envrc` symlink — **the only trace of rsenv in your repo**.\n\n```\n  YOUR PROJECT                              THE VAULT\n  ~/projects/myapp/                         ~/.rsenv/vaults/myapp-a1b2c3d4/\n  ┌─────────────────────────┐               ┌──────────────────────────────┐\n  │                         │               │                              │\n  │  .envrc ───── symlink ──────────────────── dot.envrc                   │\n  │                         │               │                              │\n  │  src/                   │               │  envs/                       │\n  │  Makefile               │               │    local.env                 │\n  │  docker-compose.yml     │               │    prod.env                  │\n  │  config/                │               │                              │\n  │    secrets.yaml         │               │  guarded/     (secrets)      │\n  │    database.yml         │               │  swap/        (dev overrides)│\n  │                         │               │  .rsenv.toml  (vault config) │\n  └─────────────────────────┘               └──────────────────────────────┘\n        git-tracked                              outside project git\n        minimal footprint:                       your personal workspace\n        ONE symlink\n```\n\n**How it connects**: `rsenv init vault` creates the vault, moves the `.envrc`\nthere (as `dot.envrc`), and creates the symlink.\n\n---\n\n## 2. File Swapping\n\nSwap temporarily replaces project files with your dev versions.\nUnlike guard, this is reversible — swap in when you start work, swap out when done.\n\n```\n  ┌─ NORMAL STATE (swapped out) ──────────────────────────────────────────┐\n  │                                                                       │\n  │  Project                              Vault/swap                      │\n  │  ┌──────────────────────┐             ┌──────────────────────┐        │\n  │  │ docker-compose.yml   │             │ docker-compose.yml   │        │\n  │  │ (official version)   │             │ (your dev version)   │        │\n  │  └──────────────────────┘             └──────────────────────┘        │\n  │                                                                       │\n  └───────────────────────────────────────────────────────────────────────┘\n                          │ rsenv swap in\n                          ▼\n  ┌─ SWAPPED IN (working) ────────────────────────────────────────────────┐\n  │                                                                       │\n  │  Project                              Vault/swap                      │\n  │  ┌──────────────────────┐             ┌──────────────────────────┐    │\n  │  │ docker-compose.yml   │             │ docker-compose.yml       │    │\n  │  │ (your dev version)   │             │   .rsenv_original        │    │\n  │  │  ◄── moved here      │             │   ◄── backup of official │    │\n  │  └──────────────────────┘             │                          │    │\n  │                                       │ docker-compose.yml       │    │\n  │                                       │   .\u003chostname\u003e.rsenv_active    │\n  │                                       │   ◄── sentinel (who did it)   │\n  │                                       └──────────────────────────┘    │\n  │                                                                       │\n  └───────────────────────────────────────────────────────────────────────┘\n                          │ rsenv swap out\n                          ▼\n               back to normal state\n        (your changes to dev version are PRESERVED)\n```\n\n**Hostname tracking**: The sentinel `.\u003chostname\u003e.rsenv_active` records which\nmachine swapped the file in, preventing conflicts when sharing vaults.\n\n**Key commands**:\n- `rsenv swap init \u003cfiles\u003e` — set up files for swapping (first time)\n- `rsenv swap in` — replace project files with vault versions\n- `rsenv swap out` — restore originals (no args = all files)\n- `rsenv swap status` — show what's swapped in, by which host\n- `rsenv swap status --silent` — exit code only: 0=clean, 1=dirty, 2=unmanaged\n\n---\n\n## 3. Environment Hierarchy\n\nEnv files form a tree using the `# rsenv: parent.env` directive.\nChildren inherit all parent variables and can override them.\n\n```\n  File contents:                          Resulting tree:\n\n  ┌─ base.env ─────────────────┐               base.env\n  │ export DB_HOST=localhost   │              /        \\\n  │ export DB_PORT=5432        │         local.env    cloud.env\n  │ export LOG_LEVEL=info      │                      /       \\\n  └────────────────────────────┘             staging.env    prod.env\n\n  ┌─ cloud.env ────────────────┐\n  │ # rsenv: base.env          │  ◄── links to parent\n  │ export DB_HOST=rds.aws.com │  ◄── overrides parent\n  └────────────────────────────┘\n\n  ┌─ prod.env ─────────────────┐\n  │ # rsenv: cloud.env         │  ◄── links to parent\n  │ export LOG_LEVEL=error     │  ◄── overrides grandparent\n  └────────────────────────────┘\n```\n\n**Build result** — `rsenv env build prod.env` merges the chain:\n\n```\n  prod.env ──inherits──► cloud.env ──inherits──► base.env\n\n  Merged output (child wins):\n  ┌────────────────────────────────────────────┐\n  │ export DB_HOST=rds.aws.com   ◄ cloud.env   │\n  │ export DB_PORT=5432          ◄ base.env    │\n  │ export LOG_LEVEL=error       ◄ prod.env    │\n  └────────────────────────────────────────────┘\n```\n\n**Key commands**:\n- `rsenv env tree` — visualize the hierarchy\n- `rsenv env select` — fuzzy-pick an env, write to `.envrc`\n- `rsenv env build \u003cfile\u003e` — merge and output variables\n- `rsenv env envrc \u003cfile\u003e` — update the vars section of `dot.envrc`\n\n---\n\n## 4. File Guarding\n\nGuard permanently moves sensitive files to the vault and leaves a symlink behind.\nGit sees the symlink, not the secret.\n\n```\n  BEFORE                              rsenv guard add config/secrets.yaml\n  ═══════                             ═══════════════════════════════════\n\n  Project                              Project                     Vault/guarded\n  ┌───────────────────┐                ┌───────────────────┐       ┌───────────────────┐\n  │ config/           │                │ config/           │       │ config/           │\n  │   secrets.yaml    │   ──guard──►   │   secrets.yaml ──────►   │   secrets.yaml     │\n  │   (real file)     │                │   (symlink)       │       │   (real file)     │\n  └───────────────────┘                └───────────────────┘       └───────────────────┘\n\n  git tracks: real file                git tracks: symlink          safe, outside git\n              (dangerous)                          (harmless)\n```\n\n**Dotfile neutralization**: Dotfiles are renamed in the vault to prevent\nside effects: `.gitignore` → `dot.gitignore`, `.envrc` → `dot.envrc`.\n\n**Key commands**:\n- `rsenv guard add \u003cfile\u003e` — move to vault, create symlink\n- `rsenv guard list` — show all guarded files\n- `rsenv guard restore \u003cfile\u003e` — move back to project\n\n---\n\n## 5. SOPS Encryption\n\nVault contents can be encrypted at rest using SOPS (with GPG or Age).\nrsenv uses content-addressed filenames to detect staleness.\n\n```\n  Plaintext                    Encrypted\n  secrets.env      ──encrypt──►  secrets.env.a1b2c3d4.enc\n                                            ^^^^^^^^\n                                            SHA-256 hash prefix of plaintext\n\n  Modify secrets.env → hash changes → rsenv detects \"stale\"\n  Re-encrypt         → new hash      → secrets.env.f9e8d7c6.enc\n```\n\n**Status categories**:\n\n```\n  ┌──────────────────┬──────────────────────────────────┬──────────────┐\n  │ Status           │ Meaning                          │ Action       │\n  ├──────────────────┼──────────────────────────────────┼──────────────┤\n  │ current          │ Hash matches, up-to-date         │ None         │\n  │ stale            │ Plaintext changed since encrypt  │ Re-encrypt   │\n  │ pending_encrypt  │ No encrypted version exists      │ Encrypt      │\n  │ orphaned         │ .enc exists but plaintext gone   │ Can delete   │\n  └──────────────────┴──────────────────────────────────┴──────────────┘\n```\n\nA pre-commit hook (`rsenv hook install`) blocks commits when files are\nstale or unencrypted. Plaintext files are auto-added to `.gitignore`.\n\n---\n\n## Putting It All Together\n\n```\n  ┌─────────────────────────────────────────────────────────────────────────┐\n  │                          rsenv workflow                                 │\n  │                                                                         │\n  │  1. rsenv init vault         create vault, link via .envrc symlink      │\n  │  2. rsenv env select         pick environment, export variables         │\n  │  3. rsenv guard add .env     move secrets to vault (permanent)          │\n  │  4. rsenv swap in            swap in dev overrides (temporary)          │\n  │  5. rsenv sops encrypt       encrypt vault at rest                      │\n  │                                                                         │\n  │  ... work ...                                                           │\n  │                                                                         │\n  │  6. rsenv swap out           restore originals, no traces               │\n  │  7. rsenv sops encrypt       re-encrypt if changed                      │\n  │                                                                         │\n  └─────────────────────────────────────────────────────────────────────────┘\n\n  Defense in depth:\n  ├── Vault location ──── secrets live outside project directory/git\n  ├── Symlinks ────────── git commits harmless symlinks, not secrets\n  ├── SOPS encryption ─── vault contents encrypted at rest\n  └── .gitignore sync ─── plaintext auto-ignored by git\n```\n\n[See all features in the wiki](https://github.com/sysid/rs-env/wiki)\n\n## Quick Start\n\n```bash\n# macOS (Homebrew)\nbrew tap sysid/rsenv\nbrew install rsenv\n\n# Or via Cargo\ncargo install rsenv\n\nrsenv init vault            # Create vault for project\nrsenv guard add .env        # Move .env to vault, create symlink\nrsenv env tree              # View environment hierarchy\nrsenv env select            # Interactive environment selection\n```\n\n[Full quick start guide](https://github.com/sysid/rs-env/wiki/Quick-Start)\n\n[Full command reference](https://github.com/sysid/rs-env/wiki/Command-Reference)\n\n## Documentation\n\n**Getting Started**: [Installation](https://github.com/sysid/rs-env/wiki/Installation) · [Quick Start](https://github.com/sysid/rs-env/wiki/Quick-Start) · [Core Concepts](https://github.com/sysid/rs-env/wiki/Core-Concepts)\n\n**Features**: [Environment Variables](https://github.com/sysid/rs-env/wiki/Environment-Variables) · [Vault Management](https://github.com/sysid/rs-env/wiki/Vault-Management) · [File Swapping](https://github.com/sysid/rs-env/wiki/File-Swapping) · [SOPS Encryption](https://github.com/sysid/rs-env/wiki/SOPS-Encryption)\n\n**Reference**: [Commands](https://github.com/sysid/rs-env/wiki/Command-Reference) · [Configuration](https://github.com/sysid/rs-env/wiki/Configuration) · [Troubleshooting](https://github.com/sysid/rs-env/wiki/Troubleshooting) · [Migration Guide](https://github.com/sysid/rs-env/wiki/MIGRATION)\n\n## License\n\nBSD-3-Clause\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsysid%2Frs-env","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsysid%2Frs-env","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsysid%2Frs-env/lists"}