mkenv documentation

mkenv runs your command with the right environment: one typed JSONC document declares your variables, and secrets decrypt on your machine — never on the server.

Early access — commands and formats below are implemented; hosted sign-in is rolling out.

#Install

The CLI is built on Bun and published to npm under the @mkenv scope.

$ bun add -g @mkenv/cli
# package publishing is part of the launch rollout — until then, run from source (github.com/mkenv)

#Quickstart

Create mkenv.jsonc in your project root, then put mkenv in front of any command.

// mkenv.jsonc
{
  "variables": {
    "PORT": 3000,
    "APP_NAME": "demo",
  },
}
$ mkenv bun start
$ mkenv terraform plan
$ mkenv ./scripts/deploy.sh

mkenv resolves the document, injects the variables into the child process environment, runs your command, and exits with the command's exit code. If resolution fails for any reason, the command never runs.

#The env document

mkenv.jsonc is JSONC — JSON with comments and trailing commas — validated against a published JSON Schema before anything runs. mkenv finds it in the nearest ancestor directory, so it works from anywhere inside your project.

fieldmeaning
variablesRequired. The environment variables to inject. Values may be any JSON value; non-strings serialize to canonical JSON text ({"beta":true}) at injection time.
extendsOrdered parent document paths, relative to this document. See layering.
tenantThe team (tenant) id that owns any referenced secrets. Read from the entry document only.
$schemaOptional editor hint: https://mkenv.net/schemas/environment-document.v1.json

#Layering with extends

Documents compose by ordered layering: parents apply first in declared order, the declaring document last. Each layer is an RFC 7396 merge patch — objects merge key by key, arrays replace wholesale, and null deletes a variable inherited from any earlier layer.

// production.jsonc — a patch on base, not a copy of it
{
  "extends": ["./base.jsonc"],
  "variables": {
    "LOG_LEVEL": "warn",     // overrides base
    "DEBUG_PORT": null,       // deletes base's DEBUG_PORT
  },
}

Cycles in the extends chain are rejected with the cycle named.

#Secret references

A variable whose value is exactly {"$secret": "NAME"} is a secret reference. At run time, mkenv fetches the encrypted value, decrypts it locally, and injects the plaintext — which exists only in the process's memory.

{
  "tenant": "acme",
  "variables": {
    "DATABASE_URL": { "$secret": "DATABASE_URL" },
  },
}

Because a reference is plain document data, a later layer can replace it with a literal value (say, in local development) — or replace a literal with a reference in production. A document with references fails closed on a machine that isn't logged in: the reference never injects as literal JSON.

#mkenv login

mkenv login uses the device-authorization flow: the CLI prints a short code and a verification URL, you approve in a signed-in browser, and the CLI stores an access token in ~/.config/mkenv/.

On first login, the CLI also generates this machine's key pair. The private key never leaves your machine — it is what lets this machine (and only this machine) unwrap your team's data key.

#Managing secrets

# once per tenant: generate the data key locally, store only a wrapped copy
$ mkenv secrets init

# the value arrives on stdin — it never lands in shell history
$ mkenv secrets set DATABASE_URL
  postgres://user:password@host/db
$ # encrypted on this machine; the server stores ciphertext

Secret names match [A-Za-z_][A-Za-z0-9_.-]*, up to 128 characters.

#Zero knowledge

Every secret value is encrypted with AES-256-GCM under your team's data key before transmission. The data key itself is stored only wrapped — once per member, using ephemeral-static ECDH (P-256) against that member's public key. The backend holds ciphertext envelopes and wrapped keys, and no key material that can open either.

The server cannot read your secrets. The API rejects anything that is not a well-formed ciphertext envelope — plaintext is refused at the door, by schema.

The crypto and document code is open source (Apache-2.0), so the code that touches your plaintext is the code you can read.

#CLI reference

commandbehavior
mkenv <command> [args...]Resolve the document, inject variables, run the command, exit with its exit code. Resolved variables win over inherited ones.
mkenv loginDevice-flow sign-in; stores the access token and generates the machine key pair on first run.
mkenv secrets initGenerate the tenant data key locally and store only the wrapped envelope server-side.
mkenv secrets set <name>Encrypt the stdin value under the tenant data key and store the ciphertext envelope.
exit codemeaning
child's codeThe command ran; mkenv is transparent (signals map to 128+N).
1Resolution failed — missing document, schema violation, unreachable secret. The command never ran.
2Usage error (no command given).
127The command could not be spawned.

#Environment variables

variablemeaning
MKENV_URLBackend base URL. Defaults to https://api.mkenv.net; point it at a self-hosted backend and the CLI needs no other change.
XDG_CONFIG_HOMEOverrides where mkenv/ config (token, machine key) lives. Default ~/.config.