Skip to main content
Paper-crafted illustration of two terminal windows connected by an envelope

envshare

About

An accountless CLI for handing off dotenv files and selected environment variables once, without storing the payload on a server.

Status
Maintained
Built with
RustLibp2pTokioXChaCha20 Poly1305CBOR

envshare is a CLI for handing a .env file or a selected set of environment variables to one receiver. There are no accounts and no hosted payload. The sender stays online until the transfer completes, expires, or is cancelled.

The receiver enters a short-lived share code. envshare uses that code to find and authenticate the sender, then transfers the encrypted payload over libp2p.

Sharing environment variables is usually more permanent than the task requires. A file sent through chat remains in the conversation. Copying values one by one is slow and easy to get wrong. A full secrets platform can be unnecessary when another developer only needs a local .env file once.

I wanted the exchange to behave like a handoff instead of an upload. Both people are present, one receiver can claim the share, and the sender knows when the payload has been accepted.

The sender can choose a dotenv file from the current directory or pass a path directly:

envshare send
envshare sender showing the selected dotenv file, share code, and waiting state

The sender remains online with the share code visible. On the receiving machine, I ran:

envshare receive
envshare receiver entering the share code through a hidden terminal prompt

The code is entered through a hidden prompt. A new payload is written to .env by default. If that file already exists, the receiver can merge values, add only missing keys, choose another path, replace the file, or cancel before claiming the share.

envshare receiver showing the available actions for an existing dotenv file

For this transfer, I chose to create a new file and saved the received values as .env.shared.

envshare receiver entering .env.shared as the destination for the received file

After the receiver writes the file, it sends an acknowledgement. The sender then reports that the environment was received and closes the one-time share.

envshare sender reporting that the environment was received and the share completed

The sender can also limit the payload to specific keys:

envshare send .env --keys DATABASE_URL,API_TOKEN

This keeps unrelated values out of the transfer and fails when a requested key is missing unless that behavior is explicitly allowed.

The run command receives the same encrypted payload and starts one child process with those values in its environment:

envshare run -- npm run dev

No dotenv file is created. Existing environment variables win by default, while --override, --strict, and --clean-env make the precedence explicit for other workflows.

envshare starts the program directly instead of passing it through a shell. It forwards interrupts, preserves the child exit status, and acknowledges the transfer after the child starts.

The public node only helps the two peers find and reach each other. Capability authentication, session-key derivation, encryption, persistence, and acknowledgement happen between the sender and receiver.

The public node handles discovery and relay traffic while the clients own authentication and encryption.

A version 1 share code contains a 20-byte random capability and a 10-bit checksum. The checksum catches typing mistakes; it does not add entropy or stretch the secret.

The capability never becomes a lookup key by itself. HKDF derives three separate roots from it and the selected network_id:

  • room_id creates the opaque Rendezvous namespace used for discovery.
  • auth_key proves that both peers possess the capability.
  • session_base_key becomes the starting point for per-transfer encryption and acknowledgement keys.

Including network_id in the derivation keeps identical capability bytes isolated between networks. A receiver using the wrong profile derives a different room and cannot discover or authenticate the sender.

After discovery, the receiver sends an Open message with a fresh nonce and an HMAC proof. The proof binds the network, room, sender Peer ID, receiver Peer ID, and nonce. The sender verifies it before disclosing any payload and atomically binds the first valid receiver.

The sender then adds its own nonce and claim ID. Both sides derive separate payload and acknowledgement keys for that claim. The envelope is encrypted with XChaCha20-Poly1305, while the associated data and proof bind the peers, expiry, claim, metadata, nonce, and ciphertext digest.

The receiver acknowledges only after the payload has been written atomically or the requested child process has started. That acknowledgement is authenticated with the separate acknowledgement key and tied to the same claim and ciphertext.

After disclosure, envshare either confirms the same claim or reports uncertainty. It never offers the payload to a second receiver.

Available accepts one authenticated receiver. Once the encrypted offer is disclosed, only that same Peer ID and receiver nonce can resume it during a bounded window. A valid acknowledgement moves the share to Consumed. If acknowledgement never arrives, the sender reports DeliveryUnknown instead of risking a second delivery.

The built-in public profile uses node.envshare.xyz for Rendezvous discovery and Circuit Relay. It is relay-only, so a normal transfer does not advertise local host addresses. Both clients can check the complete path before sharing anything:

envshare doctor --verbose

A network profile defines the public network identifier and the discovery and relay multiaddresses. For example, a team can point both clients at its own node:

network_id = "team-v1"
require_relay = true
relay_only = true
rendezvous = [
  "/dns4/node.example.com/udp/4001/quic-v1/p2p/PEER_ID",
]
relays = [
  "/dns4/node.example.com/tcp/4001/p2p/PEER_ID",
]

The profile can then be added, selected, and checked from the CLI:

envshare network add team --file team.toml
envshare network use team
envshare doctor --network team --verbose

Both peers must resolve their profile to the same network_id because that value is included in room, authentication-key, and session-key derivation. The local profile names can differ.

A profile can also be selected for one command without changing the default:

envshare send .env --network team
envshare receive --network team

For explicit connections, the sender can expose a Peer ID and multiaddress with --verbose, and the receiver can connect with --peer and --address. Local mDNS discovery is available with --mdns; receiving private or link-local addresses additionally requires --lan.

envshare-node provides Rendezvous discovery and Circuit Relay v2. It keeps a stable Ed25519 identity on disk, but it does not accept share codes or store transfer payloads.

A deployment needs a Linux host with a stable public address, TCP and UDP port 4001, a DNS record, and a persistent private directory for the node identity. The basic setup is:

cargo build --release --locked --package envshare-node
envshare-node key generate \
  --output /var/lib/envshare-node/identity.key
envshare-node config check \
  --config /etc/envshare-node/node.toml
envshare-node serve \
  --identity /var/lib/envshare-node/identity.key \
  --config /etc/envshare-node/node.toml

The generated Peer ID becomes part of every client multiaddress. The identity must survive upgrades because replacing it changes the Peer ID and invalidates existing profiles.

The node configuration places hard bounds on reservations, circuits, connections, discovery records, request rates, memory, and transfer duration. Its operations server exposes /health/live, /health/ready, and /metrics; that server is intended to remain on loopback and be reached through controlled monitoring infrastructure.

After the node is running, its TCP and QUIC multiaddresses can be added to the client profile shown above. The repository also includes maintained systemd and Docker deployment files for running it as a service.

Received files are written through a private temporary file and renamed atomically. On Unix, new files use 0600 permissions. envshare refuses symlinks, directories, and special files as destinations.

For existing dotenv files, merge modes preserve unrelated declarations and comments. Automation can choose the destination mode up front and pass the share code over standard input so it does not appear in a command argument.

envshare protects the payload while it moves between the two peers. It also keeps codes, values, peer addresses, and discovery namespaces out of normal logs and structured CLI events.

It does not make the transfer anonymous, protect a compromised endpoint, or revoke a value after the receiver has written it or started a process. Network infrastructure can still observe metadata such as IP addresses, timing, transport choice, and approximate payload size. Anyone with the complete share code can attempt to claim the payload, so the code still needs to be sent through a private channel.

envshare supports interactive transfers, selected-key sharing, file merge modes, process injection, a default public network, self-hosted profiles, direct connections, and LAN discovery. Release binaries are available for macOS, Linux, and Windows.

The CLI, node, protocol specification, and security documentation are open source under the Apache 2.0 license in the envshare repository (opens in a new tab).

Homebrew works on both macOS and Linux. The install script is available as an alternative, with a separate PowerShell installer for Windows.

brew install envoy1084/tap/envshare

Verify the installation and check discovery and relay connectivity:

envshare --version
envshare doctor

Then run envshare send in a directory containing a dotenv file and envshare receive on the other machine.