Local environment control plane for contract-driven development workflows
Project description
envctl
Your .env.local files are undocumented, unvalidated, and drift between machines. envctl fixes that.
What is this?
Most projects handle .env files like this:
- variables are not documented
- values get copied between machines
- something works locally… but breaks somewhere else
envctl gives you a simple structure to fix that.
It separates three things that usually get mixed together:
- what the project needs → defined in
.envctl.schema.yaml(committed to the repo) - what you have locally → stored in a private vault (never in git)
- what actually runs → a validated environment, built on demand
So you get:
- no secrets in git
- no undocumented variables
- no copy-pasting
.envfiles
Install
pip install envctl
Or from source:
git clone https://github.com/labrynx/envctl
cd envctl
pip install -e .
Quickstart
envctl config init # create your local config
envctl init # initialize this repository
envctl fill # set missing values (interactive)
envctl check # validate against the contract
envctl run -- python app.py # run with env injected
If you use envctl run -- docker run ..., envctl injects into the Docker client process, not directly into the container. Forward container variables explicitly with -e, --env, or --env-file.
Why not just .env.local?
Because it doesn’t scale well.
.env.local |
direnv | Doppler / Infisical | envctl | |
|---|---|---|---|---|
| Documents what variables exist | ❌ | ❌ | Partial | ✅ contract |
| Type validation | ❌ | ❌ | ❌ | ✅ |
| Values stay off git | ⚠️ easy to slip | ✅ | ✅ cloud | ✅ local vault |
| Multiple environments | manual files | manual files | ✅ | ✅ profiles |
| No cloud account required | ✅ | ✅ | ❌ | ✅ |
| Works in CI without mutation | ❌ | ❌ | ❌ | ✅ ENVCTL_RUNTIME_MODE=ci |
envctl is not a secrets manager.
It’s a local control plane for your project’s environment:
the contract says what’s needed, your machine provides values, and envctl makes them work together.
How it works
There are five pieces, but the idea is simple:
- contract → defines what variables exist and their rules
- vault → stores your real values locally
- profile → selects a set of values (
local,dev,staging, …) - resolution → combines everything in a deterministic way
- projection → makes it usable (
run,sync,export)
Think of it like this:
the repo defines the rules, your machine provides the data, and envctl builds the final environment.
Resolution now includes placeholder expansion as part of the runtime model, so check, inspect,
run, sync, and export all see the same final value.
Contracts can also attach an optional human-facing group label to variables for organization,
filtering, and dotenv section rendering. group is not a namespace, is not hierarchical, and does
not change resolution or dependency semantics.
Example contract
# .envctl.schema.yaml — commit this
version: 1
variables:
DATABASE_URL:
type: url
required: true
sensitive: true
description: Primary database connection URL
PORT:
type: int
required: true
default: 3000
sensitive: false
DEBUG:
type: bool
required: false
default: false
sensitive: false
TEST_JSON:
type: string
format: json
required: false
sensitive: false
APP_URL:
type: string
required: true
sensitive: false
group: Application
default: http://${APP_NAME}:${PORT}
This file describes what exists. It never contains real values.
Variable expansion
envctl supports explicit placeholder expansion with ${VAR} during resolution.
That means the expansion happens before projection, so the effective expanded value is what:
inspectshowscheckvalidatesruninjectssyncwritesexportprints
Example:
INFRA_NEO4J_USER=neo4j
INFRA_NEO4J_PASSWORD=super-secret
INFRA_NEO4J_AUTH=${INFRA_NEO4J_USER}/${INFRA_NEO4J_PASSWORD}
INFRA_NEO4J_AUTH resolves to the final runtime value, not the literal expression.
Rules:
- only
${VAR}is supported in v1 $VARstays literal- if
VARis a declared envctl key, envctl resolves that key first - otherwise envctl falls back to the current process environment
${HOME}works whenHOMEexists in the current process environment- malformed placeholders or unresolved references make resolution invalid
Compatibility notes:
- before this feature,
${HOME}stayed literal - now
${HOME}is expanded during resolution ${...}literal escaping is not supported in v1
Optional groups
Each contract variable may define an optional group label:
variables:
DATABASE_URL:
type: url
required: true
sensitive: true
group: Database
group is used only for:
- organization in the contract
- CLI targeting with
--group - grouped dotenv output from
syncandexport --format dotenv
It does not:
- create namespaces
- affect
${VAR}expansion rules - restrict cross-variable references
- imply hierarchy, inheritance, or prefix matching
Profiles
Instead of juggling multiple .env files:
# set up dev once
envctl --profile dev fill
# validate staging
envctl --profile staging check
# run with staging values
envctl --profile staging run -- python app.py
Profile selection priority:
--profileENVCTL_PROFILE- config default
local
Each profile is independent. No hidden inheritance.
Team workflow
The idea is simple:
- the contract is shared
- the values are local
# developer A
envctl add API_KEY sk-abc123
git add .envctl.schema.yaml
git commit -m "require API_KEY"
# developer B
git pull
envctl check # shows what's missing
envctl fill # only asks for missing values
No more guessing what goes into .env.
CI workflow
ENVCTL_RUNTIME_MODE=ci envctl check
In CI mode:
- validation works
- mutations are blocked (
add,set,fill, etc.)
You can also combine it with profiles:
ENVCTL_PROFILE=ci ENVCTL_RUNTIME_MODE=ci envctl check
Common commands
# validation and visibility
envctl check
envctl inspect
envctl explain DATABASE_URL
envctl status
envctl doctor
# values
envctl add DATABASE_URL <value>
envctl add TEST_JSON '{"key":"value"}' --type string --format json
envctl set PORT 4000
envctl unset PORT
envctl remove PORT
# run / output
envctl run -- <command>
envctl sync
envctl export
# profiles
envctl profile list
envctl profile create staging
envctl profile copy local staging
envctl profile remove staging --yes
# vault
envctl vault show
envctl vault check
envctl vault path
envctl vault prune
# project identity
envctl project bind <id>
envctl project rebind
envctl project repair
Machine-readable output
All read commands support --json:
envctl --json check
envctl --json status
envctl --json inspect
envctl --json doctor
Structured string validation
If a variable is a string but carries structured content, declare that semantic format in the contract:
variables:
TEST_JSON:
type: string
format: json
Supported format values for type: string:
jsonurlcsv
When format is declared, check, inspect, and runtime resolution validate payload semantics, not only raw string presence.
Design principles
- Contract-first: the repo defines requirements
- Deterministic: same inputs → same result
- Explicit: nothing happens automatically
- Local-first: no required cloud
- Generated files are disposable
- Profiles are value namespaces, not variants
- CI mode is policy, not a profile
Security model
- The contract contains no secrets
- Secrets stay on your machine
.env.localis optional and disposable- Sensitive values are masked in output
- Read-only commands never change state
- Vault files use restrictive permissions (
0600)
Important:
envctl assumes a trusted machine. If your machine is compromised, your secrets are compromised.
It’s not a replacement for a team-wide secrets manager.
Documentation
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file envctl-2.3.4.tar.gz.
File metadata
- Download URL: envctl-2.3.4.tar.gz
- Upload date:
- Size: 68.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b542e21f4560b64e4997b8bf52d345f8b53ea34d34f5f0f629bf2a1452c53ce4
|
|
| MD5 |
fe9fd378e62d8b368867209e8b3e4db5
|
|
| BLAKE2b-256 |
dbebd02621c62dfe04bb4ca0529d767962fa8149a72d3578d933fba41fad89c9
|
File details
Details for the file envctl-2.3.4-py3-none-any.whl.
File metadata
- Download URL: envctl-2.3.4-py3-none-any.whl
- Upload date:
- Size: 107.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c2a7245cbca1e8847f4d210c2e66267f18e06704323a6e3833182148b5078f1
|
|
| MD5 |
ec5384724984c55572fbd7ac6b1ec83e
|
|
| BLAKE2b-256 |
d9815d67f5b8f024fd521f30e8d236943e3a537ad554ff916584389e315f9b91
|