Secure, ephemeral secret sharing for developers.
Stop pasting secrets into Slack. enseal makes the secure path faster than the insecure one — share .env files and secrets through encrypted, single-use channels with one command and zero setup.
# sender
$ enseal share .env
Share code: 7-guitarist-revenge
Secrets: 14 variables (staging)
Expires: on first receive
# recipient
$ enseal receive 7-guitarist-revenge
ok: 14 secrets written to .envcargo install ensealgit clone https://github.com/FlerAlex/enseal.git
cd enseal
cargo build --release
# binary at ./target/release/ensealDownload from GitHub Releases for Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), and Windows.
Share secrets using a one-time code. No keys, no accounts — works immediately.
# terminal 1 (sender)
enseal share .env
Share code: 7-guitarist-revenge
Secrets: 14 variables
Expires: on first receive
# terminal 2 (recipient) — enter the code
enseal receive 7-guitarist-revenge
ok: 14 secrets written to .envWorks with single secrets too:
# terminal 1 — pipe a token
echo "my-api-token" | enseal share --label "API key"
Share code: 4-orbital-hammock
# terminal 2 — prints to stdout
enseal receive 4-orbital-hammock
my-api-tokenBoth terminals must be open at the same time — the sender waits until the recipient connects.
For teams with established key trust. Encrypt to a name, no coordination needed.
# one-time setup
enseal keys init
enseal keys export > my-key.pub # share this with teammates
enseal keys import teammate-key.pub # import theirs
# sender encrypts to recipient by name
enseal share .env --to sarah
# or push through the public relay (no codes at all)
enseal share .env --to sarah --relay wss://relay.enseal.dev
# or produce an encrypted file (no network)
enseal share .env --to sarah --output ./drop/# anonymous mode: sender gives you a code
enseal inject 7-guitarist-revenge -- npm start
# identity mode: listen on relay, sender pushes when ready
enseal inject --listen --relay wss://relay.enseal.dev -- docker compose up
# from an encrypted file drop
enseal inject ./staging.env.age -- python manage.py runserverSecrets exist only in the child process's memory. When it exits, they're gone.
Anonymous mode (default) — wormhole-based, zero setup. A human-readable code is all you need. SPAKE2 mutual authentication prevents MITM attacks.
enseal share .env # generates wormhole code
enseal receive 7-guitarist-revenge # uses codeWith --relay, anonymous mode bypasses wormhole and uses the enseal relay transport instead. Both sides must use the same relay:
enseal share .env --relay ws://relay.internal:4443 # generates channel code
enseal receive 3421-amber-frost --relay ws://relay.internal:4443Async upload (--upload) — sender-only. Encrypts locally and posts to burnurl.dev, returning a self-destructing URL the recipient opens in a browser. No CLI required on the recipient side.
enseal share .env --upload
# Secret URL: https://burnurl.dev/s/a3f9c2e1...
# Expires: 2026-03-08 19:42:00 UTC (24h)
# Reads: 1 (self-destructs on first open)Recipient opens the URL in any browser — no enseal install needed. Add --passphrase to encrypt client-side before upload (server sees only ciphertext):
enseal share .env --upload --passphrase # prompts for passphrase
enseal share .env --upload --ttl 4 # 4-hour TTL (max 24)API access requires a Pro or Team plan on burnurl.dev. Set BURNURL_API_KEY to your key. Override the base URL for self-hosted instances: BURNURL_URL=https://burnurl.internal.
Identity mode — public-key encryption for known teammates. Encrypt to a name.
enseal keys init # one-time setup
enseal share .env --to sarah # encrypt to sarah's public keyIdentity mode supports three transport options:
# wormhole (default, no --relay): generates a code like anonymous mode
enseal share .env --to sarah
# relay push (with --relay): zero codes, pushes directly to recipient's channel
enseal share .env --to sarah --relay wss://relay.enseal.dev
# file drop (with --output): no network, produces encrypted file
enseal share .env --to sarah --output ./drop/
# produces ./drop/sarah@company.com.env.ageenseal accepts secrets from multiple sources:
# .env file (default)
enseal share .env
enseal share staging.env
# environment profile
enseal share --env staging # resolves to .env.staging
# pipe from stdin
echo "sk_live_abc123" | enseal share
cat secrets.env | enseal share
pass show stripe/key | enseal share --to sarah
# inline (careful — visible in shell history)
enseal share --secret "API_KEY=sk_live_abc123"
# wrap raw string as KEY=VALUE
echo "sk_live_abc123" | enseal share --as STRIPE_KEY${VAR} references are resolved before sending so recipients get fully expanded values:
DB_HOST=postgres.internal
DB_PORT=5432
DATABASE_URL=postgres://user:pass@${DB_HOST}:${DB_PORT}/myappSupports ${VAR:-default} fallback syntax. Circular and forward references are detected and rejected. Use --no-interpolate to send raw ${VAR} syntax.
Control which variables are sent:
# exclude public/non-secret vars
enseal share .env --exclude "^PUBLIC_|^NEXT_PUBLIC_"
# send only matching vars
enseal share .env --include "^DB_|^API_"
# skip .env parsing entirely (send raw file)
enseal share .env --no-filterOutput adapts to what was sent:
# .env payload -> writes to file
enseal receive CODE
ok: 14 secrets written to .env
# write to specific file
enseal receive CODE --output staging.env
# raw string -> prints to stdout (pipe-friendly)
enseal receive CODE
sk_live_abc123
# force clipboard
enseal receive CODE --clipboard
ok: copied to clipboard
# force stdout for any payload
enseal receive CODE --no-write
# receive from encrypted file drop (identity mode)
enseal receive ./staging.env.age
ok: signature verified, file decrypted
ok: 14 secrets written to .envReceive secrets and inject them directly as environment variables into a child process. Secrets never touch the filesystem.
# anonymous mode: inject via wormhole code
enseal inject 7-guitarist-revenge -- npm start
# identity mode: listen for incoming transfer on relay
enseal inject --listen --relay wss://relay.enseal.dev -- docker compose up
# from encrypted file drop
enseal inject ./staging.env.age -- python manage.py runserverWith --listen, the receiver connects to the relay and waits. The sender pushes with enseal share .env --to alex --relay wss://relay.enseal.dev — no codes exchanged, zero coordination needed.
Beyond sharing, enseal is a complete .env security toolkit:
# check: verify your .env has all required vars
enseal check
error: missing from .env (present in .env.example):
JWT_SECRET, REDIS_URL
# diff: compare two .env files (keys only, never values)
enseal diff .env.development .env.staging
+ REDIS_CLUSTER_URL (only in staging)
- DEBUG (only in development)
# redact: strip values for safe sharing of structure
enseal redact .env
DATABASE_URL=<REDACTED>
API_KEY=<REDACTED>
PORT=<REDACTED>
# validate: check values against schema rules
enseal validate .env
error: missing required: JWT_SECRET
error: PORT value "abc" is not an integer
ok: 11/14 variables passed validation
# template: generate .env.example with type hints
enseal template .env
# DATABASE_URL=<postgres connection string>
# API_KEY=<32+ character string>
# PORT=<integer, 1024-65535>Encrypt .env files for safe git storage using age encryption:
# whole-file encryption
enseal encrypt .env
ok: .env encrypted in-place (14 variables, age key)
enseal decrypt .env
# per-variable: keys visible for diffing, values encrypted
enseal encrypt .env --per-var
# DB_HOST=ENC[age:abc123...]
# DB_PORT=ENC[age:def456...]
# multi-recipient: anyone on the team can decrypt
enseal encrypt .env --to sarah --to alex# generate your keypair
enseal keys init
# share your public key with teammates
enseal keys export > my-key.pub
# import a teammate's key (shows fingerprint, prompts for confirmation)
enseal keys import sarah.pub
# list all trusted keys and aliases
enseal keys list
# show your key fingerprint (for out-of-band verification)
enseal keys fingerprint
# remove a trusted key
enseal keys remove sarah@company.com
# create aliases for convenience
enseal keys alias sarah sarah@company.com
# create groups for multi-recipient sharing
enseal keys group create backend-team
enseal keys group add backend-team sarah
enseal keys group add backend-team alex
enseal keys group list backend-team
enseal share .env --to backend-team
# delete a group
enseal keys group delete backend-teamA free public relay is available at wss://relay.enseal.dev. Use it for quick testing or when you don't need a private relay.
# check relay health
curl https://relay.enseal.dev/health
# use it for identity-mode transfers
enseal share .env --to sarah --relay wss://relay.enseal.dev
enseal inject --listen --relay wss://relay.enseal.dev -- npm start
# or set it globally
export ENSEAL_RELAY=wss://relay.enseal.dev# 1. generate keys (one-time)
enseal keys init
# 2. export and import your own key (for self-testing)
enseal keys export > /tmp/mykey.pub
enseal keys import /tmp/mykey.pub
# enter an alias when prompted (e.g. "mykey")
# 3. test file drop (no network needed)
enseal share --secret "TEST=works" --to mykey --output /tmp/
enseal receive /tmp/mykey.env.age
# 4. test relay push (two terminals)
# terminal 1 (receiver):
enseal inject --listen --relay wss://relay.enseal.dev -- env | grep TEST
# terminal 2 (sender):
enseal share --secret "TEST=relay_works" --to mykey --relay wss://relay.enseal.devKeep everything inside your network. The relay is stateless — it sees only ciphertext.
# Docker (one command)
docker run -d -p 4443:4443 enseal/relay
# Or as a binary
enseal serve --port 4443
# Check relay health
curl http://localhost:4443/healthenseal serve speaks plain WebSocket (ws://). For TLS, put a reverse proxy (Caddy, nginx) in front and connect with wss://.
With --relay set, all modes route through your relay:
# Anonymous mode — generates enseal channel code instead of wormhole code
enseal share .env --relay ws://relay.internal:4443
# info: Share code: 3421-amber-frost
enseal receive 3421-amber-frost --relay ws://relay.internal:4443
# Or set globally
export ENSEAL_RELAY=ws://relay.internal:4443
enseal share .envIdentity mode with a self-hosted relay is fully codeless:
# receiver listens on the relay
enseal inject --listen --relay ws://relay.internal:4443 -- npm start
# sender pushes directly — no code generated
enseal share .env --to alex --relay ws://relay.internal:4443
ok: pushed to alexDefine rules in .enseal.toml at the project root:
[schema]
required = ["DATABASE_URL", "API_KEY", "JWT_SECRET"]
[schema.rules.DATABASE_URL]
pattern = "^postgres://"
description = "PostgreSQL connection string"
[schema.rules.PORT]
type = "integer"
range = [1024, 65535]
[schema.rules.API_KEY]
min_length = 32Then validate:
enseal validate .envValidation also runs automatically when receiving .env files — catching broken configs before they cause confusion.
enseal share --env staging # shares .env.staging
enseal validate --env production # validates .env.production
enseal diff .env.development .env.productionWormhole (default, no --relay):
- Sender encrypts the payload with
age - A SPAKE2 key exchange establishes a shared secret via the public wormhole relay
- The encrypted payload transits through the relay
- Recipient decrypts with the negotiated key
- The channel is destroyed — single use, time-limited
The relay never sees plaintext. The wormhole code provides mutual authentication.
Enseal relay (--relay):
- Sender encrypts the payload with
ageand sends it to the enseal relay under a generated channel code - Recipient connects to the same relay with the same code and receives the payload
- The channel is consumed on first receive
There is no SPAKE2 in this mode — the channel code is the only credential.
- The sender serializes the payload to an
Envelope(JSON, SHA-256 integrity check) - Optionally encrypts it client-side with an age scrypt passphrase (
--passphrase) - POSTs the payload to
burnurl.dev/api/secretover HTTPS - burnurl.dev stores it with server-side AES-256-GCM at rest and returns a self-destruct URL
- The URL is valid for the configured TTL (up to 24h on the free tier), single read only
The sender shares the URL. The recipient opens it in any browser — no enseal needed. With --passphrase, the passphrase must be shared separately; the server never sees plaintext.
Tiers: API access requires a Pro or Team plan on burnurl.dev. Set BURNURL_API_KEY to your key — the free tier has no API access.
Override BURNURL_URL to point at a self-hosted burnurl instance.
- Sender encrypts with the recipient's
agepublic key - Sender signs with their own
ed25519key - Payload transits through relay, file drop, or wormhole
- Recipient decrypts with their private key
- Recipient verifies the sender's signature
Trust is based on which keys you've imported.
Transport options in identity mode:
| Transport | Flag | How it works |
|---|---|---|
| Wormhole (default) | --to sarah |
Generates a code, like anonymous mode but with signing |
| Relay push | --to sarah --relay URL |
Pushes to recipient's deterministic channel, no code |
| File drop | --to sarah --output ./dir/ |
Produces encrypted .env.age file, no network |
With relay push, the recipient listens with enseal inject --listen --relay URL -- cmd or receives the file drop with enseal receive ./file.env.age.
Protected:
- Secrets in transit (encrypted channel)
- Secrets in Slack/email history (ephemeral, no persistence)
- MITM attacks (SPAKE2 / public key auth)
- Malicious relay (E2E encryption, relay sees ciphertext only)
- Sender impersonation (identity mode: ed25519 signatures)
- Secrets on disk (inject mode: process memory only)
- Secrets in git (encrypt: at-rest encryption)
Not protected:
- Compromised endpoints (if the machine is owned, nothing helps)
- Key distribution (you trust the keys you import — no PKI, no CA)
Optional .enseal.toml in your project root:
[defaults]
relay = "wss://relay.enseal.dev" # public relay (identity mode)
# relay = "ws://relay.internal:4443" # self-hosted without TLS
# relay = "wss://relay.internal:4443" # self-hosted with TLS reverse proxy
[filter]
exclude = ["^PUBLIC_", "^NEXT_PUBLIC_", "^REACT_APP_"]
[identity]
default_recipient = "devops-team"
[schema]
required = ["DATABASE_URL", "API_KEY", "JWT_SECRET"]CORE
enseal share [<file>] Send secrets (file, pipe, or --secret)
enseal receive [<code|file>] Receive secrets
enseal inject [<code>] -- <cmd> Inject secrets into a process
enseal keys <subcommand> Manage identity keys and aliases
enseal serve Run self-hosted relay server
.ENV TOOLKIT
enseal check [file] Verify .env has all vars from .env.example
enseal diff <file1> <file2> Compare .env files (keys only)
enseal redact <file> Replace values with <REDACTED>
enseal validate <file> Validate against schema rules
enseal template <file> Generate .env.example with type hints
ENCRYPTION
enseal encrypt <file> Encrypt .env for git storage
enseal decrypt <file> Decrypt an encrypted .env
--to <name> Identity mode: encrypt to recipient (alias, group, or identity)
--output <dir> File drop: write encrypted file (identity mode, no network)
--upload Post to burnurl.dev (async, browser-readable, no CLI on recipient side)
--ttl <hours> Secret TTL for --upload (1-24, default: 24)
--passphrase Encrypt client-side before --upload (prompts; server never sees plaintext)
--secret <value> Inline secret (raw string or KEY=VALUE)
--label <name> Human label for raw/piped secrets
--as <KEY> Wrap raw input as KEY=<value>
--relay <url> Route through relay server. Anonymous mode: uses enseal relay transport
(generates channel code, bypasses wormhole). Identity mode: push to
recipient's channel. Also: ENSEAL_RELAY env var.
--env <profile> Environment profile (resolves to .env.<profile>)
--exclude <pattern> Regex to exclude vars
--include <pattern> Regex to include only matching vars
--no-filter Send raw file, skip .env parsing
--no-interpolate Don't resolve ${VAR} references before sending
--words <n> Words in wormhole code (2-5, default: 2). Wormhole mode only (no --relay).
--quiet / -q Minimal output
--output <path> Write to specific file
--clipboard Copy to clipboard instead of stdout/file
--no-write Print to stdout even for .env payloads
--relay <url> Use specific relay server
--quiet / -q Minimal output
--listen Listen for incoming identity-mode transfer (requires --relay)
--relay <url> Use specific relay server (also: ENSEAL_RELAY)
--quiet / -q Minimal output
enseal keys init Generate your keypair
enseal keys export Print your public key bundle
enseal keys import <file> Import a colleague's public key
enseal keys list Show all trusted keys and aliases
enseal keys remove <identity> Remove a trusted key
enseal keys fingerprint Show your key fingerprint
enseal keys alias <name> <identity> Map short name to identity
enseal keys group create <name> Create a named group
enseal keys group add <group> <id> Add identity to group
enseal keys group remove <group> <id> Remove identity from group
enseal keys group list [name] List groups or group members
enseal keys group delete <name> Delete a group
--port <port> Listen port (default: 4443)
--bind <addr> Bind address (default: 0.0.0.0)
--max-mailboxes <n> Max concurrent channels (default: 100)
--channel-ttl <seconds> Idle channel lifetime (default: 300)
--max-payload <bytes> Max WebSocket message size (default: 1048576)
--rate-limit <n> Max connections per minute per IP (default: 10)
--health Print server health check and exit
--per-var Per-variable encryption (keys visible, values encrypted)
--to <name> Encrypt to specific recipients (multi-key)
--verbose / -v Debug output (never prints secret values)
--quiet / -q Minimal output (for scripting)
| enseal | Slack DM | 1Password Share | dotenvx | croc | |
|---|---|---|---|---|---|
| Zero setup | Yes | Yes | No | No | Yes |
| End-to-end encrypted | Yes | No | Yes | N/A | Yes |
| Ephemeral (no history) | Yes | No | Yes | N/A | Yes |
| .env aware | Yes | No | No | Yes | No |
| Process injection | Yes | No | No | Yes | No |
| Schema validation | Yes | No | No | No | No |
| At-rest encryption | Yes | N/A | N/A | Yes | No |
| Self-hostable relay | Yes | No | No | N/A | Yes |
| Raw string/pipe support | Yes | Yes | No | No | Yes |
- v0.1 — Core: share/receive, pipe/stdin, .env toolkit (check, diff, redact) (done)
- v0.2 — Identity mode: keys, aliases,
--toflag (done) - v0.3 — Inject command, self-hosted relay (done)
- v0.4 — Schema validation, templates, interpolation, profiles (done)
- v0.5 — At-rest encryption (encrypt/decrypt) (done)
- v0.10 — Groups, Helm chart, docs (done)
- v0.11 — Security hardening, docs sync (done)
- v0.12 — .enseal.toml wired up, private relay for anonymous mode (done)
- v0.16 — Async upload via burnurl.dev (
--upload) (current) - v1.0 — crates.io publish, final polish
MIT