EnvShield 🛡️ – Configuration Orchestration for Multi-Service Projects
Stop managing configuration like it's a secret. Start declaring it like it's code.
EnvShield turns scattered .env files and configuration chaos into a single, versioned contract (env.schema.toml) that every service agrees on. One schema, multiple services, always in sync — with typed config code, automated onboarding, and zero-friction validation.
For teams that have:
- 🔀 Multiple services in one repo (API + web + worker)
- 😰 Config drift between local, staging, and production
- ⏰ Long onboarding where devs spend 2 hours figuring out what env vars they need
- 🤦 Silent failures because a typo in
os.getenv("DATABSE_URL")returnsNone
EnvShield is your answer.
📚 Full Documentation | 🌐 Website
The Problem
Configuration is chaos.
Your project probably looks like this:
├── .env.example (2 years out of date)
├── services/
│ ├── api/
│ │ ├── config/env_config.local.py
│ │ └── app/...
│ ├── web/
│ │ ├── .env.example (missing DATABASE_URL, has dead vars)
│ │ └── src/...
│ └── worker/
│ └── ... (config docs are in a Slack thread)
The pain:
- New dev: "What env vars do I need?" → 2 hours of digging
- Refactoring
DB_HOSTtoDATABASE_HOST→ breaks 3 services before anyone notices .env.exampledrifts from reality → someone commits a secret, doesn't catch it, pushes to prod- CI fails because
STRIPE_API_KEYisn't in the staging secrets — nobody knows if it should be there - Adding a new third-party integration → scattered across 5 config files with no central record
EnvShield solves this by treating configuration as a contract, not a file.
The Solution: Configuration as Contract
One file, multiple services, single source of truth.
# Single command: read your existing configs
envshield import services/api/config/env_config.local.py --service api
envshield import services/web/.env --service web
# Now you have a contract
cat services/api/env.schema.toml
[DATABASE_URL]
description = "PostgreSQL connection string for the API"
secret = true
[API_PORT]
description = "Port the API listens on (dev: 5000, prod: 8000)"
secret = false
defaultValue = "5000"
[STRIPE_API_KEY]
description = "Stripe API key for payment processing"
secret = true
What this gets you:
-
Typed config code — Not raw strings, actual validated objects:
from config import env db = psycopg2.connect(env.DATABASE_URL) # Type-checked, SecretStr (won't log) port = env.API_PORT # Typed as int, validates on startup
-
Sync everything — All services agree on what config exists:
envshield check services/api/config/.env --service api # Is API's config valid? envshield scan --staged --service api # Are there secrets staged? envshield setup --service api # New dev? Interactive setup
-
One source of truth — Change the contract, everyone sees it:
# Edit services/api/env.schema.toml envshield schema sync --service api # Regenerates .env.example from the contract envshield doctor --service api # Health check: is .env.example in sync?
Quick Start
1. Install
pip install envshield
2. For a single service
cd my-project
envshield init # Create env.schema.toml + .env.example
envshield setup # Interactive setup for new devs
envshield generate --lang python # Generate typed config.py (pydantic-settings)
3. For multi-service (the real win)
# At repo root with multiple services -- no envshield.yml needed yet
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
One command scans for service-like directories (a dotenv file, or a
recognizable config module -- see below), registers each one in
envshield.yml, and seeds every schema straight from that service's
real, current values (the same logic as import) -- skipping any
directory that's just a shared library with no environment config of its
own. Run it again later to pick up a new service without touching the
ones already configured -- it's additive, not a one-time bootstrap.
Detection isn't limited to a plain .env: it matches any .env.* file, so
Rails/Mastodon's .env.production, Nx's per-target
.env.<target>.<configuration>, and similar conventions are all found --
falling back to a checked-in template (.env.example, .env.sample, ...)
when no real local file exists yet.
Prefer to wire things up by hand, or auto-detection missed something?
envshield service add api services/api --import services/api/.env
envshield service list
# Now every command is service-aware
envshield scan --service api # Scan API's code for undeclared vars
envshield setup --service web # Setup web service
envshield setup # "Which service? (api / web / all)"
schema sync, setup, doctor, and check all resolve .env.example /
.env inside each service's own directory by default (wherever its
env.schema.toml lives) -- so two services never clobber the same
root-level file. And --service is genuinely optional on a multi-service
project: omit it with only one service configured and that one is used
automatically; omit it with several and you're prompted -- pick one, or
All services to run against every service in the same command (setup
walks through each service's wizard in turn; the rest just loop and report
per service).
Not using dotenv? Point EnvShield at your real local config file
Some projects don't use .env at all -- e.g. a Flask app whose local
config is a plain Python module (config/env_config.local.py) checked
straight into git. envshield service discover already looks for this
shape automatically (alongside a handful of other common config-module
paths) and sets it up for you; to do it by hand, override local_file per
service and EnvShield reads and writes it as source, not as a dotenv file:
services:
athena:
path: athena/env.schema.toml
local_file: athena/config/env_config.local.py
envshield import athena/config/env_config.local.py --service athena
envshield schema sync --service athena # appends any missing schema vars as `KEY = "..."` -- never rewrites the file
envshield setup --service athena # prompts only for vars that are still blank or missing, patches them in place
Both commands only ever append or patch the specific lines they own -- existing values, comments, imports, and any other logic in the file (conditionals, etc.) are left completely untouched.
Key Features
✅ Intelligent Lifecycle Management
Automatically install and manage Git hooks during project setup with intelligent prompting. When you run init, service discover, or setup, EnvShield asks if you want to install pre-commit and post-merge hooks — but only once. The lifecycle improvements include:
- Smart Hook Prompting: Asked once during initial setup, never spammed again
- Schema-Aware Post-Merge Hook: After merging branches, the hook only runs
envshield doctorif schemas actually changed - Single-Step Integration: Hooks installed automatically during
initorservice discover
# Initialize a project and get prompted for hook installation
$ envshield init
✓ Created env.schema.toml
✓ Created envshield.yml
? Install git hooks for security? (Y/n) y
✓ Pre-commit hook installed (scans for secrets before commits)
✓ Post-merge hook installed (checks config drift after merges)
✅ One Schema, All Services
Declare what config each service needs in one place. Multi-service projects finally have a single source of truth.
Real-life: Your API added STRIPE_API_KEY last month. Did the worker service get it? Web service? With EnvShield, you know instantly.
✅ Migrate Existing Projects in Seconds
envshield import reads your actual .env or settings.py and generates 90% of the schema for you. No manual TOML writing.
envshield import .env # Reads your current .env
# → Generates env.schema.toml with:
# - All variables auto-detected
# - Secrets vs. non-secrets classified
# - Default values extracted
envshield import --interactive # Confirm each classification
Classification understands frontend "intentionally public" naming
conventions too -- NEXT_PUBLIC_*, VITE_*, REACT_APP_*, NUXT_PUBLIC_*,
GATSBY_*, and dotenvx's DOTENV_PUBLIC_KEY are never treated as secrets
just because their name contains "key" or "token": these values are
inlined straight into the client-side bundle by design (a Stripe
publishable key is meant to be public). A real secret-shaped value under
one of these names still gets flagged -- only the naming convention's
false positive is suppressed.
✅ Typed, Validated Config Code
Stop using os.getenv(). Generate real, importable config modules:
# Before (error-prone, untyped)
from os import getenv
db_url = getenv("DATABASE_URL") # str | None, untyped
api_port = getenv("API_PORT", "5000") # defaults as strings
stripe_key = getenv("STRIPE_API_KEY") # visible in logs if leaked!
# After (type-safe, validated, secret-masked)
from config import env
db_url = env.DATABASE_URL # Validated on startup, typed
api_port = env.API_PORT # Validated as int, wrong type fails immediately
stripe_key = env.STRIPE_API_KEY # SecretStr — won't appear in logs
For TypeScript:
import { env } from './config';
const db = await postgres.connect(env.DATABASE_URL); // Zod-validated, typed
const port: number = env.API_PORT; // Type error if not number
✅ Interactive Onboarding
New developer? envshield setup walks them through creating .env, knows which vars are secrets, shows descriptions:
🛡️ EnvShield Setup
Which service? api
Please provide values for the following variables:
[DATABASE_URL]
PostgreSQL connection string for the API
Enter value (password): ••••••••••••
[API_PORT]
Port the API listens on (dev: 5000, prod: 8000)
Enter value: 5000
✓ Successfully created your .env file!
✅ Prevents Configuration Drift
Validate at every step:
envshield check services/api/.env --service api
# ✗ Missing in Local: STRIPE_API_KEY (required, no default)
# ✓ Extra in Local: DEBUG_MODE (not in schema — typo?)
envshield doctor --service api
# ✗ .env.example is out of sync with schema (5 new vars added)
# Suggestion: run `envshield schema sync --service api`
✅ Diff-Aware Secret Scanning (C6)
Blocks secrets before they're committed, with line-level intelligence:
EnvShield's C6 feature scans only newly-added lines in excluded files, allowing pre-existing baseline secrets while catching real secrets you just added:
# Scenario: You have a baseline config with 15 fake dev secrets
# (excluded from scanning). A teammate adds a REAL secret.
$ git add config/env_config.local.py
$ git commit -m "Update config"
# Pre-commit hook runs:
$ envshield scan --staged
ℹ️ config/env_config.local.py (excluded; diffs only: 1 new line)
🚨 DANGER: Found 1 potential secret(s)!
Line 47: PRODUCTION_SECRET = 'real_secret_key_with_long_content'
Commit aborted. Please fix the issues above.
# Pre-existing fakes on lines 1-46 are NOT flagged
# Only the newly-added real secret on line 47 is caught
Why it matters: For projects like Zeus with intentional fake secrets in local configs, this prevents false positives while still catching real secrets before they ship.
CLI Commands
| Command | What It Does | Real-Life Use |
|---|---|---|
envshield init |
Auto-detect framework, create schema & hook | Fresh project setup |
envshield import <file> |
Convert existing .env to schema |
Adopting on existing project |
envshield check <file> |
Validate local env against schema | "Is my .env valid?" |
envshield scan [paths] |
Find secrets & undeclared variables | CI gate, pre-commit |
envshield setup |
Interactive onboarding wizard | New dev on the team |
envshield generate |
Compile schema into typed config code | Generate config.py or config.ts |
envshield schema sync |
Regenerate .env.example from schema |
Keep docs fresh |
envshield doctor |
Health check your setup | "Is everything wired up?" |
envshield install-hook |
Install Git pre-commit hook | CI/security integration |
All commands support --service <name> for multi-service projects.
Real-Life Example: A Multi-Service Monorepo
Before EnvShield
services/
├── api/
│ ├── .env (has DATABASE_URL, missing STRIPE_API_KEY)
│ └── config/app_config.py (uses getenv, untyped)
├── web/
│ ├── .env.example (outdated, has LEGACY_VAR)
│ └── config/.env (fresh, correct)
└── worker/
└── (no documented env vars at all)
Problem: Is DATABASE_URL the same across all three? Nobody knows.
New dev: "I set up my .env but the API won't start."
→ After 2 hours: missing STRIPE_API_KEY (only used in API, not documented)
After EnvShield
# Phase 1: Import existing configs (30 seconds)
envshield import services/api/.env --service api
envshield import services/web/.env --service web
envshield import services/worker/config.py --service worker
# Now we have:
services/
├── api/env.schema.toml (5 variables, all documented)
├── web/env.schema.toml (5 variables, all documented)
└── worker/env.schema.toml (3 variables, all documented)
# Phase 2: Sync documentation (5 seconds)
envshield schema sync --service api
envshield schema sync --service web
envshield schema sync --service worker
# → .env.example files regenerated, always in sync
# Phase 3: Onboarding (2 minutes instead of 2 hours)
# New dev clones repo
envshield setup
# ? Which service? (api / web / worker / all)
# → New dev runs through *all three* services interactively
# → Gets descriptions of what each variable is for
# → Passwords are prompted as hidden input
# → Done
# Phase 4: Prevent drift
# Someone adds ANALYTICS_KEY to API mid-sprint
envshield scan --staged
# ✗ Undeclared variable: ANALYTICS_KEY (used in code, not in schema)
# Commit aborted. Update env.schema.toml first.
envshield doctor
# ✓ Configuration Files
# ✗ Example File Sync: api/.env.example missing ANALYTICS_KEY
# → Run `envshield schema sync --service api` to fix
How It Compares
| Problem | EnvShield | Gitleaks | dotenvx | Infisical | direnv |
|---|---|---|---|---|---|
| Prevent secrets in commits | ✅ | ✅ Better detection | ✅ | ❌ (stores them) | ❌ |
| Validate .env against schema | ✅ | ❌ | ✅ | ✅ | ❌ |
| Generate typed config code | ✅ | ❌ | ❌ | ❌ | ❌ |
| Onboarding wizard | ✅ | ❌ | ❌ | ✅ Cloud-only | ❌ |
| Multi-service support | ✅ Built-in | ❌ | ❌ | ❌ | ❌ |
| Works offline | ✅ | ✅ | ✅ | ❌ | ✅ |
| Single source of truth | ✅ | ❌ | ❌ | ✅ Cloud | ❌ |
The difference: Gitleaks detects secrets. dotenvx encrypts files. Infisical stores secrets in the cloud. EnvShield takes a different angle — one schema that drives docs, validation, onboarding, and typed code generation, for one service or many.
Roadmap
Phase 1 (Free) ✅ Live now
- Multi-service schema management
- Schema validation & syncing
- Typed config generation (Python + TypeScript)
- Secret scanning & pre-commit hook
- Interactive onboarding
Phase 2 (Paid tier, coming soon)
- Config profiles (dev/staging/prod schemas with environment-specific overrides)
- Schema diffing ("What changed between staging and prod?")
- CI/CD integration (validate config before deploy)
- Team workspaces & change notifications
- Auto-generated team documentation
Phase 3 (Enterprise, future)
- Cloud config backend (optional)
- Vault integration (pull from HashiCorp, AWS Secrets)
- Audit logs & advanced RBAC
- Policy engine (enforce naming conventions, require descriptions)
- Deployment automation
Installation & Next Steps
pip install envshield
envshield --help
# Read the docs
open https://docs.envshield.dev/
Community
Questions? Ideas? Found a bug?
- 🐙 GitHub Issues
- 💬 GitHub Discussions
- 🌐 Website
- 🐍 PyPI
License
MIT — Use it freely, in any project.
TL;DR: EnvShield stops configuration chaos. One schema, multiple services, always in sync. Migrate existing projects in seconds. Generate typed config code. Onboard devs in minutes, not hours. All free, all local, all in your git repo.
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 envshield-4.1.0.tar.gz.
File metadata
- Download URL: envshield-4.1.0.tar.gz
- Upload date:
- Size: 76.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb02cde4f59026e699a4624b8c9efecea6a897b5a03284d7e8035f7a012facd4
|
|
| MD5 |
fe66ba9efbc7488de7876728cbcc040c
|
|
| BLAKE2b-256 |
4272dc601029097c3e844fbd28ba72b33fb568c36f142f0867450df9bd9f0f3c
|
Provenance
The following attestation bundles were made for envshield-4.1.0.tar.gz:
Publisher:
publish.yml on rabbilyasar/envshield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
envshield-4.1.0.tar.gz -
Subject digest:
eb02cde4f59026e699a4624b8c9efecea6a897b5a03284d7e8035f7a012facd4 - Sigstore transparency entry: 2339881880
- Sigstore integration time:
-
Permalink:
rabbilyasar/envshield@f281a21d82a8ea9c2de882509b8907596051d7f1 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/rabbilyasar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f281a21d82a8ea9c2de882509b8907596051d7f1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file envshield-4.1.0-py3-none-any.whl.
File metadata
- Download URL: envshield-4.1.0-py3-none-any.whl
- Upload date:
- Size: 86.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
52af282243c3d7be843f21fbcef837467a9ddce713296e3891d1b75c2752319d
|
|
| MD5 |
216173d1fae3f6a14ce08c75de9556e8
|
|
| BLAKE2b-256 |
b4cc61499b64962cf7c738004c046e000620b04f8074e82a515373fbff010ecd
|
Provenance
The following attestation bundles were made for envshield-4.1.0-py3-none-any.whl:
Publisher:
publish.yml on rabbilyasar/envshield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
envshield-4.1.0-py3-none-any.whl -
Subject digest:
52af282243c3d7be843f21fbcef837467a9ddce713296e3891d1b75c2752319d - Sigstore transparency entry: 2339881925
- Sigstore integration time:
-
Permalink:
rabbilyasar/envshield@f281a21d82a8ea9c2de882509b8907596051d7f1 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/rabbilyasar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f281a21d82a8ea9c2de882509b8907596051d7f1 -
Trigger Event:
push
-
Statement type: