Documentation
๐ Quick Start Guide
Go from a chaotic environment to a single Environment Contract โ documented, validated, and compiled into typed code โ in less than 5 minutes. This guide will walk you through the essential workflow.
๐ฌ Prefer visual learning? Check out the Features & Examples page for real-life walkthroughs with before/after scenarios and animations.
1. Install EnvShield
You'll need Python 3.10 or newer. Install EnvShield directly from PyPI โ it's a standalone CLI, no dependencies on your project's stack.
pip install envshield
2. Initialize Your Project (Three Paths)
Choose the path that fits your project. All three create a schema (`env.schema.toml`) that becomes your single source of truth.
# Scaffolds a schema from your framework (Next.js, Django, Flask, etc.)
# Also creates a git hook and updates .gitignore
envshield init
# Reads your existing .env file and auto-generates 90% of the schema
# The tool detects secrets, suggests defaults, and saves you manual work
envshield import .env.production --interactive
# Finds every service (api, web, worker, ...), registers them, and seeds
# each schema from that service's own real, current config -- in one command
envshield service discover
๐ก Verified example: we ran envshield import against a real 59-variable Flask config โ it correctly classified all 27 secrets and suggested real default values for 15 of the non-secret variables, with zero manual TOML written.
3. Curate Your Schema (The Contract)
Open the newly created `env.schema.toml`. This is your Environment Contract โ the single source of truth for all configuration. Review each variable, add human-readable descriptions, and mark which ones are secrets. This is the ONLY file you'll hand-edit.
[DATABASE_URL]
description = "The full connection string for the database."
secret = true
[LOG_LEVEL]
description = "Controls the application's log verbosity (DEBUG, INFO, WARNING, ERROR)"
secret = false
defaultValue = "info"
[STRIPE_API_KEY]
description = "Stripe API secret for payment processing"
secret = true
๐ก Why this matters: This schema now drives everything โ your `.env.example` generation, your typed config code, your validation, your onboarding wizard. Once it's right, everything stays in sync automatically.
4. Sync Documentation & Setup Your Environment
Now generate your `.env.example` (documentation) from the schema, and create your local `.env` file interactively. EnvShield will prompt you for each variable, hide secrets as you type, and validate everything.
# Generate .env.example from your schema (docs stay in sync)
envshield schema sync
# Interactively create your local .env (you'll be prompted for each var)
envshield setup
# Or create a different env file:
envshield setup .env.local
๐ก What just happened: Your `.env.example` was regenerated from the schema (never stale again), and you created your local `.env` without any manual copy-paste or guessing about which vars are secrets.
5. You're All Set!
Your environment is now secure and documented. The Git pre-commit hook is active and will scan for secrets before each commit. Your configuration is guaranteed to be in sync with your schema. You're ready to go!
# Verify everything is in order
envshield doctor
# Now build something amazing!
python app.py # or npm start, etc.
6. (Optional) Compile Your Contract Into Code
Turn the schema into the actual config your app
imports โ typed, validated, and with secrets
masked by default. Auto-detected for your
stack, or set explicitly with
--lang.
# Python (pydantic-settings)
envshield generate --lang python
# TypeScript (zod)
envshield generate --lang typescript
๐ Intelligent Lifecycle Management & C6 Diff-Aware Scanning
EnvShield now includes two powerful features designed to make your workflow seamless while catching real security issues:
Intelligent Lifecycle Management
When you run init, service discover, or setup, EnvShield prompts to install Git hooks for you โ but only once. The post-merge hook is also smart: it only runs drift detection if schema files actually changed, preventing unnecessary output on code-only merges.
- Smart Hook Prompting: Asked during initial setup, never spammed again
- Pre-commit Hook: Scans for secrets before every commit
- Post-merge Hook (Schema-Aware): Only runs `envshield doctor` if schema files changed
C6: Diff-Aware Secret Scanning
EnvShield's C6 feature solves the "false positives vs. missed secrets" problem by scanning only newly-added lines in excluded files. This is perfect for projects with intentional baseline secrets (like Zeus's local configs with 15+ fake dev secrets).
- Line-Level Analysis: Compares HEAD vs. staged content to identify new lines
- Pre-Existing Baseline Ignored: Old secrets don't trigger false positives
- Real Secrets Caught: New secrets added to excluded files are flagged
- Zero Configuration: Works automatically when scanning excluded files
Use case: Your local config file has 15 intentional fake secrets (committed to git for dev use). A teammate accidentally adds a real production secret. C6 catches the real secret while not flagging any of the 15 baseline fakes โ no false positives, no missed leaks.
๐ก Core Concepts: The Environment Contract
EnvShield treats configuration as a contract โ a formal agreement between your schema and your application. Understanding these three artifacts is key to mastering this workflow. Only the first one is hand-written โ the other two are always generated from it.
- The Schema (`env.schema.toml`): This file is the Environment Contract itself. It's the undisputed source of truth, defining every variable your project needs, what it does, whether it's a secret, its type/format constraints, and if it has a default value. It lives in your repository and is version-controlled like the rest of your code.
- The Example File (`.env.example`): This file is the human-readable documentation. It's a build artifact that is automatically generated from your schema via the `schema sync` command. You never edit this file by hand; you commit it so that anyone browsing your repository can quickly understand the configuration requirements without needing the tool installed.
- The Generated Config Module: This is the machine-readable enforcement of the contract. Running `envshield generate` compiles the schema into real, importable `pydantic-settings` (Python) or `zod` (TypeScript) code โ your application reads its configuration through this typed, validated module instead of raw `os.getenv`/`process.env` calls.
Every field a schema variable can have
| Field | Example | Meaning |
|---|---|---|
description |
"PostgreSQL connection string" |
Shown during setup; copied into generated code as documentation. |
secret |
secret = true |
Hidden input in setup; masked in generated code (SecretStr / Secret<T>). Never given an inferred type during import. |
defaultValue |
defaultValue = "5000" |
A fallback. Without it, the variable is required. |
type |
type = "port" |
string (default) | int | float | bool | port (1โ65535) | url | email. Enforced by check/doctor/setup; drives the generated type. |
enum |
enum = ["debug","info","warn","error"] |
Value must be one of these. Implies type = "enum". setup shows a picker, not free text. |
pattern |
pattern = "^v\d+\.\d+\.\d+$" |
An extra regex constraint, layered on top of type. |
requiredIf |
{ var = "X_ENABLED", equals = "true" } |
Required only when another variable currently holds that value. |
A variable with none of type/enum/pattern/requiredIf behaves exactly as it always has โ an unconstrained string, required unless it has a default. Schemas written before these fields existed remain fully valid.
๐งฉ Sharing Variables Across Services (`extends`)
A monorepo with ten services usually has five or six variables every one of them needs โ `LOG_LEVEL`, `SENTRY_DSN`, `DATADOG_API_KEY`. A schema can extend a shared base instead of redeclaring them in every service.
[LOG_LEVEL]
description = "Log verbosity, shared across every service"
enum = ["debug", "info", "warn", "error"]
defaultValue = "info"
[SENTRY_DSN]
description = "Error tracking"
secret = true
extends = "../../shared/base.schema.toml"
[DATABASE_URL]
description = "API-specific"
secret = true
Loading services/api/env.schema.toml now transparently includes LOG_LEVEL, SENTRY_DSN, and DATABASE_URL โ every command just sees the merged result.
extendsaccepts a list, for more than one shared base.- Chains work: a base can itself extend another base. A circular chain is detected and rejected.
- The child's own definition always wins on a conflict, in full โ fields aren't merged individually.
Honest limitation: extends resolves a local path within your project โ it doesn't fetch a schema from a separate git repository or a package registry today. See the Roadmap.
๐ฆ Validating Deployment Manifests
envshield check accepts a docker-compose file or a Kubernetes manifest, in addition to a plain .env file โ auto-detected by content, not extension.
envshield check docker-compose.yml
envshield check k8s/deployment.yaml --container api
docker-compose: merges environment: with any env_file: references (environment: wins on a conflict, matching Compose's own precedence). A bare KEY with no value, or anything sourced only from env_file, is treated as present-but-not-visible-here rather than flagged missing.
Kubernetes: Deployment, StatefulSet, DaemonSet, Job, CronJob, and bare Pod manifests, including multi-document (`---`-separated) files. A `ConfigMap`/`Secret` referenced via `envFrom` is resolved if it's defined in the same file; a `valueFrom` reference is treated the same as compose's `env_file` case.
Multiple services/containers in one file? `--container` picks which one. Omit it and EnvShield tries your `--service` name first, before asking you to be explicit.
Register it once: `init`/`service discover`/`service add` auto-detect a compose file and register it. Once registered, plain `envshield check` validates it automatically alongside your `.env`, and `doctor` gains a "Deployment Manifest" health check โ shown only for projects that have one registered.
envshield service add api services/api --deployment-manifest docker-compose.yml --container api
Honest limitation: EnvShield only reads deployment manifests โ it never generates or rewrites one.
๐ฏ Why EnvShield is Different
EnvShield isn't trying to replace a dedicated secret scanner or a cloud secret manager โ it's the schema/contract/codegen layer that sits alongside whichever of those you already use. Here's how it compares to the tools people usually reach for instead:
| Capability | EnvShield | Gitleaks | dotenvx | Doppler / Infisical |
|---|---|---|---|---|
| Schema-driven validation (types, enums, conditional requirements) | โ | โ | โ | โ |
| Multi-service schema management, with shared/composed schemas | โ | โ | โ | Partial (multi-environment, not a documented shared contract) |
| Typed config code generation | โ | โ | โ | โ |
| Validates deployment manifests (compose, Kubernetes) | โ | โ | โ | โ |
| Interactive onboarding | โ | โ | โ | โ (hosted) |
| Secret detection depth/accuracy | โ ๏ธ Good enough for most teams | โ Best in class, actively maintained | โ | Varies by plan |
| Syncs real secret values across a team/environments | โ (never touches real values) | โ | Encrypts values in the file | โ |
| Works offline | โ Always | โ | โ | Depends on plan/self-hosting |
The honest bottom line: if raw secret-detection accuracy is your main concern, run Gitleaks (or similar) alongside `scan` rather than relying on it alone โ its maintainers focus on detection full-time, EnvShield's `scan` doesn't try to compete on that axis. If syncing real secret values across a team is your main need, Doppler/Infisical solve that, and EnvShield deliberately doesn't attempt it. What EnvShield is the only thing here that does at all: one schema, in your own git repo, that's simultaneously documentation, a validation contract, a codegen input for two languages, and something your deployment manifests are checked against.
๐ A-to-Z Command Reference
๐๏ธ init
The "zero-to-hero" command. Run this first in a new project to scaffold a complete, best-practice configuration foundation.
What it does:
- Intelligently inspects your project to detect the framework (e.g., Next.js, Django).
- Creates an `env.schema.toml` file with smart defaults for that framework.
- Creates a simple `envshield.yml` configuration file.
- Safely updates your `.gitignore` file with necessary patterns.
- Automatically installs the Git pre-commit hook for proactive security.
Flags:
--force or -f: Allows
`init` to overwrite existing EnvShield files. It
will always ask for a final confirmation before
proceeding.
Use Case
You're starting a new Django project. You run `envshield init`. The tool detects Django, creates a schema with `SECRET_KEY` and `DATABASE_URL`, updates your `.gitignore`, and installs the security hook. Your project is set up for success in one command.
Already have more than one service in this repo? `init` bootstraps a single schema โ for a multi-service project, use `envshield service discover` instead.
๐ import
The "get started yesterday" command. This is the fastest way to adopt EnvShield for an existing project with a large, messy `.env` file.
What it does:
- Reads an existing `.env` file (like `.env.production`).
- Intelligently analyzes each variable's key and value.
- Automatically marks known secrets (API keys, tokens) using its built-in scanner logic.
- Suggests default values for common variables (like `DEBUG` or `PORT`).
- Generates a complete `env.schema.toml` file for you.
Arguments & Flags:
-
<file>: (Required) The path to the `.env` file you want to import. -
--outputor-o: Specify a different output path for the schema file (default: `env.schema.toml`). -
--forceor-f: Allows `import` to overwrite an existing schema file. -
--interactive: Walks you through each variable one by one, letting you confirm if it's a secret and if you want to set a default value.
Use Case
You want to adopt EnvShield for a 2-year-old project. You run the command:
envshield import .env --interactive
The tool guides you through all 50 variables, correctly guessing 45 of them, and generates a perfect schema file in two minutes instead of an hour of manual work.
๐ญ service
The fast path for multi-service projects. A
subcommand with three actions:
discover, add, and
list.
discover โ What it does:
-
Scans your repo for directories that look
like independent services โ a real dotenv
file (any
.env.*name, not just.env), or a recognizable Python config module. -
Skips a directory that only has a generic
project marker (
pyproject.toml,package.json) with no actual config in it โ a shared library sitting next to your real services won't get mistaken for one. - Shows you what it found and asks before writing anything.
-
On confirmation, registers each service in
envshield.ymland seeds its schema straight from its real, current config โ the same logic asimport. - Safe to run again later: already-registered services are never re-suggested or touched. It bootstraps a fresh setup from nothing, or extends an existing one with whatever's new.
$ envshield service discover
Discovered Services
โโโโโโโโโโณโโโโโโโโโโโโโโโโโณโโโโโโโโโณโโโโโโโโโโโโโโโโโโโโโโโโ
โ Name โ Directory โ Format โ Config File โ
โกโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฉ
โ api โ services/api โ dotenv โ (default .env) โ
โ web โ services/web โ dotenv โ (default .env) โ
โโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโ
? Add these services to envshield.yml? (api, web pre-selected) Yes
โ Registered api โ services/api/env.schema.toml
โ Registered web โ services/web/env.schema.toml
add & list
envshield service add <name> <directory>
registers one service by hand โ useful when you'd
rather be explicit than rely on detection, with
--local-file, --example-file,
--description / -d,
--schema (a non-default schema path),
and --import <file> to seed its
schema in the same step.
--deployment-manifest registers a
docker-compose file or Kubernetes manifest to
validate automatically (auto-detected if omitted โ
see Deployment
Manifests), and --container names
which service/container in it is this one, if it
isn't named the same.
envshield service list prints every
service currently configured.
Use Case
You're adopting EnvShield in an existing Turborepo
with five apps. Instead of hand-writing
envshield.yml and running
import five times, one command finds
them all, and one confirmation seeds all five
schemas from their real, current
.env files.
๐ช scan
Your project's personal bodyguard. It scans files for hardcoded secrets and undeclared environment variables used in your code.
Arguments & Flags:
-
[PATHS]...: The specific files or directories to scan. If you don't provide a path, it scans the current directory. -
--staged: Scans only the files you've staged for your next Git commit. This is the heart of the pre-commit hook. -
--config <file>: Use a different `envshield.yml` for this specific scan. -
--exclude <pattern>: A glob pattern to exclude from this specific scan. Can be used multiple times.
Use Case
Your pre-commit hook is blocking a commit because of a secret in a test file. You realize you forgot to add an exclusion rule. You can add it to `envshield.yml` to fix it permanently, or use a one-off command to bypass it for now:
git commit -m "My commit" --no-verify # Bypass the hook for now
envshield scan . --exclude "**/tests/*"
๐ install-hook
Manually installs the Git pre-commit hook if you skipped it during `init` or if it was removed.
What it does:
Creates (or overwrites) a `pre-commit` script in your `.git/hooks/` directory that runs `envshield scan --staged`.
Use Case
You've added `envshield` to a project that already had a different pre-commit hook. After you manually merge the two scripts, you can run `envshield install-hook --force` to create the final, combined hook.
๐ schema
The `schema` command is a subcommand with one action: `sync`. It's your primary tool for keeping your project's documentation (`.env.example`) perfectly in sync with your schema.
Usage
envshield schema sync
Use Case
You add a new `REDIS_URL` variable to your `env.schema.toml`. Before you commit, you run `envshield schema sync`. The `.env.example` is instantly and correctly updated with the new variable and its description, ready to be committed.
โ check
The "is it plugged in?" command for your local setup. It validates a local environment file โ or, these days, a docker-compose file or a Kubernetes manifest โ against the official contract in `env.schema.toml`.
What it does:
It reports variables missing entirely, variables declared but still left blank (e.g. a required secret checked in as an empty placeholder), variables present but not matching their declared `type`/`enum`/`pattern`, and extra variables that aren't in the schema โ helping you find configuration errors before they cause runtime bugs, not after. When you don't pass a file argument, it also checks the registered deployment manifest (if any) in the same run โ see Deployment Manifests.
Arguments & Flags:
-
[file]: The file to validate. Omit it to check the project's/service's default local file (and its registered deployment manifest, if any). -
--service/-s: On a multi-service project, which service's schema to check against. -
--container: For a docker-compose or Kubernetes manifest declaring more than one service/container, which one to check. Tried against `--service`'s name automatically first.
Use Case
Your app fails to start after a teammate's PR. You run the command:
envshield check .env.local
The tool reports `Missing in Local: NEW_SERVICE_API_KEY`, instantly telling you what's wrong. Add it as a CI step (see the README's Maintaining EnvShield section) and this becomes a pull-request check, not a post-deploy surprise.
๐ช setup
A taste of the automated onboarding magic. This is the perfect command for getting started on a project.
What it does:
It looks at whatever you already have โ your real local file if one exists, or `.env.example` as a template if not โ and prompts you only for variables that are still blank or missing, showing each one's description and masking input for secrets. On a multi-service project, running it with no `--service` asks which one (or All services, walking through each service's wizard in turn). If your local config isn't a dotenv file โ e.g. a Python module โ it patches just the missing values into it in place, instead of overwriting the file.
Use Case
You just cloned a new project. You run `envshield setup`. The tool asks you for the `DATABASE_URL` and `STRIPE_API_KEY`, then generates your fully-populated `.env` file. You are ready to run the project in minutes.
๐ฉบ doctor
The "turn it off and on again" command for your entire configuration. It runs a comprehensive suite of health checks on your project's `envshield` setup.
What it does:
- Checks that your `envshield.yml` and `env.schema.toml` files exist and are valid.
- Validates your local `.env` file against the schema, including type/enum/pattern constraints, not just presence.
- Ensures your `.env.example` file is in sync with the schema.
- Verifies that the security hook is installed where Git will actually run it (respecting a configured `core.hooksPath`, e.g. Husky).
- If a deployment manifest is registered for this project/service, validates it too โ this check is only shown at all when one actually is.
Flags:
--fix: If the doctor finds a problem,
it interactively asks if you want to fix it โ
re-running `init`, regenerating `.env.example`,
installing the hook, or, for missing/blank/invalid
local values, running the same `setup` wizard used
for onboarding (which fixes exactly what's wrong
and leaves everything else untouched).
Use Case
Something just feels wrong with your setup. You run `envshield doctor`. It reports that your `.env.example` is out of date and the Git hook is missing. You run `envshield doctor --fix`, answer "Yes" to both prompts, and the tool fixes everything for you.
๐งฌ generate
The command that turns your contract into code. It compiles `env.schema.toml` into a typed, validated config module โ no more raw `os.getenv()` / `process.env` calls scattered through your codebase, and no more secrets that can accidentally end up in a log line.
What it does:
- Renders every variable in your schema into a real, importable module: `pydantic-settings` for Python, or a `zod`-validated module for TypeScript.
- Types each field from its schema's explicit `type`/`enum`/`pattern` when set (`AnyUrl`, `EmailStr`, `Literal[...]`, a regex-constrained string) โ or, if none is set, infers `int`/ `bool`/`string` from the shape of `defaultValue`, same as always. Variables with no default become required fields โ the module raises a validation error at import time if they're missing.
- Wraps every `secret = true` variable in a masked type (`SecretStr` in Python, a local `Secret<T>` wrapper in TypeScript) so it can't accidentally leak via a `print()`, `console.log`, `JSON.stringify`, or stack trace. You call `.get_secret_value()` / `.value` explicitly to read it.
Arguments & Flags:
-
[output_file]: Where to write the generated module. Defaults to `config.py` or `config.ts` depending on the resolved language. -
--langor-l: `python` or `typescript`. If omitted, EnvShield auto-detects from your project the same way `init` does โ Next.js, Vite, and Node.js projects default to TypeScript; everything else defaults to Python. -
--forceor-f: Allows `generate` to overwrite an existing output file.
Use Case
Your Next.js app currently reads `process.env.STRIPE_SECRET_KEY` directly in six different files, with no validation that it's actually set. You run:
envshield generate --lang typescript
EnvShield writes `config.ts` with a `zod` schema built from your contract. You replace the six direct `process.env` reads with `import { env } from './config'` โ now a missing or malformed variable fails loudly at startup instead of silently at runtime, and `env.STRIPE_SECRET_KEY` prints as `**********` anywhere it's accidentally logged.
โ๏ธ The `env.schema.toml` File
This file is the heart of your configuration. It's a TOML file where you define every environment variable your project needs. See the full field reference above for every key a variable can have โ here's a realistic file using several of them together:
# A schema can extend a shared base -- see "Sharing Variables Across Services" below.
# extends = "../../shared/base.schema.toml"
[DATABASE_URL]
description = "The full connection string for the PostgreSQL database."
secret = true
[API_PORT]
description = "Port the API listens on."
type = "port"
defaultValue = "5000"
[LOG_LEVEL]
description = "Controls the application's log verbosity."
enum = ["debug", "info", "warn", "error"]
defaultValue = "info"
[STRIPE_API_KEY]
description = "Stripe API secret for payment processing."
secret = true
[FEATURE_X_ENABLED]
description = "Toggles the new billing flow."
type = "bool"
defaultValue = "false"
[FEATURE_X_API_KEY]
description = "Only needed once feature X is turned on."
secret = true
requiredIf = { var = "FEATURE_X_ENABLED", equals = "true" }
โ๏ธ The `envshield.yml` File
This file controls EnvShield's own settings โ and, for a
multi-service project, it's where every service is
registered. envshield service discover
writes this file for you (see below); this is what it
looks like under the hood.
# The name of your project (used for display purposes).
project_name: my-project
# Global settings for the secret scanner.
secret_scanning:
# A list of file patterns to ignore during scans (e.g., test files).
exclude_files:
- "**/tests/*"
- "**/test/*"
services:
api:
path: services/api/env.schema.toml
# Auto-detected by `service discover`/`service add`, or set with
# --deployment-manifest / --container -- see Deployment Manifests above.
deployment_manifest: docker-compose.yml
container: api
web:
path: services/web/env.schema.toml
# A service whose local config isn't a dotenv file at all -- e.g. a
# Flask app that keeps its local config in a plain Python module --
# points `local_file` at it directly. EnvShield reads/writes it as
# source, patching or appending plain assignments in place rather
# than treating it like a dotenv file.
worker:
path: services/worker/env.schema.toml
local_file: services/worker/config/settings.local.py
A single-service/root project registers its deployment manifest at the top level instead (there's no `services:` block to hold it): a bare `deployment_manifest: docker-compose.yml` (and optional `container:`) alongside `project_name`.
Every command accepts --service <name>.
Omit it on a single-service project and nothing changes;
omit it on a multi-service project and you're prompted โ
pick one service, or All services to run
against every one of them in the same command.
๐ฃ๏ธ Roadmap
Everything documented on this page is available now, free, and stays that way โ nothing here moves behind a paywall later.
Available now, free, forever โ
- Schema-driven configuration management, with real types, enums, patterns, and conditional requirements
- Multi-service support, with automatic service discovery and shared/composed schemas (`extends`)
- Typed config code generation (Python + TypeScript)
- Deployment-manifest validation (docker-compose, Kubernetes)
- Interactive onboarding wizard
- Configuration drift detection
- Pre-commit secret scanning
Being explored โ no committed timeline
What actually gets built next is driven by what real projects hit first, not a fixed plan. Currently under consideration:
- Team secret sharing & environment coordination: a hosted way to share actual secret values and coordinate per-environment (dev/staging/prod) overrides across a team โ the one thing this tool is explicit about not doing today.
- Cross-repo schema sharing: today's `extends` is local-path-only, within one project. Referencing a base schema across separate repositories isn't supported yet.
- Secret-manager integrations: pulling real values from Vault/AWS/GCP Secrets Manager for local `setup`/`check`, without EnvShield ever storing them itself.
If any of these would matter to you, open a discussion โ real usage, not a roadmap slide, decides what ships next.