Encrypted environment variable manager for developers who are tired of
.envfiles lying around in plaintext.
sky-env stores your environment variables in an AES-256-CBC encrypted SQLite database, organised by project and environment. The database is portable across machines, safe to back up, and (with the right secret hygiene) safe to commit to a private git repository.
Built with Sky — a pure functional language that compiles to a single Go binary, no runtime dependencies.
The standard developer workflow for environment variables is broken:
.envfiles sit on disk in plaintext, often in the project root.env.exampledrifts out of sync with the real.env- Switching between
dev,staging,prodmeans juggling files - Sharing secrets with teammates means Slack DMs, 1Password lookups, or "ask Bob"
- Backing up secrets means hoping you remembered to copy
.envbefore reformatting your laptop - Multiple projects mean multiple
.envfiles to track and rotate
sky-env fixes all of these:
| Pain | Fix |
|---|---|
| Plaintext on disk | AES-256-CBC encrypted at rest |
.env.example drift |
sky-env diff shows missing keys across environments |
| Juggling files | One database, multiple projects, multiple environments |
| Onboarding teammates | Share the encrypted DB + secret separately (Slack the secret, commit the DB) |
| Laptop reformat | cp ~/.local/sky-env/skyenv.db /backup/ and you're done |
| Multi-project | Auto-detected from cwd's basename |
curl -fsSL https://github.com/anzellai/sky-env/releases/latest/download/sky-env-$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/') -o /usr/local/bin/sky-env
chmod +x /usr/local/bin/sky-env
sky-env initGrab the binary for your platform from Releases:
| Platform | Binary |
|---|---|
| macOS arm64 (Apple Silicon) | sky-env-darwin-arm64 |
| macOS x64 (Intel) | sky-env-darwin-x64 |
| Linux x64 | sky-env-linux-x64 |
| Linux arm64 | sky-env-linux-arm64 |
| Windows x64 | sky-env-windows-x64.exe |
chmod +x the binary, drop it on your PATH, and you're ready.
opensslmust be on yourPATH(installed by default on macOS and most Linux distributions)- That's it — no Node, no Python, no Go, no Sky runtime
git clone https://github.com/anzellai/sky-env
cd sky-env
sky install
sky build src/Main.sky
./sky-out/sky-env --help$ sky-env init
Generated sky-env secret (256 bits, hex-encoded):
2f7eb3c30e4a50f2111e8b402643ca35d0248131ef773550515bd33433326d4d
Add this to your shell config (~/.bashrc or ~/.zshrc):
export SKY_ENV_SECRET="2f7eb3c30e4a50f2111e8b402643ca35d0248131ef773550515bd33433326d4d"
Then restart your shell or run: source ~/.zshrc
Keep this secret safe — losing it means losing access to
all encrypted environment variables stored under this key.The secret is read from /dev/urandom (256 bits of true entropy). It is the only thing you need to keep safe — every encrypted value derives its key from this secret via PBKDF2 (100,000 iterations).
sky-env init will refuse to overwrite an existing SKY_ENV_SECRET to prevent accidentally destroying access to existing data.
$ cd ~/code/my-app
$ ls .env*
.env.dev .env.prod .env.example
$ sky-env import dev
Imported 12 variables into my-app/dev
$ sky-env import prod
Imported 14 variables into my-app/prodsky-env import <env> looks for .env.<env> first, then falls back to .env, then prompts you with .env.example keys interactively. The project name comes from the current directory's basename.
You can now safely delete or git-ignore .env.dev and .env.prod — they're in the encrypted database.
$ sky-env print dev
API_KEY=abc123
DB_URL=postgres://localhost/myapp_dev
STRIPE_KEY=sk_test_xxx
...
# Pipe straight back into your shell
$ eval "$(sky-env print dev | sed 's/^/export /')"
# Or write back to a temporary file for tools that need .env
$ sky-env print dev > .env
node server.js
rm .env$ sky-env set dev API_KEY
Enter value for API_KEY: ********
Set API_KEY in my-app/devThe value is read from stdin so it never appears in your shell history (unlike export API_KEY=...).
$ sky-env diff
Comparing environments for my-app:
KEY dev staging prod
------------------------------------------------------------
API_KEY OK OK OK
DB_URL OK OK OK
STRIPE_KEY OK MISSING OK
SENTRY_DSN MISSING OK OK
Missing keys:
staging is missing: STRIPE_KEY
dev is missing: SENTRY_DSNThis is the killer feature for keeping environments in sync. No more "it works on staging but breaks on prod because we forgot to add SENTRY_DSN."
$ sky-env list
my-app
- dev
- staging
- prod
my-other-app
- dev
- prod
internal-tool
- devWhen you need to swap to a new secret (a teammate leaves, the old one was on a machine you no longer trust, scheduled rotation), one command does the whole thing:
$ sky-env rotate
WARNING: this will rotate the encryption secret.
It will:
1. Back up the database file
2. Generate a new 256-bit secret
3. Decrypt every stored variable with the current secret
4. Re-encrypt them with the new secret and write back
After rotation you MUST update SKY_ENV_SECRET in your shell
config and reload before running any other sky-env command.
The old secret will no longer decrypt the data.
Type 'rotate' to confirm: rotate
Database backed up to:
~/.local/sky-env/skyenv.db.bak.20260409-181237
Re-encrypted 26 variable(s).
NEW SECRET (save this NOW — it will not be shown again):
068a0a3091e2f11cc8f532a7be085203323c4606903cb92c35b5744a8a7643c9
Update your shell config (~/.bashrc or ~/.zshrc):
export SKY_ENV_SECRET="068a0a3091e2f11cc8f532a7be085203323c4606903cb92c35b5744a8a7643c9"The rotation is transactionally safe:
- The database file is copied to a timestamped
.bak.YYYYMMDD-HHMMSSbackup before anything is touched. - Every record is decrypted with the current secret and re-encrypted with the new secret in memory. If any single value fails to decrypt or re-encrypt, the rotation aborts and the database is left untouched.
- Only after every record is validated does sky-env write the new ciphertexts back.
- If a write fails mid-way, sky-env prints the exact
cpcommand to restore from the backup.
You must type the literal word rotate to confirm — anything else cancels with no changes. After a successful rotation, update SKY_ENV_SECRET in your shell config and reload before running any other sky-env command.
sky-env uses AES-256-CBC with PBKDF2 key derivation, via the system openssl binary. Each encrypted value uses:
- A fresh random 8-byte salt (per encryption call)
- PBKDF2-HMAC-SHA256 with 100,000 iterations to derive the AES key from your secret
- AES-256-CBC symmetric cipher
- Output is base64-encoded with the
Salted__magic header so it round-trips through SQLite TEXT columns
This is the same encryption you get from openssl enc -aes-256-cbc -pbkdf2 -salt. It is standards-compliant, interoperable, and decryptable from any tool that can read the OpenSSL salted format — not a custom Sky-only scheme.
✓ At-rest encryption. Without SKY_ENV_SECRET, the SQLite file is opaque ciphertext. No casual or determined reader can recover your values without the secret.
✓ Per-value salts. Encrypting the same value twice produces different ciphertexts. An attacker comparing the database across snapshots cannot tell which values changed.
✓ Slow key derivation. PBKDF2 with 100k iterations means brute-forcing the secret is computationally expensive — orders of magnitude slower than a naive sha256 hash.
✓ Standards-based. If sky-env disappears tomorrow, you can decrypt your database with openssl enc -aes-256-cbc -pbkdf2 -d -base64 -A -pass env:SKY_ENV_SECRET <<< "<ciphertext>".
✗ Losing the secret. PBKDF2 is one-way. If you lose SKY_ENV_SECRET, your data is gone. There is no recovery, no backdoor, no support email. Treat your secret like an SSH private key.
✗ Active malware on your machine. If something is running as your user, it can read both the database AND your environment variable. sky-env protects data at rest, not against running adversaries.
✗ Brief temp file exposure. During encrypt/decrypt, the plaintext is briefly written to a temp file (so openssl can read it without the value appearing in process arguments). This file is created in the OS temp directory with default user-only permissions and deleted immediately. Single-user developer machines: fine. Multi-user shared servers: avoid sky-env or use a tmpfs path.
~/.local/sky-env/skyenv.db -- SQLite database
The database has two tables:
CREATE TABLE projects (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE env_vars (
id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL,
environment TEXT NOT NULL,
key TEXT NOT NULL, -- plain text
value TEXT NOT NULL, -- AES-256-CBC ciphertext, base64
UNIQUE(project_id, environment, key)
);Variable keys are stored in plain text (so you can grep for WHERE key = 'API_KEY'). Variable values are encrypted. Project names are also plain text. If you consider key names sensitive (e.g. STRIPE_LIVE_SECRET_KEY), keep this in mind.
The single SQLite file is the entire data store. To move sky-env to a new machine:
# On the old machine
cp ~/.local/sky-env/skyenv.db /tmp/skyenv.db
# transfer to new machine (scp, syncthing, USB, etc.)
# On the new machine
mkdir -p ~/.local/sky-env
cp /tmp/skyenv.db ~/.local/sky-env/skyenv.db
# AND set the same SKY_ENV_SECRET on the new machine
export SKY_ENV_SECRET="<the same hex string from sky-env init>"The secret never lives in the database, so the database alone is useless without it. The secret never lives on disk (unless you put it in .bashrc/.zshrc), so it's safe from anyone reading your home directory.
Private repo: yes, this is a reasonable backup strategy. Anyone with repo access still cannot read values without the secret. The database is at-rest encrypted with PBKDF2(100k iterations) + AES-256-CBC. Brute-forcing is expensive enough that even a leaked private repo + sufficient compute would not yield secrets quickly. Make sure to:
- Never commit your
SKY_ENV_SECRET(never put it in repo files, only in your shell config or a secrets manager) - Rotate the secret periodically and re-import all values
Public repo: technically possible but not recommended. Even with PBKDF2, a determined attacker with infinite time and compute can attempt offline brute force. If your secret has low entropy (which sky-env init's secrets do not — they're 256 bits from /dev/urandom), this becomes feasible. Use private repos or out-of-band backups for sky-env databases.
The intended workflow:
- One person on the team generates the secret with
sky-env initand shares it once via a secure channel (1Password, encrypted Slack DM, signed email, in person on a sticky note). - Each developer sets
SKY_ENV_SECRETin their shell config from that shared value. - The encrypted database is committed to your private repo (or a shared dropbox / NAS / etc.).
sky-env import devpopulates the database when someone changes a variable.- Everyone else
git pulls to get the updated encrypted database. - Everyone runs
sky-env print devto see the latest values.
When someone leaves the team, rotate the secret:
- Run
sky-env rotate— confirm withrotate, and sky-env will back up the database, re-encrypt every value under a fresh 256-bit secret, and print the new secret - Update
SKY_ENV_SECRETin your shell config to the new value and reload - Share the new secret with remaining teammates via your secure channel
- Commit the rotated database
- Once everyone has updated, delete the
~/.local/sky-env/skyenv.db.bak.*backup
sky-env init Generate a new 256-bit secret (refuses if SKY_ENV_SECRET already set)
sky-env import <env> Encrypt and import .env.<env> (or .env, or interactive from .env.example)
sky-env print <env> Decrypt and print all variables for <env>
sky-env set <env> <key> Set a single variable (prompts for value via stdin)
sky-env list List all stored projects and their environments
sky-env diff Compare environments for the current project, show missing keys
sky-env rotate Rotate the encryption secret (re-encrypts every value, with backup)
The "current project" is always the basename of the working directory. You can manage multiple projects by cd-ing between them.
Entirely in Sky, a pure functional language that compiles to a single Go binary. The compiler is written in itself (self-hosted) and ships with:
- Hindley-Milner type inference — no type annotations needed, but the compiler still catches every type mismatch
- Exhaustiveness checking — all
caseexpressions must cover every constructor - No runtime panics from FFI — Go errors flow through
Result String aat the type level - Single 8MB binary output — no Node, no Python, no Go runtime, no shared libraries
sky-env itself is ~600 lines of Sky source split across 11 modules. The encryption logic in src/Env/Encrypt.sky is ~70 lines. The whole thing builds in under 2 seconds.
If you're curious about Sky as a language, the main repo has 17 examples ranging from "hello world" to a full Sky.Live monitoring dashboard.
MIT — see LICENSE.
Bug reports and PRs welcome. The code is small and idiomatic Sky, so it's a good entry point if you've never seen a functional language compile to Go before.
If you want to add a feature, please open an issue first to discuss. Things on the roadmap:
-
sky-env export <env> > file— explicit export to a file (currently you pipeprint) -
sky-env unset <env> <key>— remove a single variable -
sky-env rename <old> <new>— rename an environment -
sky-env rotate— re-encrypt all values under a new secret in one command - Per-project secrets (different secret per project)
- Native AES via Sky stdlib (currently shells out to openssl)