senv is a secure, decentralized environment variables manager built for the terminal. By utilizing a hybrid RSA/AES-GCM encryption architecture, senv allows teams to safely store encrypted environment configurations inside source control (.senv.json), while maintaining unique local identities to restrict decryption access.
Disclaimer: This project and its underlying code (including the cryptography logic) were fully AI-generated. While standard cryptography algorithms and practices were used, the codebase has not been audited by a human security professional. Use at your own risk.
Instead of maintaining .env files that cannot be safely committed, senv encrypts your environment payloads inside .senv.json.
- AES-256-GCM is used to encrypt the key-value payload.
- RSA-2048 is used to encrypt the AES Data Encryption Key (DEK).
.senv.json stores one encrypted blob per identity (e.g., alice-local, bob-local). Each identity's blob is encrypted with that identity's RSA public key, so only holders of the corresponding private key can decrypt it. Your private keys are kept secure in your local keystore (~/.config/senv/identity.json, created with 0600 permissions) and are never committed.
.senv.json (safe to commit):
{
"version": "1.1",
"public": [
{ "key": "PUBLIC_URL", "value": "http://localhost:3000", "environment": "dev" },
{ "key": "LOG_LEVEL", "value": "debug", "environment": "dev" }
],
"presets": {
"backend": ["API_KEY", "DB_URL"],
"frontend": ["PUBLIC_URL"]
},
"identities": {
"alice-local": "<base64 RSA-encrypted AES-GCM payload>",
"bob-local": "<base64 RSA-encrypted AES-GCM payload>"
}
}identities— one encrypted blob per identity; values are opaque base64 strings (see below).public— optional project-wide plaintext values (URLs, modes, etc.); same item shape as decrypted payload entries (key,value,environment). Readable without a keystore. A key cannot exist in bothpublicand an encrypted identity for the same environment when you can decrypt the relevant identities locally (see limitation below).presets— optional plaintext lists of env-var names (not values); used bysenv use <preset>andsenv preset.
Inside each identity blob — each value in identities is a base64 string. Decoding it yields JSON with the hybrid crypto envelope; decrypting that yields the actual secrets:
// After base64-decode (still encrypted; safe to commit)
{
"encryptedDEK": "<RSA-OAEP-wrapped AES-256 key>",
"iv": "<12-byte GCM nonce, base64>",
"authTag": "<GCM auth tag, base64>",
"encryptedPayload": "<AES-256-GCM ciphertext of the JSON below, base64>"
}// After RSA + AES-GCM decrypt (plaintext; never stored on disk)
[
{ "key": "API_KEY", "value": "super_secret_value", "environment": "dev" },
{ "key": "DB_URL", "value": "postgres://...", "environment": "prod" }
]Each item is one env var scoped to an environment (dev, prod, or any string you pass to -e/--env). Keys must match /^[A-Za-z_][A-Za-z0-9_]*$/; values are strings up to 16 KB. The array can be empty. Multiple items may share the same key when environment differs.
~/.config/senv/identity.json (never commit):
{
"version": "1.0",
"projects": {
"/absolute/path/to/your/project": {
"alice-local": {
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"privateKey": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
}
}
}
}projects— keys are absolute project directory paths; values are identity name → RSA keypair (PEM).- Override the keystore path with
-k/--keystoreorSENV_CONFIG_DIR.
Unless SENV_PROJECT_DIR is set, senv resolves the project directory (for .senv.json and keystore projects keys) in this order:
SENV_PROJECT_DIR— always wins when set.- Current working directory — when
.senv.jsonexists in cwd. - Git repository root — when cwd has no
.senv.json, you are inside a git repo, and the repo root has.senv.json. Lets you runsenv key add,senv use, etc. from a subdirectory withoutcdto the root. - Current working directory — fallback when none of the above apply (commands that need an existing file will error).
Nested configs in one repo: If both cwd and the git root contain .senv.json, cwd wins. Use this for monorepos where individual packages keep their own .senv.json; keep a single root config when the whole repo shares one file.
To allow another team member to access a given identity's secrets, share that identity's private key with them out-of-band (e.g., via a secure channel). They import the base64-encoded keypair into their local keystore with senv identity import. There is no automatic key distribution or multi-recipient encryption; each identity is a single-recipient envelope.
Note: there is currently no command to add a teammate's public key to an existing identity. If you want both Alice and Bob to share the same set of secrets under one name, treat it as a shared identity: have one person generate the keypair, distribute the private key (or its decrypt-only export) to the other, and both keep a copy locally.
curl -fsSL https://raw.githubusercontent.com/Kirow/senv/refs/heads/main/scripts/install.sh | shDownloads the latest release from GitHub, verifies SHA256 checksums, and installs to ~/.local/bin/senv. If Bun is on your PATH, the small bundled JS build is used (~60 KB); otherwise a standalone binary is downloaded for your OS/arch (~60 MB, no Bun required). Requires curl.
Pin a version: SENV_VERSION=0.1.0 curl -fsSL ... | sh
Custom install dir: SENV_INSTALL_DIR=/usr/local/bin curl -fsSL ... | sh
(Make sure ~/.local/bin is in your $PATH!)
- Bun is required to build from source. The bundled JS install also requires Bun at runtime; the standalone binary does not.
Clone the repository and pick an install variant:
git clone https://github.com/your-username/senv.git
cd senv
bun installBundled JS (~60 KB, requires Bun at runtime):
make install # or: make install-jsStandalone binary (~60 MB, no Bun required at runtime):
make install-standaloneBuild artifacts only (without installing):
make build # bundled JS → dist/senv
make build-standalone # standalone binary → dist/senv-standalone
make build-all # both(Make sure ~/.local/bin is in your $PATH!)
- Bump
VERSIONinsrc/version.ts - Commit and tag:
git tag v0.1.0 && git push origin v0.1.0 - GitHub Actions builds all platform binaries, generates
checksums.sha256, and publishes the release
Run this in the root of your project. It will generate a local RSA keypair (if one doesn't exist) and create .senv.json. The default identity name is derived from $USER (with non-alphanumeric characters sanitized to -).
senv initAdd, remove, and list variables. By default, senv targets the dev environment. You can specify a different environment using the -e or --env flag.
# Add a secret (encrypted, per identity)
senv key add my-identity API_KEY "super_secret_value"
# Add a public value (plaintext in .senv.json, no identity or keystore required)
senv key add --public PUBLIC_URL "http://localhost:3000"
senv key rm --public PUBLIC_URL
# List variables (public values shown in plaintext; secrets masked)
senv key list
senv key list -e prod
senv key list -i my-identity
senv key list -i public
# Get a plaintext value
senv key get API_KEYWhen the same key exists in multiple identities,
key get/key listshow a conflict warning on stderr and use the first-encountered identity's value. Pass-i <name>(or--identity <name>) to disambiguate.key listwithout-eshows all environments grouped by identity; with-erestricts to that environment.
Public vs encrypted exclusivity:
senvrejects adding a public key when any identity you can decrypt locally already defines that key (and vice versa). If a teammate's encrypted blob uses the same key name and you lack their private key,senvcannot detect the overlap — review.senv.jsonin git before adding public values.
The identity name
publicis reserved for the project-wide public section (key list -i public). Use a different name for encrypted identities.
You can easily source your decrypted environment variables into your active shell session:
eval $(senv use)
# Or for a specific environment
eval $(senv use -e prod)
# Or only keys from a named preset
eval $(senv use backend)Named subsets of keys stored in plaintext inside .senv.json:
# List all presets
senv preset list
# Define a preset (incremental; dedupes keys)
senv preset add backend API_KEY DB_URL
# Remove specific keys or the whole preset
senv preset rm backend DB_URL
senv preset rm backend
# Verify all preset keys are decryptable for the current env
senv preset check
senv preset check --strictpreset check and senv use <preset> print a [WARN] for each key in the preset that is missing or not decryptable for the target environment. preset check --strict exits with code 1 if any keys are missing.
To allow another team member to access a given identity, export that identity's keys and have the recipient import them.
Export your keys:
senv identity export my-identity
# Export decrypt-only access (private key only)
senv identity export my-identity --decrypt-onlyImport a keypair (yours or a teammate's):
senv identity import "<BASE64_STRING>"If multiple people edit .senv.json simultaneously, use the merge command to safely merge conflicting identity payloads:
senv merge .senv.json .senv.incoming.jsonsenv merge only resolves encrypted identities blobs. Conflicts in public, presets, or other plaintext sections should be resolved with normal git merge tools before or after running senv merge.
When identity conflict markers are present, senv merge preserves an intact public array from outside the conflict block (same as presets). It does not merge or auto-resolve public — only keeps what already survived in the file. Incoming public from a second file argument is ignored; use git to reconcile plaintext sections.
When git conflict markers are present, senv merge uses a branch-name heuristic to pick incoming blobs for identities you cannot decrypt: the >>>>>>> label is matched against the <owner>-local portion of the identity name (e.g. branch alice matches alice-local). Branch names that do not match this pattern (e.g. feature/alice-local) may keep the wrong side — use explicit FILE_A + FILE_B merge instead.
One-time import of keys from a plaintext .env file:
# Into an encrypted identity
senv migrate my-identity .env
# Into the public section (no identity required)
senv migrate --public .envValues larger than 16 KB are skipped with a warning. Uses the current -e/--env environment (default dev).
Bump an older .senv.json on disk to the current schema version (e.g. 1.0 → 1.1):
senv upgradeOlder versions remain readable without upgrading; this only rewrites the version field (and normalizes public if present). Not the same as senv migrate (.env import) or senv update (CLI self-update).
senv updateChecks GitHub for a newer release and runs the same install script as Quick Install (curl … | sh). Only use on networks and sources you trust.
Install the senv agent skill so AI tools know how to use the CLI in this project:
senv install skillThis creates or replaces .agents/skills/secure-env-tool/SKILL.md.
To run the automated test suite covering filesystem operations, cryptography, and integration edge cases:
bun test- Google Gemini 3.1 Pro
- OpenAI Codex 5.3
- MiniMax M3
- Composer 2.5
- DeepSeek V4 Pro