/
Sys 02 · DossierActive

niche

CLI — encrypted environments, per repository

The problem

Managing the environment when working on software is hard. It requires tracking both the variables needed by the system and the values those variables must contain for your desired configuration. All this and you want it to be safe, easy, simple, boring.

Repo
gitlab.com/whytheplatypus/niche
Status
Active
Stack
Rust · X25519 · ChaCha20-Poly1305 · mDNS
License
MIT

Notes

I made niche to solve the problem for myself, and I hope it helps others as well. The .env file has been the bane of many onboarding efforts in my career as a tech lead. Figuring out which variables need to be sent over to a new dev, or onto my own machine, to get things running. Keeping me up nights worrying that a credential now lives forever in version control.

Of course, I still use it, all the time, it’s easy. A plain file next to the code, apparently containing a clear list of the configuration needed by the application. But easy and simple are different things. Easy is about effort — how close at hand something is. Simple is about entanglement — whether a thing is one strand or several woven together. A .env file is easy, and it is entangled. Managing it means considering your codebase (one git add . from living there forever), your chat history (how it got to your laptop), and your application code (the extra code that loads it into the environment).

niche is an attempt at simple environment management: easy enough that I’ll actually reach for it, and simple enough that I can describe it to others and reason about it myself.

Using it

Installing

cargo install --git https://gitlab.com/whytheplatypus/niche.git niche, and if you haven’t already, add the cargo install directory (usually $HOME/.cargo/bin) to your path. There’s no account to make and no daemon to run — the first niche command of any kind, even niche --help, quietly generates this machine’s X25519 keypair and writes it to ~/.config/niche/identity. niche whoami prints the public half and a fingerprint.

Each machine is its own identity, easy and simple.

Linking an environment to a repository

From within a repository, run

niche link

This establishes the environment’s identity as the repository’s root commit SHA and nothing else which means cloning the same repo again on the same machine finds the values already there: nothing to transfer or configure.

Adding and removing variables

niche add DATABASE_URL "postgresql://localhost:5432/mydb"
niche list                 # names only
niche list --show-values   # decrypted
niche remove DATABASE_URL

One blob holding every variable would be a small braid of its own: touch one value and you’ve rewritten them all. So each variable is its own encrypted file. This makes remove an unlink, and add never touches a value it wasn’t asked to. The environment’s data key sits beside them in .key, and it’s local, each machine has its own, and a transfer re-encrypts for the destination rather than copying it.

list shows names only unless you ask for values. This provides a quick way to see what variables are recorded for the system.

Getting values into something that runs

niche run -- npm start

Nothing touches the filesystem: the environment variables are set only for the child process.

A .env

For a codebase already set up to need a .env file, niche generate writes a file to /dev/shm and symlinks .env in the project to it. Now a file sits where the code expects one, readable by anything that opens it, but the entanglement doesn’t. The bytes are on tmpfs, never on persistent disk, they’re gone at reboot. The generated file carries a header marking it as niche’s, which lets generate refuse to overwrite a .env it didn’t write. --persist writes a real file and warns on stderr.

Getting values into your shell

eval "$(niche apply)"
eval "$(niche clear)"

A child process can’t reach into its parent shell’s environment, so apply prints shell statements and your shell runs them itself with eval.

apply also exports NICHE_APPLIED listing the names it set, so clear undoes exactly what was applied, including variables you’ve since deleted from the vault.

Named environments

niche add --env staging DATABASE_URL "postgresql://staging.internal/app"
niche envs
export NICHE_ENV=staging

A named environment uses what it sets and inherits everything else from the default. list tells you which layer each value came from, making any complexity obvious.

Sharing between machines

On the machine that has the values:

$ niche export
  Code: 482-916
Verification code: BRAVE-OTTER-4217

On the other, in a clone that has already run niche link:

$ niche import 482-916
ℹ Found the relay for this code at http://192.168.1.43:41207
Verification code: BRAVE-OTTER-4217

Neither machine has to be told where the relay is. export looks for one announced on the network and otherwise starts its own, on a port the OS picks, announced over mDNS under the pairing code — so two exports on one network can’t send the wrong machine to the wrong relay. It withdraws the announcement on the way out.

Combine --relay and a long-lived niche relay where multicast doesn’t carry.

A transfer carries one layer, not the merged cascade: niche export --env staging sends only what that layer sets, and the receiving machine’s own default still shows through underneath.

In practice

My hope is that this maintains the ease that made me reach for a .env file in the first place. A single binary, no account, no daemon, and the first command you run makes the identity it needs and gets on with it. Easy isn’t a bad thing. Entanglement is, and making something simple and easy is decidedly hard. Hopefully I’ve managed that for this problem, but we’ll see what happens as it faces reality.

If you happen to use it I’d love to hear what works and what doesn’t.