dotenvage 0.2.0

Dotenv with age encryption: encrypt/decrypt secrets in .env files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# dotenvage

[![Crates.io](https://img.shields.io/crates/v/dotenvage.svg)](https://crates.io/crates/dotenvage)
[![Documentation](https://docs.rs/dotenvage/badge.svg)](https://docs.rs/dotenvage)
[![CI](https://github.com/dataroadinc/dotenvage/workflows/CI%2FCD/badge.svg)](https://github.com/dataroadinc/dotenvage/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/dataroadinc/dotenvage/blob/main/LICENSE)
[![Downloads](https://img.shields.io/crates/d/dotenvage.svg)](https://crates.io/crates/dotenvage)

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:

```bash
cargo install cargo-binstall
cargo binstall dotenvage
```

### Using cargo install

Build from source (slower, requires Rust toolchain):

```bash
cargo install dotenvage
```

### Manual Installation

Download pre-built binaries from
[GitHub Releases](https://github.com/dataroadinc/dotenvage/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

```bash
# 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

```rust,no_run
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:

```bash
# .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:

```bash
# .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):

```bash
# .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:

```bash
# .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:

```bash
# 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
# 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`:

```bash
# .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:

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

## Contributing

Contributions are welcome! Please see
[CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and
guidelines.

## License

Licensed under the Creative Commons Attribution-ShareAlike 4.0
International License. See
[LICENSE](https://github.com/dataroadinc/dotenvage/blob/main/LICENSE)
for details.