dotseal
Seal individual values in .env files with scoped local keys.
dotseal set prompts without terminal echo when no plaintext source is
provided. For non-interactive use, prefer stdin or a file:
|
dotseal set --value ... is supported for convenience, but the plaintext is
visible through argv (ps, shell history, sudo/audit logs, and similar tooling).
No scope writes to .env and uses the default key. A scope writes to
.env.<scope> and uses masterkey.<scope>.
API_SUPER_KEY=enc:v1:...
By default keys are stored under:
~/.config/dotseal/masterkey.default
~/.config/dotseal/masterkey.prod
Automatic key-file creation is currently hardened on Unix only. On Windows,
use --key-cmd or provide an externally protected --key-file until Dotseal
sets current-user-only DACLs itself.
Use --key-file or --key-cmd to override key loading:
--key-cmd reads the command's stdout as UTF-8 text and trims surrounding
whitespace before parsing the key.
dotseal protects values at rest in dotenv files. It does not hide a secret
from a process that can read both the encrypted file and the matching master
key.
When dotseal fits
Good fit when you want to:
- back up a
.env.productionto a sync service (iCloud, Dropbox, a NAS, a 1Password attachment) without keeping plaintext in that channel - distribute
.env.productionto teammates via channels you wouldn't trust with plaintext (chat, S3, an internal artifact store) - protect values at rest on a deployment host where the binary needs them but other tenants on the host shouldn't see them
- rotate a single secret without re-encrypting everything around it
- run a binary with decrypted secrets via
dotseal execwithout writing the plaintext to disk - mix encrypted and plaintext values in the same env file (
PORT=3000stays plain; only sensitive values are sealed) - give a sandboxed process access to a project tree without granting it the means to decrypt those secrets (see § Coding-agent sandboxing)
Not a fit for:
- centrally-managed secret distribution to fleets (use Vault, AWS Secrets Manager, Doppler, Infisical)
- per-user / per-role access control on individual values — dotseal is a symmetric scheme, anyone with the master key can read every sealed value in that scope
Not a substitute for
.gitignore. Encryption does not make.env.*safe to commit. Master-key compromise — leaked from a teammate's password manager, a stolen laptop, an over-shared vault — makes every past commit retroactively decryptable, and git's append-only history means rotating the key cannot rescind a leaked blob already in the log. Treat encrypted.envfiles the same as plaintext ones for source control: keep them out. Dotseal is for moving and storing the file, not for shipping it through history.
Use cases
1. Encrypted backup of .env
You want your .env.production to survive a laptop loss without keeping
a plaintext copy in iCloud, Dropbox, or a shared drive.
The resulting file:
NODE_ENV=production
PORT=3000
OPENAI_API_KEY=enc:v1:Cy2Wxt...
STRIPE_SECRET_KEY=enc:v1:Y8q4dK...
DATABASE_URL=enc:v1:mAk9p4...
This file is now safe to drop into iCloud Drive / Dropbox / a NAS / a
1Password attachment. The master key file
(~/.config/dotseal/masterkey.production) does not go through the
same channel — back it up to your password manager separately.
Recover on a new machine:
# 1. Restore the master key (from password manager, gpg-encrypted backup, etc.)
# 2. The synced .env.production is already in place — just run.
The encrypted file by itself reveals variable names and value lengths (approximately, via the encoded ciphertext length) but not the values.
2. Per-secret rotation
A vendor leaks an API key — rotate it without touching anything else:
doctor --all re-decrypts every encrypted entry and reports failures one
by one. It also warns on duplicate names.
3. Multi-environment setup
Each scope writes to its own .env.<scope> and uses its own master key.
The AAD binding means a value sealed under staging cannot be decrypted
with the production key — even if a developer accidentally swaps files.
Pick at runtime:
4. CI/CD: decrypt-on-deploy
# .github/workflows/deploy.yml
- name: Deploy
env:
DOTSEAL_KEY: ${{ secrets.DOTSEAL_PRODUCTION_KEY }}
run: |
dotseal -s production --key-cmd 'printf "%s" "$DOTSEAL_KEY"' \
exec . ./scripts/deploy.sh
--key-cmd reads the master key from a shell command's stdout — the key
never lands in a file.
5. systemd unit with sealed env
[Service]
ExecStart=/usr/local/bin/dotseal -s production \
--key-file /etc/dotseal/masterkey.production \
exec /etc/myapp /usr/local/bin/myapp
User=myapp
dotseal exec forwards SIGINT, SIGTERM, SIGHUP to the child, so
systemctl stop myapp propagates correctly. Plaintext lives only in the
process's env page — never on disk.
6. Coding-agent sandboxing
You're running a coding agent (Claude Code, Cursor, an LLM-driven CI bot)
inside a sandbox over a project that contains .env.production. The
sandbox profile already restricts the agent to the project tree.
Because the master key file lives outside the project tree (default
~/.config/dotseal/masterkey.<scope>), the agent reads the encrypted
values but cannot decrypt them. There's nothing to move, nothing to
re-key, nothing to mask in source.
To revoke an agent's access to a scope:
# firejail: blacklist the dotseal config dir
# bubblewrap: simply don't bind ~/.config/dotseal into the sandbox
# Apple Seatbelt / sandbox-exec: omit ~/.config/dotseal from the
# allow-list of readable paths.
To grant an agent access to a specific scope while denying others, point it at one explicit key file rather than letting it scan the default location:
Inside the sandbox, only the staging key is reachable; production
remains opaque even though .env.production is in the project tree.
Per-scope blacklist/whitelist is a path-policy concern, not a dotseal one — that's the point. You don't have to rearrange your repo to revoke.
7. Local dev: master key from a password manager
Don't keep a long-lived key file on your laptop. Pull on demand:
# 1Password CLI
# pass(1)
If the manager prompts for a touch / unlock, you'll get one prompt per
dotseal invocation.
8. Validation in an existing toolchain
Detect malformed sealed values at boot, even outside the dotseal load path:
// Node: from 'dotseal-env'; Deno: from '@dotseal/env' (JSR).
import from 'dotseal-env'
isEncryptedValue, isSafeEnvName, isValidScope are pure predicates
exported by every loader.
9. Migration from a plaintext .env
for; do
current=
done
Plaintext entries stay plaintext until you set on them. Migration is
incremental — one secret per PR is fine.
Packages
This repo intentionally keeps the CLI and runtime loaders separate:
dotseal Rust CLI and Rust loader crate
dotseal-env JavaScript loader primitives (npm)
dotseal-env Python loader primitives (PyPI)
@dotseal/env Deno loader primitives (JSR)
dotseal-go Go loader primitives
packages/shell POSIX shell helpers over the dotseal CLI
The loaders decrypt the same enc:v1:<payload> envelope and do not implement
project-specific config merging.
Rust usage:
use ;
let key = parse_key?;
let env = parse_env;
let decrypted = decrypt_env?;
Shell usage:
dotseal print-env emits shell-quoted assignments. It is useful but should be
treated as sensitive output. Prefer dotseal exec when possible.
Format
See FORMAT.md.