Skip to main content

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.

CI Python 3.10+ License: MIT


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 .env files

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:

  • inspect shows
  • check validates
  • run injects
  • sync writes
  • export prints

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
  • $VAR stays literal
  • if VAR is a declared envctl key, envctl resolves that key first
  • otherwise envctl falls back to the current process environment
  • ${HOME} works when HOME exists 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 sync and export --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:

  1. --profile
  2. ENVCTL_PROFILE
  3. config default
  4. 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:

  • json
  • url
  • csv

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.local is 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

envctl-2.3.4.tar.gz (68.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

envctl-2.3.4-py3-none-any.whl (107.2 kB view details)

Uploaded Python 3

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

Hashes for envctl-2.3.4.tar.gz
Algorithm Hash digest
SHA256 b542e21f4560b64e4997b8bf52d345f8b53ea34d34f5f0f629bf2a1452c53ce4
MD5 fe9fd378e62d8b368867209e8b3e4db5
BLAKE2b-256 dbebd02621c62dfe04bb4ca0529d767962fa8149a72d3578d933fba41fad89c9

See more details on using hashes here.

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

Hashes for envctl-2.3.4-py3-none-any.whl
Algorithm Hash digest
SHA256 0c2a7245cbca1e8847f4d210c2e66267f18e06704323a6e3833182148b5078f1
MD5 ec5384724984c55572fbd7ac6b1ec83e
BLAKE2b-256 d9815d67f5b8f024fd521f30e8d236943e3a537ad554ff916584389e315f9b91

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page