dotenvage 0.2.2

Dotenv with age encryption: encrypt/decrypt secrets in .env files
Documentation

dotenvage

Crates.io Documentation CI License: MIT Downloads

Dotenv with age encryption: encrypt/decrypt secrets in .env files.

The key advantage: With encrypted secrets, you can safely commit all your .env* files to version control - including production configs, user-specific settings, and files with sensitive data. No more .gitignore juggling or secret management headaches.

  • Selective encryption of sensitive keys
  • Uses age (X25519) for modern encryption
  • Library + CLI
  • CI-friendly (supports key via env var)
  • Automatic file layering with precedence rules

Installation

Using cargo-binstall (Recommended)

The fastest way to install pre-built binaries:

cargo install cargo-binstall
cargo binstall dotenvage

Using cargo install

Build from source (slower, requires Rust toolchain):

cargo install dotenvage

Manual Installation

Download pre-built binaries from GitHub Releases:

  • Linux (x86_64): dotenvage-x86_64-unknown-linux-gnu.zip
  • Linux (ARM64): dotenvage-aarch64-unknown-linux-gnu.zip
  • macOS (Intel): dotenvage-x86_64-apple-darwin.zip
  • macOS (Apple Silicon): dotenvage-aarch64-apple-darwin.zip
  • Windows (x86_64): dotenvage-x86_64-pc-windows-msvc.zip
  • Windows (ARM64): dotenvage-aarch64-pc-windows-msvc.zip

Usage

# Generate a key
dotenvage keygen

# Encrypt sensitive values in .env.local
dotenvage encrypt .env.local

# Edit (decrypts in editor, re-encrypts on save)
dotenvage edit .env.local

# Set a value (auto-encrypts if key name matches patterns)
dotenvage set FLY_API_TOKEN=abc123 --file .env.local

# Get a decrypted value (searches .env then .env.local)
dotenvage get FLY_API_TOKEN

# List all variables from all .env* files (merged in standard order)
dotenvage list

# List with decrypted values shown (🔒 = encrypted)
dotenvage list --show-values

# List in plain ASCII format (no icons, just variable names)
dotenvage list --plain

# List in JSON format
dotenvage list --json

# List in JSON with values
dotenvage list --json --show-values

# List from a specific file only
dotenvage list --file .env.local

# Show which files are being read (works with any command)
dotenvage --verbose list
dotenvage -v dump

# Dump all decrypted env vars (merges all .env* files with layering)
dotenvage dump

# Dump a specific file
dotenvage dump --file .env.local

# Dump with bash-compliant escaping (for values with $, `, etc.)
dotenvage dump --bash

# Dump in GNU Make format (VAR := value with Make-safe escaping)
dotenvage dump --make

# Dump with export prefix for bash sourcing (auto-enables --bash)
dotenvage dump --export

# Source in bash (loads all env vars into current shell)
eval "$(dotenvage dump --export)"
# or
source <(dotenvage dump --export)

# Use in Makefile (GNU Make) - secure, no temp file created
# $(eval $(shell dotenvage dump --make-eval))
# export
#
# In recipes, use: $$DATABASE_URL (shell env var)
# Not: $(DATABASE_URL) (Make variable expansion)

Library

use dotenvage::{SecretManager, EnvLoader};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load env files with auto-decryption
    EnvLoader::new()?.load()?;

    // Get all variable names (functional style)
    let vars = EnvLoader::new()?.get_all_variable_names()?.join(", ");

    // Encrypt and decrypt values
    let manager = SecretManager::generate()?;
    let enc = manager.encrypt_value("secret")?;
    let dec = manager.decrypt_value(&enc)?;
    Ok(())
}

File Layering

One of dotenvage's key features is automatic file layering - multiple .env* files are loaded and merged with a clear precedence order. Later files override values from earlier files.

Loading Order

Files are loaded using a flexible power-set algorithm that generates all possible combinations of ENV, OS, ARCH, USER, and VARIANT. This allows any combination you need without being constrained by a fixed hierarchy.

Key principle: All multi-part file names use dots as separators only (not dashes), ensuring unambiguous parsing.

Files are loaded in specificity order (later overrides earlier):

  1. .env - Base configuration (always first)
  2. Single-part patterns: .env.<ENV>, .env.<OS>, .env.<ARCH>, .env.<USER>, .env.<VARIANT>
  3. Two-part combinations: .env.<ENV>.<OS>, .env.<ENV>.<ARCH>, .env.<ENV>.<USER>, .env.<ENV>.<VARIANT>, etc.
  4. Three-part combinations: .env.<ENV>.<OS>.<ARCH>, .env.<ENV>.<OS>.<USER>, .env.<ENV>.<OS>.<VARIANT>, etc.
  5. Four-part combinations: .env.<ENV>.<OS>.<ARCH>.<USER>, .env.<ENV>.<OS>.<ARCH>.<VARIANT>, etc.
  6. Five-part combination: .env.<ENV>.<OS>.<ARCH>.<USER>.<VARIANT> (most specific)
  7. .env.pr-<NUMBER> - PR-specific (GitHub Actions only, always last)

All files can be safely committed to git since secrets are encrypted.

Example Combinations

With ENV=prod, OS=linux, ARCH=amd64, USER=alice, VARIANT=docker, these files would be loaded (in order, showing a subset):

  • .env
  • .env.prod
  • .env.linux
  • .env.amd64
  • .env.alice
  • .env.docker
  • .env.prod.linux
  • .env.prod.amd64
  • .env.prod.alice
  • .env.prod.docker
  • .env.linux.amd64
  • .env.linux.alice
  • .env.linux.docker
  • ... (more combinations)
  • .env.prod.linux.amd64.alice
  • .env.prod.linux.amd64.docker
  • .env.prod.linux.alice.docker
  • .env.prod.amd64.alice.docker
  • .env.linux.amd64.alice.docker
  • .env.prod.linux.amd64.alice.docker

With all 5 dimensions set, up to 31 file combinations are checked. You only need to create the files you use - the loader checks which exist.

Placeholders

Placeholder Environment Variables (priority order) Default / Notes
<ENV> DOTENVAGE_ENV, EKG_ENV, VERCEL_ENV, NODE_ENV Defaults to local
<OS> DOTENVAGE_OS, EKG_OS, CARGO_CFG_TARGET_OS, TARGET, RUNNER_OS Runtime detection if not set
<ARCH> DOTENVAGE_ARCH, EKG_ARCH, CARGO_CFG_TARGET_ARCH, TARGET, TARGETARCH, TARGETPLATFORM, RUNNER_ARCH None if not detected
<USER> DOTENVAGE_USER, EKG_USER, GITHUB_ACTOR, GITHUB_TRIGGERING_ACTOR, GITHUB_REPOSITORY_OWNER, USER, USERNAME System username
<VARIANT> DOTENVAGE_VARIANT, EKG_VARIANT, VARIANT None if not set
<PR_NUMBER> PR_NUMBER, GITHUB_REF GitHub Actions only

Supported Operating Systems

The <OS> placeholder supports these canonical values (with normalization):

Canonical File Example Aliases (normalized to canonical)
linux .env.prod.linux -
macos .env.prod.macos darwin, osx
windows .env.prod.windows win32, win
freebsd .env.prod.freebsd -
openbsd .env.prod.openbsd -
netbsd .env.prod.netbsd -
android .env.prod.android -
ios .env.prod.ios -

Supported Architectures

The <ARCH> placeholder supports these canonical values (with normalization):

Canonical File Example Aliases (normalized to canonical)
amd64 .env.prod.amd64 x64, x86_64
arm64 .env.prod.arm64 aarch64
arm .env.prod.arm armv7, armv7l, armhf
i386 .env.prod.i386 i686, x86
riscv64 .env.prod.riscv64 riscv64gc
ppc64le .env.prod.ppc64le powerpc64le
s390x .env.prod.s390x -

Note: Custom architecture values (e.g., docker-s3) are passed through as lowercase and can include dashes within the value itself (e.g., .env.prod.docker-s3), but dots remain the separator between file name parts.

Example

Given these files:

# .env - Base config (safe to commit)
DATABASE_URL=postgres://localhost/dev
API_KEY=public_key

# .env.local - Local overrides (safe to commit with encryption)
DATABASE_URL=postgres://localhost/mydb
SECRET_TOKEN=age[...]  # encrypted, safe to commit!

Running dotenvage dump produces:

# .env
API_KEY=public_key
DATABASE_URL=postgres://localhost/dev

# .env.local
DATABASE_URL=postgres://localhost/mydb
SECRET_TOKEN=decrypted_value

Running dotenvage dump --export produces (note: --export automatically enables bash-compliant escaping):

# .env
export API_KEY=public_key
export DATABASE_URL=postgres://localhost/dev

# .env.local
export DATABASE_URL=postgres://localhost/mydb
export SECRET_TOKEN=decrypted_value

Dynamic Dimension Discovery

Dimension values (ENV, OS, ARCH, USER, VARIANT) can be discovered from loaded .env files, not just environment variables. This enables powerful chained configurations:

# .env - Sets the environment
NODE_ENV=production

# .env.production - Loaded because NODE_ENV=production was discovered
VARIANT=docker

# .env.production.docker - Loaded because VARIANT=docker was discovered
DOCKER_HOST=tcp://localhost:2375

The loader iteratively:

  1. Loads .env first
  2. Discovers dimension values from loaded variables
  3. Computes additional file paths based on discovered values
  4. Repeats until no new files are found

This allows you to set NODE_ENV=staging in .env and have .env.staging automatically loaded, which might set VARIANT=canary causing .env.staging.canary to load as well.

Note: Encrypted dimension values are skipped during discovery (they can't be decrypted until the key is loaded).

Bash-Compliant Escaping

When using --bash or --export (which auto-enables --bash), special bash characters are properly escaped:

# Without --bash (simple .env format)
PASSWORD=my$ecret

# With --bash (bash-safe escaping)
PASSWORD="my\$ecret"

This ensures values with $, `, \, !, and other bash special characters are safely preserved when sourced.

GNU Make Integration

Use --make-eval to securely load variables directly into Make without creating temporary files:

# Makefile example - secure, no temp file with secrets
$(eval $(shell dotenvage dump --make-eval))
export

.PHONY: deploy
deploy:
	@echo "Deploying to $$DATABASE_URL"
	@echo "Using API key: $$API_KEY"

Security Note: --make-eval outputs $(eval ...) statements that are processed directly by Make, avoiding the security risk of writing decrypted secrets to temporary files.

Important: Access variables as $$VAR (environment variables) in recipes, not $(VAR) (Make variable expansion). The export directive makes all variables available to recipe shells as environment variables, where special characters like $ are properly preserved.

Alternative: If you need the Make format for other purposes, --make outputs VAR := value format (but creates a file if redirected).

This layering system allows you to:

  • Commit ALL .env* files to version control - secrets are encrypted
  • Share environment-specific configs across the team (.env.production, .env.staging)
  • Provide user-specific overrides (.env.local.alice) without conflicts
  • Configure architecture-specific settings (.env.local.arm64)

Key Management

Keys are discovered in this priority order:

  1. DOTENVAGE_AGE_KEY env var (full identity string)
  2. AGE_KEY env var (full identity string)
  3. EKG_AGE_KEY env var (for EKG project compatibility)
  4. AGE_KEY_NAME from .env → key file at $XDG_STATE_HOME/{AGE_KEY_NAME}.key
  5. Default: ~/.local/state/{CARGO_PKG_NAME}/dotenvage.key

Project-Specific Keys

For multi-project setups, configure in .env:

# .env (committed, not secret)
AGE_KEY_NAME=myproject/myapp

Key stored at: ~/.local/state/myproject/myapp.key

XDG Base Directories

  • Prefers $XDG_STATE_HOME
  • Falls back to ~/.local/state
  • Or $XDG_CONFIG_HOME / ~/.config (legacy)

CI/CD

Set DOTENVAGE_AGE_KEY, AGE_KEY, or EKG_AGE_KEY in CI secrets:

env:
  DOTENVAGE_AGE_KEY: ${{ secrets.AGE_KEY }}

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for setup instructions and guidelines.

License

Licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. See LICENSE for details.