Every service and every deploy gets the configuration it needs, and you find out before something breaks.
EnvShield checks your code, your local .env files and your Docker Compose and Kubernetes manifests against one committed contract, env.schema.toml. When code starts reading a variable nobody declared, when a service or a deployment is missing one, or when the contract itself changes, EnvShield tells you: on your machine, in a Git hook, or in CI.
Free, open source (MIT), and fully local. EnvShield never sends your configuration anywhere and never prints a secret value.
Website · Documentation · GitHub · PyPI
A coding agent adds a webhook that reads STRIPE_WEBHOOK_SECRET. envshield undeclared names the file and line. Once the variable is declared for the api service, envshield check shows that api's .env and its Compose container both lack it, while worker is unaffected.
While you develop
- A variable the code reads but nobody declared.
envshield undeclaredcompares your working tree (or a pull request) with a Git revision and names each new read by file and line. Python discovery is AST-based; JavaScript/TypeScript is pattern-based. - A local setup that no longer matches.
envshield checkvalidates your.envagainst the contract: missing, blank, wrongly typed or extra variables, and code that reads something the contract doesn't declare. - A contract change in review.
envshield schema diffclassifies each change between two revisions as breaking, security-relevant or informational, so a configuration change is reviewed like any other code change. - What a pull just changed. After a pull that touches a service's configuration, the post-merge hook runs
doctorfor it, which names any variable your local file is now missing.
Before you deploy
- A deployment that doesn't provide what its service needs.
envshield checkvalidates a Docker Compose file or Kubernetes manifest against the same contract. Register the manifest with acontainers:map and every container is checked against its own service. - Values EnvShield can't see (a Kubernetes Secret reference, a Compose
${VAR}) are reported as unverified, never as valid. In v4 they don't fail the check yet; see Known limitations.
When someone joins
envshield initbuilds the contract from the configuration you already have, andenvshield setupwalks a new developer through their local.env, with each variable's description and masked input for secrets. Filling values from your team's secret manager is planned for v5.
Supporting tools: explain (everything EnvShield knows about one variable), generate (typed pydantic-settings or zod config from the contract), scan (hardcoded secrets before they're committed) and doctor (a health check of the whole setup).
Coding agents write code that reads configuration they don't know exists. EnvShield gives them a deterministic check to run in their loop: envshield undeclared --json and envshield check --json return machine-readable, value-free findings that name the variable, file and line, and the Git hooks catch what an agent commits. No secret value appears in EnvShield's output, so it's safe to put in front of an agent.
pipx install envshieldPrefer an existing Python/pip workflow instead? pip install envshield works the same way, as an alternative.
In an existing project:
envshield initinit looks for a real config source — a .env, .env.example, or a recognizable Python config module — and builds env.schema.toml from your actual variables (classifying secrets, suggesting defaults, inferring types where the shape is unambiguous). A brand-new project with nothing to read yet gets a framework-aware starting template instead.
envshield setup # interactive wizard: fills in your local .env from the schema
envshield check # validate your local file against the schemaThat's the whole loop for a single-service project. Everything past this point — multiple services, schema composition, CI, deployment manifests — is opt-in.
env.schema.toml is plain TOML, meant to be committed to git and hand-edited. It declares shape, never secret values:
[DATABASE_URL]
description = "PostgreSQL connection string"
type = "url"
secret = true
[APP_ENV]
description = "Deployment environment"
enum = ["local", "staging", "production"]
defaultValue = "local"
[PORT]
description = "Port the API listens on"
type = "port"
defaultValue = "8000"
[DEBUG]
description = "Enable debug mode"
type = "bool"
defaultValue = "false"
[ADMIN_EMAIL]
description = "Where alerts get sent"
type = "email"
[STRIPE_SECRET_KEY]
description = "Stripe secret key"
secret = true
requiredIf = { var = "PAYMENTS_ENABLED", equals = "true" }What each field actually means:
| Field | Meaning |
|---|---|
type |
string (default), int, float, bool, port (1–65535), url, email. Enforced by check/doctor/setup. |
enum |
Value must be one of this list. Implies an enum type regardless of type. |
pattern |
A regex the value must also match, e.g. pattern = "^v\\d+\\.\\d+\\.\\d+$". |
secret |
Marks the variable sensitive — masked input in setup, masked in generated code. import auto-suggests it from the variable's name/value; it never overwrites a secret value you've already committed. |
defaultValue |
What setup writes automatically (and .env.example and generated code show). A default never makes a variable optional: it still has to be explicitly present in your local file — check/doctor name the default inline so a missing one is obvious. |
required |
Optional. required = false makes a variable optional: it may be absent or blank, but a value that is set must still be valid. required = true is the default. |
requiredIn |
Optional. A list of environments where the variable is required, e.g. ["staging", "production"]; it's optional everywhere else (see Environments, below). |
description |
Shown in setup, copied into generated code. |
Requiredness: every variable is required unless required = false, requiredIn limits it to some environments, or requiredIf makes it conditional (see below). A variable sets at most one of the three. A defaultValue never changes whether a variable is required. Any other key, or an unknown type, is an error — a typo like default = "8000" is reported (did you mean 'defaultValue'?) instead of silently ignored.
A variable that's only relevant behind a feature flag doesn't need to be required unconditionally. requiredIf = { var = "OTHER_VAR", equals = "some value" } makes a field required only when that condition holds, with or without a defaultValue. Without requiredIf, a variable stays required unconditionally:
[PAYMENTS_ENABLED]
type = "bool"
defaultValue = "false"
[STRIPE_SECRET_KEY]
secret = true
requiredIf = { var = "PAYMENTS_ENABLED", equals = "true" }With PAYMENTS_ENABLED=false, STRIPE_SECRET_KEY is optional. Flip the flag in any environment, and check/doctor/setup immediately start requiring it there.
A monorepo with several services usually shares a handful of variables. Factor them into a base schema and extend it — the child's own definition always wins on a conflict, no per-field merging:
# services/api/env.schema.toml
extends = "../../shared/base.schema.toml"
[DATABASE_URL]
secret = trueenvshield --help groups commands by stage: Onboarding (init, setup), Development & deployment (check, undeclared, hook) and Supporting (everything else). This section walks through them in the order you'd use them: define the contract, validate against it, discover what code depends on, and review how the contract changes.
env.schema.toml (see The Schema above) is what you define. envshield init/envshield import <file> build it from your real, existing configuration — see Existing projects.
envshield check — validate local configuration, and commonly-used Docker Compose and Kubernetes deployment-manifest patterns, against the same schema (see Known limitations). It also scans the service's own source code and fails on a variable the code reads but the schema doesn't declare (the same files the pre-commit scan checks). --process-env also counts variables set in your shell (for a name the schema declares, the shell's value wins over the file's). --json adds a versioned, value-free report per service.
$ envshield check
Validating .env against schema...
Invalid
✗ DATABASE_URL missing (required)
✗ PORT invalid value (must be a port number from 1-65535)
✓ LOG_LEVEL valid
INVALID: 2 invalid, 1 valid.Every variable gets one verdict: valid, invalid, could not verify, not required, or error. check never calls a value valid unless it read it. A manifest often only points at a value (a Kubernetes Secret, a Compose ${VAR} from the deploy host), so those variables are "could not verify", and check exits 1. When that's expected, accept the specific reason: envshield check k8s/prod.yaml --accept-unverified=external_reference exits 0, still lists every unverified variable, and ends with PASSED WITH UNVERIFIED VALUES. The reasons you can accept are host_forward, external_reference and value_not_inspected. In --json, success stays false for an accepted run.
Environments. A variable can be optional on a laptop and required in production. Declare the environments in envshield.yml and say where each variable is required:
# envshield.yml
environments: [staging, production] # `local` is built in
manifests:
- file: k8s/production.yaml
environment: production
containers: {api: api}# env.schema.toml
[GIT_SHA]
description = "Set by the deploy script; never in .env."
requiredIn = ["staging", "production"]Plain envshield check judges your local file (and untagged manifests) by local's requirements, so GIT_SHA isn't needed there. envshield explain GIT_SHA lists where it's required, schema diff says which environment a requiredness change affects (a variable added with requiredIn = ["production"] is breaking for production only), and generate makes it optional in the generated loader, since that loader runs in every environment. envshield check --env production (or ENVSHIELD_ENV=production) checks the manifests tagged production against production's requirements. envshield check deploy/production.env --env production checks one env file the same way. A variable sets at most one of required, requiredIn or requiredIf.
envshield setup — interactive onboarding: fills in whatever's missing, blank, or now-invalid in your local file, prompting with each variable's description.
envshield doctor — a full health check on your project's EnvShield setup at once (config files present, schema/template in sync, deployment manifest registered and valid, and more). --fix offers to fix what it can.
Want typed, validated config code instead of raw os.getenv() calls? envshield generate compiles the schema into a Python (pydantic-settings) or TypeScript (zod) module — a secondary, opt-in convenience once the contract exists, not a separate thing to learn.
envshield undeclared — catch a new environment-variable read that isn't in the schema yet, before you commit it. Compares source code between two git revisions (or your working tree against HEAD by default):
$ envshield undeclared
┌────────────────┬─────────────┬──────┬───────────┬──────────────────────┐
│ Variable │ File │ Line │ Access │ Contract status │
├────────────────┼─────────────┼──────┼───────────┼──────────────────────┤
│ ANALYTICS_KEY │ app/main.py │ 42 │ os.getenv │ missing declaration │
└────────────────┴─────────────┴──────┴───────────┴──────────────────────┘envshield explain VARIABLE — everything EnvShield knows about one variable: its contract, where it's declared, what source code reads it, and which deployment manifests reference it.
envshield schema diff — compare the schema contract between two git revisions, classified by impact:
$ envshield schema diff origin/main HEAD
┌───────────────────────┬──────────┬───────────────────────────────────────────┐
│ Variable │ Category │ Description │
├───────────────────────┼──────────┼───────────────────────────────────────────┤
│ STRIPE_WEBHOOK_SECRET │ BREAKING │ 'STRIPE_WEBHOOK_SECRET' was added and │
│ │ │ must always be present (no requiredIf, so │
│ │ │ nothing exempts an existing config from │
│ │ │ now needing it). │
│ STRIPE_KEY │ SECURITY │ 'STRIPE_KEY' secret classification │
│ │ │ WEAKENED (true -> false) -- it will no │
│ │ │ longer be masked in generated code, scan │
│ │ │ output, or validation-error messages. │
└───────────────────────┴──────────┴───────────────────────────────────────────┘This is the schema functioning as a real contract: a change to it is reviewable on a PR, the same way an API contract change would be — not just a local file that happens to agree with itself.
envshield service discover / service add register services in a multi-service project — see Monorepos. A supporting safety check, scan, exists alongside all of this — see Secret safety.
Every command above documents its own options with --help; the full reference is in the docs.
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches before a change reaches production:
A new environment variable used in code but missing from the schema:
envshield undeclared origin/main HEAD --service apiA schema contract change:
envshield schema diff origin/main HEAD --service apiA secret accidentally introduced into a commit:
envshield hook installThe pre-commit hook runs scan --staged --enforce, applying classification-based enforcement with interactive override for high-confidence findings on local commits, and strict blocking in CI. See Secret safety for enforcement behavior details.
In CI, diff against the merge-base, not the base branch's current tip (which can move while the PR is open):
# .github/workflows/envshield.yml
on: pull_request
jobs:
contract-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: pipx install envshield
- run: |
BASE=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
envshield schema diff "$BASE" HEAD
envshield undeclared "$BASE" HEADBoth commands exit non-zero on a real finding, and also on an error that prevented evaluation (a bad revision, a schema that failed to parse) — never a silent "clean" for something it couldn't actually check. Add --json if you want to post a custom summary instead.
envshield undeclared also supports --sarif — a standard SARIF 2.1.0 log, for GitHub Code Scanning or any other SARIF-consuming CI tool, so a missing declaration is annotated on its exact file and line rather than only appearing in a CI log:
- run: |
BASE=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
envshield undeclared "$BASE" HEAD --sarif > envshield.sarif
continue-on-error: true # upload the report even when it found something
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: envshield.sarif--sarif only exists for undeclared, deliberately: each finding there has a real file and line to point at. schema diff findings describe the contract (a variable added, removed, or changed) rather than a source location, so forcing them into SARIF would mean inventing a line number that isn't real — use its --json/exit-code output instead, or annotate it yourself (e.g. a jq step over the JSON, emitting ::error:: workflow commands).
An error that prevents evaluating a service (a bad revision, a schema that fails to parse) is never turned into a fabricated SARIF result — it's reported as a SARIF invocation with executionSuccessful: false, so a consumer can't mistake "couldn't check" for "checked and clean."
Adopting EnvShield into a project that already has configuration doesn't require rewriting anything:
.env (existing)
↓
envshield import .env
↓
review env.schema.toml (fix classifications, add descriptions)
↓
envshield check (confirm your local file already satisfies it)
↓
envshield undeclared (confirm nothing's silently missing)
↓
CI: schema diff + undeclared (governance from here on)
import reads your real values and does most of the schema-writing for you — it always leaves a "TODO: Add description." marker as a nudge to review, and any automatic secret/type classification is a starting point, not a final answer. Not using a .env file? envshield import config/settings.py reads a Python config module's assignments via its AST (never executes the file).
One schema can serve every service in a repository. Grant each variable to the services that use it, and each service is checked against only its own variables:
[LOG_LEVEL] # no services: every service gets it
defaultValue = "info"
[DATABASE_URL]
secret = true
services = ["api", "worker"]
[PORT] # per-service values
type = "port"
[PORT.services.api]
defaultValue = "8000"
[PORT.services.worker]
defaultValue = "9000"# envshield.yml
services:
api:
schema: env.schema.toml
dir: api
local_file: api/.env
worker:
schema: env.schema.toml
dir: worker
local_file: worker/.env
manifests:
- file: docker-compose.yml
containers: # Compose service -> EnvShield service
api: api
worker: workerA few rules keep this safe:
- A secret in a shared schema must name its services. An unscoped secret is an error, so a secret never reaches a service by accident.
- Each service sharing a schema has its own
dir:. Two services can't claim the same directory (service addwritesdir:for you). - A registered manifest maps containers to services.
checkthen validates every mapped container against its own service's variables.
The full model is in docs/architecture/shared-system-schema.md. One schema per service still works too, with shared variables composed through extends (see Composing schemas).
envshield service discover # finds service-like directories, registers + seeds each one
envshield service list
envshield check --service apiEvery command is service-aware. --service is optional with exactly one service configured, inferred automatically when you're standing inside a registered service's own directory, and only prompted for interactively when there's more than one and neither applies.
undeclared and explain only walk a service's own directory by default. If part of a service's real configuration is read from a shared internal library that lives outside that directory — a common-utilities package imported by several services, say — those reads are invisible to both commands, for every service that imports the library.
additional_source_roots widens a service's discovery scope to cover extra directories:
services:
api:
schema: services/api/env.schema.toml
additional_source_roots:
- libs/sharedundeclared --service api and explain --service api now also look inside libs/shared — a variable read there counts toward api's own discovery results exactly as if it lived in services/api/ itself. The same directory can be listed by more than one service; each one independently sees the reads it contains. A path that doesn't exist yet contributes nothing (no error); a path resolving outside the project is refused, the same protection every other path in envshield.yml already has. scan is unaffected — it's already repository-wide.
By default, every registered source for a service — its local_file, each registered deployment manifest — is validated independently against the whole schema: check/doctor expect any one of them to be self-sufficient on its own. Some services genuinely don't work that way: part of the configuration lives in a local config file, the rest is supplied by a deployment manifest. Under the default model, a variable that only ever lives in one of them shows up as "missing" everywhere else, even though nothing is actually wrong.
Set completeness: union on a service to validate against the combined presence of all its registered sources instead — a variable satisfied by any one of them satisfies the service as a whole:
services:
api:
schema: services/api/env.schema.toml
local_file: services/api/config/settings.py
completeness: union
manifests:
- file: docker-compose.yml
containers:
api: api[DB_HOST]
description = "set in config/settings.py"
[FEATURE_MODE]
description = "set via docker-compose.yml"With completeness: union, check/doctor stop expecting config/settings.py to declare FEATURE_MODE, or docker-compose.yml to set DB_HOST — the schema is satisfied as long as every variable is present somewhere among the registered sources. envshield schema sync respects this too: it only ever appends a variable to a Python local_file that's genuinely missing from every registered source, never one that another source already covers.
A few things worth knowing:
- Opt-in, per service. Unset (the default) means every source must be independently self-sufficient, exactly as before this existed — nothing changes unless you set the key.
- Union means presence, not values. Whether an ordinary variable is satisfied depends only on whether it's present (and valid) somewhere — never on merging, comparing, or picking between its values across sources. A blank or invalid value is still reported, wherever it occurs. The one deliberate exception is a
requiredIftrigger's own value (see next) — deciding whether a condition currently holds has always meant reading one specific value, even beforecompleteness: unionexisted; union only extends where that one value may come from, it doesn't turn this into general value merging. - A
requiredIfcondition (see Conditional requirements above) is resolved against the union too — if the variable a condition depends on lives in a different source than the one being checked,completeness: unionstill finds it. Sources that agree on the trigger's value resolve normally; if two sources genuinely disagree, EnvShield reports it as an explicit conflict rather than guessing which one wins — registration order is never used as a tiebreaker. - A source that fails to load is always a failure, even if the sources that did load happen to satisfy the schema between them.
doctorreports this as two separate signals for a union-mode service — source health (does this file exist and parse) and registered-source completeness (does the union satisfy the schema) — so one can never quietly hide behind the other.check --jsonreflects the same split: each source's own result is still reported individually, plus onecombinedentry per union-mode service carrying the aggregate verdict.
A manifest registration normally points at one file:
manifests:
- file: docker-compose.yml
containers:
api: apiA real Compose project commonly splits its configuration across a base file plus an override that Compose applies automatically (docker-compose.yml + docker-compose.override.yml, or any other -f a -f b combination). Registering only the base misses whatever the override adds; registering the override alone reports the base's own variables as falsely missing, since an override file is deliberately just a delta. files: registers the whole ordered layer set as one logical manifest instead:
manifests:
- files:
- docker-compose.yml
- docker-compose.override.yml
containers:
api: apiOrder matters — later files override earlier ones, exactly like docker compose -f docker-compose.yml -f docker-compose.override.yml itself. check/doctor/explain all validate the merged result as a single source, under one combined label ("docker-compose.yml + docker-compose.override.yml").
This is Compose-specific merge behavior for the fields EnvShield already understands (environment:, env_file:) — not a general YAML merge engine. Volumes, networks, ports, build, and every other Compose field are never inspected or merged. files: is unrelated to completeness: union above: union combines presence across different kinds of source (a local file and a manifest); files: merges values across ordered layers of one Compose source. A manifest entry can declare file: or files:, never both.
Source-code discovery (what powers undeclared and explain's "used in source" section) is AST-based for Python (os.environ.get, os.getenv, os.environ[...]) and pattern-based for JavaScript/TypeScript (process.env.X, process.env["X"], import.meta.env.X, including single-level destructuring). No other language is discovered yet — the schema and validation commands work with any stack, but undeclared/explain can only see what's read from these two.
explain additionally recognizes Flask's current_app.config[...]/.get(...) in Python — including the common from flask import current_app as <alias> import form — and reports it at medium confidence, shown inline as (medium confidence): it's one level of indirection through a config object whose contents could come from anywhere, not a direct environment read. This recognition is deliberately explain-only — it never affects undeclared's or scan's pass/fail signal, so a Flask-config read can never quietly satisfy (or break) a completeness check the way a direct os.environ/os.getenv read does.
A supporting check alongside the contract, not the product itself. secret = true on a field tells EnvShield (and setup, and code generation) that it's sensitive — the schema records that fact, never the actual value.
envshield scan looks for hardcoded secrets by pattern (Stripe, AWS, GitHub tokens, and more), and for env-var reads the schema doesn't declare, in one pass. Values are always redacted in output — a Stripe publishable key (pk_...) is never flagged, since detection matches the secret-key pattern (sk_...) by shape, not the variable's name.
Python candidates are analyzed for syntactic context to distinguish code patterns from secret values:
# Classified as code (suppressed):
client(key=CONFIG_CONSTANT)
def handler(api_key: str): ...
# Classified as likely secret (detected):
client(key="sk_live_abc123...")This reduces false positives on ordinary code patterns without broadly suppressing secret-shaped values. Classification is lightweight and syntactic — it recognizes type annotations, function arguments, and identifier references, but doesn't perform semantic analysis beyond what the tokenizer can see.
envshield hook install wires secret scanning into your Git workflow with classification-based enforcement:
envshield hook installThe pre-commit hook runs scan --staged --enforce, which scans only staged content and applies an enforcement policy based on classification confidence.
| Finding type | Interactive (local commit) | Non-interactive (CI) |
|---|---|---|
| No findings | Allow | Allow |
| Clearly code-shaped | Suppressed | Suppressed |
| Ambiguous | Block | Block |
| High-confidence secret | Block + explicit override | Block |
Clearly code-shaped candidates (type annotations, function keyword arguments, etc.) are suppressed by the classifier and don't produce findings.
Ambiguous findings — insufficient context to confidently classify — block without override in both modes.
High-confidence secrets (string literals matching secret patterns) block by default but offer an interactive override for local commits:
⚠ EnvShield found potential secrets
HIGH CONFIDENCE
src/config.py:42 Generic API Key [REDACTED (30 chars)]
[1] Abort commit
[2] Commit anyway
Choice: 2
Type COMMIT ANYWAY to confirm: COMMIT ANYWAY
Proceeding with commit. The finding remains in the repository history.
The override requires exact confirmation text — yes or Y won't work. EOF (Ctrl+D) or Ctrl+C aborts. Raw secret values are never displayed in enforcement output.
In CI or other non-interactive contexts, high-confidence findings block with no prompt — the commit is refused, and the scan exits non-zero.
envshield scan # Audit entire codebase
envshield scan --staged # Scan only staged Git content
envshield scan --staged --enforce # Staged scan + enforcement policyThe pre-commit hook uses the third form. scan without flags is for repository-wide audits, not commit-time enforcement.
envshield hook removeRemoves EnvShield's Git hook integration while preserving your project configuration — it does not delete envshield.yml, any env.schema.toml, .env.example, any registered local configuration file (.env, a Python config module, etc.), .gitignore, or any code envshield generate has produced, regardless of whether EnvShield originally created any of it. Those are your project's own configuration contract and application config, not disposable installation artifacts. It lists what it removed and what it preserved.
The only thing it ever deletes is a Git hook whose content exactly matches what EnvShield would generate right now. A hand-modified hook, a foreign hook (even one that happens to contain EnvShield's marker comment), or an EnvShield hook that's gone stale relative to your current envshield.yml (e.g. after registering a new service) is left in place and reported, never deleted or silently regenerated.
envshield hook remove --yes # skip the confirmation prompt--yes only skips the prompt — it never overrides the ownership check; a modified, foreign, or stale hook is preserved with --yes exactly as it is interactively. There is no --force and no --purge: this is intentionally not a "delete everything EnvShield ever created" command. To remove EnvShield's configuration from a project entirely, delete envshield.yml and the schema files yourself once you're sure you no longer need them.
envshield uninstall (and envshield install-hook, for hook install) still work but are deprecated and will be removed in v6.
EnvShield is built around the patterns real projects actually use. A few narrower cases aren't handled yet. None of them let a secret leak:
- Docker Compose: a value with any
${VAR}or$VARreference in it, with or without an operator (-,:-,?,:?,+,:+), comes from the deploy host, which a static check can't see, socheckreports it as could not verify (host_forward). A${VAR-x}/${VAR:-x}fallback that itself breaks the contract is still invalid. Interpolation inside anenv_fileisn't evaluated. - Kubernetes:
env[].valueFrom.secretKeyRef/configMapKeyRefare matched by the container's environment-variable name (env[].name) — name your schema variable after the env var, not the key — and, when the referenced Secret/ConfigMap is defined in the same manifest, the referenced key is validated to actually exist there.envFrom.prefixis applied.initContainersare selectable via--containerbut never auto-selected by default. - Source discovery: Python is AST-based; JavaScript/TypeScript is pattern-based. No other language is discovered yet.
- .env: multiline quoted values aren't supported.
- Windows: the write-safety fix for a failed permission change (BL-162) hasn't been run on Windows; where
os.fchmodis missing (Windows before Python 3.13) it's skipped, which is tested on Linux by simulation only. - CI: runs on Ubuntu only (Python 3.10–3.12).
These are bounded, tracked engineering work — see the roadmap for what's next and what EnvShield deliberately isn't building.
This README is enough to get real value out of EnvShield. For the full command reference, every schema field, envshield.yml structure, exit codes, and JSON output shapes, see the documentation.
MIT — see LICENSE.md.
