Skip to main content

EnvShield 🛡️ – Configuration Orchestration for Multi-Service Projects

CI PyPI version License: MIT Downloads Website Stars

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") returns None

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_HOST to DATABASE_HOST → breaks 3 services before anyone notices
  • .env.example drifts from reality → someone commits a secret, doesn't catch it, pushes to prod
  • CI fails because STRIPE_API_KEY isn'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:

  1. 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
    
  2. 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
    
  3. 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 doctor if schemas actually changed
  • Single-Step Integration: Hooks installed automatically during init or service 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.

Multi-service configuration

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.

Importing existing config

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:

Interactive setup wizard

🛡️  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:

Health check catching drift

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:

Secret and undeclared variable detection

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?


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

envshield-4.1.0.tar.gz (76.4 kB view details)

Uploaded Source

Built Distribution

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

envshield-4.1.0-py3-none-any.whl (86.4 kB view details)

Uploaded Python 3

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

Hashes for envshield-4.1.0.tar.gz
Algorithm Hash digest
SHA256 eb02cde4f59026e699a4624b8c9efecea6a897b5a03284d7e8035f7a012facd4
MD5 fe66ba9efbc7488de7876728cbcc040c
BLAKE2b-256 4272dc601029097c3e844fbd28ba72b33fb568c36f142f0867450df9bd9f0f3c

See more details on using hashes here.

Provenance

The following attestation bundles were made for envshield-4.1.0.tar.gz:

Publisher: publish.yml on rabbilyasar/envshield

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for envshield-4.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 52af282243c3d7be843f21fbcef837467a9ddce713296e3891d1b75c2752319d
MD5 216173d1fae3f6a14ce08c75de9556e8
BLAKE2b-256 b4cc61499b64962cf7c738004c046e000620b04f8074e82a515373fbff010ecd

See more details on using hashes here.

Provenance

The following attestation bundles were made for envshield-4.1.0-py3-none-any.whl:

Publisher: publish.yml on rabbilyasar/envshield

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

4.7.1

2 files

4.7.0

2 files

4.6.2

2 files

4.6.1

2 files

4.6.0

2 files

4.5.1

2 files

4.5.0

2 files

4.4.0

2 files

4.3.0

2 files

4.2.0

2 files

This release

4.1.0 This release

2 files

4.0.1

2 files

4.0.0

2 files

3.1.1

2 files

3.1.0

2 files

3.0.0

2 files

2.1.0

2 files

2.0.1

2 files

2.0.0

2 files

1.4.0

2 files

1.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page