
Env Manager
- 233 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
Define, validate, and sync environment variables across local, staging, and production without leaking secrets in repos.
About
Environment variable management for multi-stage apps: naming conventions, validation, per-environment overrides, secret-manager and CI injection, and checks that prevent committing credentials while keeping local, staging, and prod configs aligned.
- .env schema and validation
- Per-environment overrides
- Secret manager sync
- CI/CD variable injection
- Leak prevention checks
Env Manager by the numbers
- 233 all-time installs (skills.sh)
- Ranked #372 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill env-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 233 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
What it does
Define, validate, and sync environment variables across local, staging, and production without leaking secrets in repos.
Files
Environment Variable Manager (env-manager)
Comprehensive environment variable validation, security scanning, and management for modern web applications.
  
Overview
The env-manager skill provides systematic environment variable management across local development, CI/CD pipelines, and deployment platforms. It prevents common issues like missing variables, exposed secrets, and framework-specific configuration errors.
Key Features:
- Framework-Aware Validation: Next.js, Vite, React, Node.js, Flask support
- Security-First: Never logs secrets, detects exposed credentials
- Platform Integration: Ready for Vercel, Railway, Heroku, and CI/CD
- Fast: Validates 1000 variables in 0.025s (80x faster than 2s target)
- Zero Dependencies: Pure Python, works anywhere
Why Use env-manager?
Common problems this solves:
- "Works on my machine, but not in production" (missing env vars)
- Accidentally exposing API keys in client-side code (NEXT_PUBLIC_ with secrets)
- Missing required variables during deployment
- Inconsistent .env files across team members
- No documentation of required environment variables
- Security vulnerabilities from exposed secrets
Quick Start
Installation
No installation needed! env-manager is a bundled skill in Claude MPM.
Requirements:
- Python 3.7+
- No external dependencies
5-Minute Quick Start
# 1. Validate your .env file
python3 scripts/validate_env.py .env
# 2. Check for framework-specific issues (Next.js example)
python3 scripts/validate_env.py .env --framework nextjs
# 3. Compare with .env.example to find missing vars
python3 scripts/validate_env.py .env --compare-with .env.example
# 4. Generate .env.example for documentation
python3 scripts/validate_env.py .env --generate-example .env.example
# 5. Get JSON output for CI/CD integration
python3 scripts/validate_env.py .env --jsonThat's it! Environment variables are now validated professionally.
Usage Examples
Basic Validation
Validate a .env file for structural issues:
python3 scripts/validate_env.py .envWhat it checks:
- Valid key=value format
- No duplicate keys
- Proper naming conventions (UPPERCASE_WITH_UNDERSCORES)
- No empty values (unless explicitly allowed)
- Proper quoting for values with spaces
Example output:
✅ Validation successful!
- 15 variables validated
- 0 errors
- 0 warningsFramework-Specific Validation
Next.js
Validate Next.js environment variables:
python3 scripts/validate_env.py .env.local --framework nextjsNext.js-specific checks:
- NEXT_PUBLIC_* variables are client-exposed (warns if secrets detected)
- .env.local, .env.production, .env file hierarchy
- Detects secrets in client-side variables
Example:
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_API_KEY=secret123 # ⚠️ WARNING: Secret in client-exposed variable!
DATABASE_URL=postgresql://... # ✅ Server-side onlyVite
python3 scripts/validate_env.py .env --framework viteVite-specific checks:
- VITE_* variables are client-exposed
- Warns if secrets detected in VITE_ prefixed vars
React (Create React App)
python3 scripts/validate_env.py .env --framework reactReact-specific checks:
- REACT_APP_* variables are client-exposed
- Warns if secrets in REACT_APP_ prefixed vars
Node.js/Express
python3 scripts/validate_env.py .env --framework nodejsNode.js-specific checks:
- Common NODE_ENV, PORT, DATABASE_URL patterns
- Standard Node.js conventions
Flask/Python
python3 scripts/validate_env.py .env --framework flaskFlask-specific checks:
- FLASK_APP, FLASK_ENV variables
- SQLAlchemy DATABASE_URL format
Comparing with .env.example
Ensure your .env has all required variables:
python3 scripts/validate_env.py .env --compare-with .env.exampleWhat it checks:
- All variables in .env.example exist in .env
- No extra undocumented variables in .env
Example output:
❌ Missing variables:
- DATABASE_URL (required in .env.example)
- STRIPE_SECRET_KEY (required in .env.example)
⚠️ Extra variables not in .env.example:
- DEBUG_MODE (consider adding to .env.example)Perfect for:
- Onboarding new team members
- CI/CD validation
- Deployment pre-checks
Generating .env.example
Create documentation for your environment variables:
python3 scripts/validate_env.py .env --generate-example .env.exampleWhat it does:
- Reads your .env file
- Sanitizes secret values (replaces with placeholders)
- Generates .env.example with safe defaults
Example:
# Input: .env
DATABASE_URL=postgresql://user:pass@localhost/db # pragma: allowlist secret
STRIPE_SECRET_KEY=sk_live_abc123xyz
NEXT_PUBLIC_API_URL=https://api.example.com
# Output: .env.example
DATABASE_URL=postgresql://user:password@localhost/dbname # pragma: allowlist secret
STRIPE_SECRET_KEY=your_stripe_secret_key_here
NEXT_PUBLIC_API_URL=https://api.example.comSecurity note: env-manager detects common secret patterns and replaces them with safe placeholders.
CI/CD Integration
Get machine-readable JSON output for automated workflows:
python3 scripts/validate_env.py .env.example --strict --jsonJSON output format:
{
"valid": true,
"errors": [],
"warnings": [],
"stats": {
"total_vars": 15,
"errors": 0,
"warnings": 0
}
}Exit codes:
0: Validation passed1: Validation errors found2: Missing required file3: Warnings found (only in --strict mode)
GitHub Actions example:
name: Validate Environment Variables
on: [push, pull_request]
jobs:
validate-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate .env.example
run: |
python3 scripts/validate_env.py .env.example --strict --json
working-directory: ./path/to/skill
- name: Check for framework-specific issues
run: |
python3 scripts/validate_env.py .env.example --framework nextjs --json
working-directory: ./path/to/skillStrict Mode
Treat warnings as errors (useful for CI/CD):
python3 scripts/validate_env.py .env --strictWhen to use:
- Pre-deployment validation
- CI/CD pipelines
- Release gates
- Team standard enforcement
Quiet Mode
Show only errors, suppress warnings:
python3 scripts/validate_env.py .env --quietWhen to use:
- You've already reviewed warnings
- Automated scripts that only care about errors
- Noisy environments where warnings are distracting
Supported Frameworks
| Framework | Prefix | Client-Exposed | Notes |
|---|---|---|---|
| Next.js | NEXT_PUBLIC_* | Yes | Auto-exposed in browser |
| Vite | VITE_* | Yes | Bundled into client code |
| React (CRA) | REACT_APP_* | Yes | Embedded in production build |
| Node.js | N/A | No | Server-side only |
| Flask | N/A | No | Server-side only |
Security warning: Never put secrets in client-exposed variables (NEXT_PUBLIC_, VITE_, REACT_APP_). env-manager will warn you if it detects common secret patterns.
CLI Reference
Command Structure
python3 scripts/validate_env.py <file> [options]Options
| Option | Description | Example |
|---|---|---|
--compare-with FILE | Compare with .env.example | --compare-with .env.example |
| `--framework {nextjs\ | vite\ | react\ |
--strict | Treat warnings as errors | --strict |
--json | JSON output for automation | --json |
--quiet | Only show errors | --quiet |
--generate-example OUTPUT | Generate .env.example | --generate-example .env.example |
Exit Codes
| Code | Meaning | When |
|---|---|---|
0 | Success | No errors (warnings OK unless --strict) |
1 | Validation errors | Structural issues, duplicates, etc. |
2 | File not found | Specified .env file doesn't exist |
3 | Warnings in strict mode | Warnings exist and --strict enabled |
Common Use Cases
Scenario 1: New Developer Onboarding
# New developer clones repo
git clone <repo>
cd <project>
# Copy example and fill in values
cp .env.example .env
# Edit .env with actual values...
# Validate setup
python3 scripts/validate_env.py .env --compare-with .env.example
# If missing variables, fix them
# Validation passes ✅Scenario 2: Pre-Deployment Check
# Before deploying to Vercel/Railway/Heroku
python3 scripts/validate_env.py .env.production --framework nextjs --strict
# Fix any errors
# Deploy with confidence ✅Scenario 3: Security Audit
# Check for accidentally exposed secrets
python3 scripts/validate_env.py .env.local --framework nextjs
# Look for warnings like:
# ⚠️ NEXT_PUBLIC_STRIPE_SECRET: Contains potential secret in client-exposed variableScenario 4: Team Documentation
# After adding new environment variable
echo "NEW_API_KEY=abc123" >> .env
# Regenerate .env.example
python3 scripts/validate_env.py .env --generate-example .env.example
# Commit updated .env.example
git add .env.example
git commit -m "docs: add NEW_API_KEY to environment variables"Scenario 5: CI/CD Quality Gate
# In your CI pipeline
- name: Validate environment configuration
run: |
python3 scripts/validate_env.py .env.example --strict --json > validation.json
# Fail pipeline if validation fails
if [ $? -ne 0 ]; then
cat validation.json
exit 1
fiPerformance
env-manager is designed for speed:
Benchmarks:
- Validates 1000 variables in 0.025s
- 80x faster than 2s target
- Zero external dependencies
- Minimal memory footprint
Why it matters:
- Fast feedback during development
- No CI/CD slowdown
- Works in resource-constrained environments
Security Notes
Critical security features:
1. Never Logs Secrets: env-manager NEVER displays actual secret values in output 2. Client-Exposure Detection: Warns when secrets are in NEXT_PUBLIC_, VITE_, REACT_APP_ variables 3. Secret Sanitization: When generating .env.example, replaces secrets with safe placeholders 4. No Network Calls: All validation is local, no data leaves your machine
Security-audited: This skill has undergone security review. See references/security.md for details.
Best practices:
- Never commit .env files with secrets
- Always use .env.example for documentation
- Use platform secret managers (Vercel, Railway, etc.) for production
- Validate before every deployment
- Run security scan regularly
Common Issues
"Missing equals sign" error
Cause: Line in .env doesn't have = separator
Fix:
# ❌ Bad
API_KEY
# ✅ Good
API_KEY=your_key_here"Duplicate key" error
Cause: Same variable defined multiple times
Fix:
# ❌ Bad
API_KEY=value1
API_KEY=value2
# ✅ Good
API_KEY=value2"Invalid variable name" warning
Cause: Variable name doesn't follow UPPERCASE_WITH_UNDERSCORES convention
Fix:
# ❌ Bad
apiKey=value
api-key=value
# ✅ Good
API_KEY=value"Potential secret in client-exposed variable" warning
Cause: NEXT_PUBLIC_, VITE_, or REACT_APP_ variable contains secret-like value
Fix:
# ❌ Bad (secret exposed to client!)
NEXT_PUBLIC_STRIPE_SECRET=sk_live_abc123
# ✅ Good (server-side only)
STRIPE_SECRET_KEY=sk_live_abc123
NEXT_PUBLIC_STRIPE_PUBLISHABLE=pk_live_xyz789"Empty value" warning
Cause: Variable has no value
Fix:
# ❌ Bad
DATABASE_URL=
# ✅ Good (if optional, document it)
DATABASE_URL= # Optional, uses SQLite if not set
# ✅ Better
DATABASE_URL=postgresql://localhost/mydbFile not found error
Cause: Specified .env file doesn't exist
Fix:
# Check file exists
ls -la .env
# Or create it
touch .envTroubleshooting
Validation passes locally but fails in CI
Check: 1. Line endings (CRLF vs LF) 2. File encoding (UTF-8 expected) 3. File permissions 4. Python version (3.7+ required)
Warnings about client-exposed variables
This is intentional! env-manager is warning you that variables like NEXT_PUBLIC_API_KEY will be visible in the browser.
Options: 1. Move secret to server-side variable (remove NEXT_PUBLIC_ prefix) 2. Use public/publishable keys only in client-exposed variables 3. If truly not a secret, ignore the warning
.env.example generation replaces too much
env-manager is conservative about secrets. If it over-sanitizes: 1. Manually edit .env.example after generation 2. Use specific placeholder values in .env that won't trigger sanitization
Advanced Usage
Custom Validation Patterns
See references/validation.md for advanced validation patterns.
Platform-Specific Deployment
See references/synchronization.md for Vercel, Railway, Heroku integration patterns.
Framework-Specific Guides
See references/frameworks.md for comprehensive framework guides.
Related Documentation
- [Validation Reference](references/validation.md): Complete validation workflows and checks
- [Security Reference](references/security.md): Secret scanning and security patterns
- [Synchronization Reference](references/synchronization.md): Platform sync patterns (coming soon)
- [Frameworks Reference](references/frameworks.md): Framework-specific patterns and conventions
- [Troubleshooting Guide](references/troubleshooting.md): Common issues and solutions
Integration with Claude MPM
env-manager is a bundled skill in Claude MPM. Agents can use it for:
- Pre-deployment validation
- Security scanning
- Environment setup verification
- Documentation generation
See INTEGRATION.md for agent integration patterns.
Contributing
env-manager follows Claude MPM contribution guidelines:
1. Run make lint-fix during development 2. Run make quality before commits 3. Add tests for new features (85%+ coverage required) 4. Update documentation
See CONTRIBUTING.md for details.
License
MIT License - Part of Claude MPM project
Support
- Issues: Report bugs via GitHub Issues
- Documentation: See references/ directory
- Examples: See examples/ directory
- Integration: See INTEGRATION.md
---
Version: 1.0.0 Status: Stable, Security-Audited Test Coverage: 85%+ Performance: 80x faster than target
{
"name": "env-manager",
"version": "1.0.0",
"category": "universal",
"toolchain": null,
"framework": null,
"tags": [
"performance",
"api",
"security",
"testing",
"debugging"
],
"entry_point_tokens": 65,
"full_tokens": 17364,
"author": "bobmatnyc",
"license": "MIT",
"requires": [],
"updated": "2025-11-21",
"source_path": "infrastructure/env-manager/README.md",
"source": "https://github.com/bobmatnyc/claude-mpm",
"created": "2025-11-21",
"modified": "2025-11-21",
"maintainer": "Claude MPM Team",
"attribution_required": true,
"repository": "https://github.com/bobmatnyc/claude-mpm-skills"
}
Framework-Specific Environment Patterns
Part of: env-manager
Category: infrastructure
Reading Level: Intermediate
Purpose
Complete patterns for framework-specific environment variable handling: Next.js, Express, Flask, Django, Vite, and Create React App.
Framework Detection
Auto-Detection Logic
from pathlib import Path
from typing import Optional
def detect_framework(project_dir: Path) -> Optional[str]:
"""Auto-detect framework from project structure."""
detection_patterns = {
'nextjs': ['next.config.js', 'next.config.mjs', 'next.config.ts'],
'vite': ['vite.config.js', 'vite.config.ts'],
'react': ['react-scripts', 'craco.config.js'],
'express': ['express', 'app.js', 'server.js'],
'flask': ['app.py', 'wsgi.py', 'requirements.txt'],
'django': ['manage.py', 'settings.py'],
'fastapi': ['main.py', 'fastapi']
}
# Check package.json for JS frameworks
package_json = project_dir / 'package.json'
if package_json.exists():
import json
with open(package_json) as f:
data = json.load(f)
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
if 'next' in deps:
return 'nextjs'
elif 'vite' in deps:
return 'vite'
elif 'react-scripts' in deps:
return 'react'
elif 'express' in deps:
return 'express'
# Check for Python frameworks
requirements = project_dir / 'requirements.txt'
if requirements.exists():
content = requirements.read_text().lower()
if 'flask' in content:
return 'flask'
elif 'django' in content:
return 'django'
elif 'fastapi' in content:
return 'fastapi'
# Check for specific files
for framework, patterns in detection_patterns.items():
for pattern in patterns:
if (project_dir / pattern).exists():
return framework
return 'generic'Next.js
File Precedence
Next.js loads env files in this order (higher precedence first):
1. .env.$(NODE_ENV).local (e.g., .env.production.local)
2. .env.local (always, except in test)
3. .env.$(NODE_ENV) (e.g., .env.production)
4. .envPublic vs Private Variables
Client-Side (Public):
# Prefix with NEXT_PUBLIC_ for browser access
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=UA-123456789
NEXT_PUBLIC_SITE_NAME=My AppServer-Side (Private):
# No prefix - only available server-side
DATABASE_URL=postgres://localhost:5432/mydb
JWT_SECRET=super-secret-key-never-expose
API_SECRET_KEY=sk_live_abc123Validation Rules
def validate_nextjs_env(env_file: Path) -> Dict:
"""Validate Next.js environment variables."""
errors = []
warnings = []
vars_dict = parse_env_file(env_file)
for key, value in vars_dict.items():
# Check for secrets in public vars
if key.startswith('NEXT_PUBLIC_'):
secret_indicators = ['secret', 'key', 'password', 'token', 'private']
if any(indicator in key.lower() for indicator in secret_indicators):
errors.append({
'key': key,
'error': 'SECURITY: Secret in NEXT_PUBLIC_ variable (exposed to browser)'
})
# Check for API endpoints without NEXT_PUBLIC_
if 'api' in key.lower() and 'url' in key.lower():
if not key.startswith('NEXT_PUBLIC_'):
warnings.append({
'key': key,
'warning': 'API URL without NEXT_PUBLIC_ prefix (not accessible client-side)'
})
return {'errors': errors, 'warnings': warnings}File Structure Example
# .env (committed - shared defaults)
NEXT_PUBLIC_APP_NAME=My App
DATABASE_URL=postgres://localhost:5432/dev
# .env.local (gitignored - local overrides)
DATABASE_URL=postgres://localhost:5432/mylocal
JWT_SECRET=dev-jwt-secret-123
# .env.production (committed - production defaults)
NEXT_PUBLIC_API_URL=https://api.example.com
# .env.production.local (gitignored - production secrets)
DATABASE_URL=postgres://prod.example.com:5432/prod
JWT_SECRET=prod-jwt-secret-xyzExpress/Node.js
Standard Variables
# Node environment
NODE_ENV=development # or production, test
# Server configuration
PORT=3000
HOST=localhost
# Database
DATABASE_URL=postgres://localhost:5432/mydb
# Security
JWT_SECRET=your-secret-key
SESSION_SECRET=session-secret
# External services
REDIS_URL=redis://localhost:6379
SMTP_HOST=smtp.example.com
SMTP_PORT=587Validation Rules
NODE_STANDARD_VARS = {
'NODE_ENV': ['development', 'production', 'test'],
'PORT': lambda v: v.isdigit() and 1 <= int(v) <= 65535,
'DATABASE_URL': lambda v: v.startswith(('postgres://', 'mysql://', 'mongodb://'))
}
def validate_nodejs_env(env_file: Path) -> List[Dict]:
"""Validate Node.js environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
for key, validator in NODE_STANDARD_VARS.items():
if key in vars_dict:
value = vars_dict[key]
if isinstance(validator, list):
if value not in validator:
errors.append({
'key': key,
'error': f'Invalid value "{value}", expected one of {validator}'
})
elif callable(validator):
if not validator(value):
errors.append({
'key': key,
'error': 'Value validation failed'
})
return errorsFlask/Python
Standard Variables
# Flask configuration
FLASK_APP=app.py
FLASK_ENV=development # or production
FLASK_DEBUG=1
# Database (SQLAlchemy)
DATABASE_URL=postgresql://localhost/mydb
SQLALCHEMY_DATABASE_URI=postgresql://localhost/mydb
# Security
SECRET_KEY=your-secret-key-here
WTF_CSRF_SECRET_KEY=csrf-secret
# External services
REDIS_URL=redis://localhost:6379
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587Python-dotenv Loading
# Load environment in Flask app
from dotenv import load_dotenv
import os
# Load .env file
load_dotenv()
# Access variables
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
app.config['DATABASE_URL'] = os.getenv('DATABASE_URL')Validation Rules
FLASK_STANDARD_VARS = {
'FLASK_APP': lambda v: v.endswith('.py'),
'FLASK_ENV': ['development', 'production'],
'FLASK_DEBUG': ['0', '1', 'true', 'false']
}
def validate_flask_env(env_file: Path) -> List[Dict]:
"""Validate Flask environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
# Check required Flask vars
required = ['FLASK_APP', 'SECRET_KEY']
for var in required:
if var not in vars_dict:
errors.append({
'key': var,
'error': f'Required Flask variable missing'
})
# Validate present vars
for key, validator in FLASK_STANDARD_VARS.items():
if key in vars_dict:
value = vars_dict[key]
if isinstance(validator, list) and value not in validator:
errors.append({
'key': key,
'error': f'Invalid value, expected one of {validator}'
})
elif callable(validator) and not validator(value):
errors.append({
'key': key,
'error': 'Value validation failed'
})
return errorsDjango
Standard Variables
# Django configuration
DJANGO_SETTINGS_MODULE=myproject.settings
DJANGO_SECRET_KEY=your-secret-key
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
# Database
DATABASE_URL=postgres://localhost:5432/mydb
# Static/Media files
STATIC_ROOT=/var/www/static
MEDIA_ROOT=/var/www/media
# Security
CSRF_TRUSTED_ORIGINS=https://example.comSettings.py Integration
# settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# Load .env
load_dotenv()
# Use env variables
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '').split(',')Vite
Environment Variable Access
Client-Side Variables:
# Must prefix with VITE_ for access
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
VITE_ENABLE_ANALYTICS=trueUsage in Code:
// Access via import.meta.env
const apiUrl = import.meta.env.VITE_API_URL;
const appTitle = import.meta.env.VITE_APP_TITLE;Validation Rules
def validate_vite_env(env_file: Path) -> List[Dict]:
"""Validate Vite environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
for key, value in vars_dict.items():
# Check for secrets in VITE_ vars
if key.startswith('VITE_'):
if any(s in key.lower() for s in ['secret', 'key', 'password', 'token']):
errors.append({
'key': key,
'error': 'SECURITY: Secret in VITE_ variable (exposed to browser)'
})
# Warn about non-VITE_ vars (won't be accessible)
elif not key in ['NODE_ENV', 'PORT']:
errors.append({
'key': key,
'warning': f'{key} not prefixed with VITE_ (not accessible in client code)'
})
return errorsCreate React App
Environment Variable Access
Client-Side Variables:
# Must prefix with REACT_APP_ for access
REACT_APP_API_URL=https://api.example.com
REACT_APP_AUTH0_DOMAIN=example.auth0.com
REACT_APP_ENABLE_ANALYTICS=trueBuilt-in Variables:
NODE_ENV=development # Set by CRA automatically
PUBLIC_URL=/ # Public URL of the appUsage in Code:
// Access via process.env
const apiUrl = process.env.REACT_APP_API_URL;
const domain = process.env.REACT_APP_AUTH0_DOMAIN;Framework Comparison Table
| Framework | Client Prefix | Server Access | File Precedence | Auto-Reload |
|---|---|---|---|---|
| Next.js | NEXT_PUBLIC_ | All vars | Complex (4 files) | Dev only |
| Vite | VITE_ | All vars | .env.local > .env | Dev only |
| CRA | REACT_APP_ | All vars | .env.local > .env | Requires restart |
| Express | N/A | All vars | .env only | With nodemon |
| Flask | N/A | All vars | .env only | With debug mode |
| Django | N/A | All vars | .env only | With runserver |
Summary
Framework Detection:
- Auto-detect from package.json, requirements.txt, or config files
- Support explicit --framework override
Key Patterns:
- ✅ Next.js: NEXT_PUBLIC_ for client, file precedence critical
- ✅ Express: Standard NODE_ENV, PORT, DATABASE_URL
- ✅ Flask: FLASK_APP, FLASK_ENV, SECRET_KEY required
- ✅ Django: DJANGO_SETTINGS_MODULE, DJANGO_SECRET_KEY
- ✅ Vite: VITE_ prefix for client access
- ✅ CRA: REACT_APP_ prefix for client access
Security Rules:
- Never put secrets in client-exposed vars (NEXT_PUBLIC_, VITE_, REACT_APP_)
- Validate format of framework-specific vars
- Check for required variables per framework
Related References
- Validation: General validation workflows
- Security: Secret protection patterns
- Troubleshooting: Framework-specific issues
--- Lines: 279 ✓ 200-280 range
Environment Security Patterns
Part of: env-manager
Category: infrastructure
Reading Level: Advanced
Purpose
Comprehensive security patterns for environment variables: secret detection, exposure scanning, git history validation, and format verification.
Security Principles
Never Log Secrets
Critical Rule: NEVER log, print, or display actual secret values in any output.
# ❌ NEVER DO THIS
print(f"API_KEY: {api_key}")
logging.info(f"Database password: {db_pass}")
error(f"Failed to connect with {credentials}")
# ✅ ALWAYS DO THIS
print(f"API_KEY: {'*' * len(api_key)}")
logging.info(f"Database credentials present: {bool(db_pass)}")
error(f"Failed to connect (credentials masked)")Defense in Depth
Multiple layers of secret protection: 1. Prevention: .gitignore, pre-commit hooks 2. Detection: Pattern scanning, entropy analysis 3. Response: Rotation procedures, incident handling 4. Audit: Git history scanning, access logs
Secret Pattern Detection
Common Secret Patterns
AWS Credentials:
AWS_PATTERNS = {
'aws_access_key': re.compile(r'AKIA[0-9A-Z]{16}'),
'aws_secret_key': re.compile(r'[0-9a-zA-Z/+=]{40}'),
'aws_account_id': re.compile(r'\d{12}')
}GitHub Tokens:
GITHUB_PATTERNS = {
'personal_token': re.compile(r'ghp_[0-9a-zA-Z]{36}'),
'oauth_token': re.compile(r'gho_[0-9a-zA-Z]{36}'),
'app_token': re.compile(r'(ghu|ghs)_[0-9a-zA-Z]{36}')
}API Keys and Tokens:
GENERIC_PATTERNS = {
'jwt': re.compile(r'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'),
'slack': re.compile(r'xox[baprs]-[0-9A-Za-z-]{10,72}'),
'stripe': re.compile(r'sk_(test|live)_[0-9a-zA-Z]{24,}'),
'mailgun': re.compile(r'key-[0-9a-z]{32}'),
'twilio': re.compile(r'SK[0-9a-f]{32}')
}Entropy-Based Detection
High entropy strings often indicate secrets:
import math
from collections import Counter
def calculate_entropy(data: str) -> float:
"""Calculate Shannon entropy of a string."""
if not data:
return 0.0
entropy = 0
counter = Counter(data)
length = len(data)
for count in counter.values():
probability = count / length
entropy -= probability * math.log2(probability)
return entropy
def is_high_entropy_secret(value: str, threshold: float = 4.5) -> bool:
"""Check if value has high entropy (likely a secret)."""
# Skip short values
if len(value) < 20:
return False
# Calculate entropy
entropy = calculate_entropy(value)
# High entropy suggests random generation
return entropy > thresholdSecret Scanner Implementation
from pathlib import Path
from typing import List, Dict
import re
class SecretScanner:
"""Scan for exposed secrets in code and config files."""
def __init__(self):
self.patterns = {
**AWS_PATTERNS,
**GITHUB_PATTERNS,
**GENERIC_PATTERNS
}
def scan_file(self, file_path: Path) -> List[Dict]:
"""Scan a single file for secrets."""
findings = []
try:
with open(file_path) as f:
for line_num, line in enumerate(f, 1):
# Check against patterns
for secret_type, pattern in self.patterns.items():
matches = pattern.finditer(line)
for match in matches:
findings.append({
'file': str(file_path),
'line': line_num,
'type': secret_type,
'matched': self._mask_secret(match.group()),
'context': line[:50] + '...' if len(line) > 50 else line
})
# Check entropy
if '=' in line:
key, value = line.split('=', 1)
value = value.strip().strip('"\'')
if is_high_entropy_secret(value):
findings.append({
'file': str(file_path),
'line': line_num,
'type': 'high_entropy',
'key': key.strip(),
'entropy': calculate_entropy(value)
})
except Exception as e:
logging.error(f"Error scanning {file_path}: {e}")
return findings
def _mask_secret(self, secret: str) -> str:
"""Mask a secret for display."""
if len(secret) <= 4:
return '*' * len(secret)
return secret[:2] + '*' * (len(secret) - 4) + secret[-2:]Git History Scanning
Check for Historical Exposures
def scan_git_history(repo_path: Path, patterns: Dict) -> List[Dict]:
"""Scan git history for exposed secrets."""
try:
import git
except ImportError:
logging.warning("GitPython not installed, skipping history scan")
return []
findings = []
repo = git.Repo(repo_path)
# Scan last 100 commits
for commit in repo.iter_commits(max_count=100):
for file_path in commit.stats.files:
if file_path.endswith('.env'):
findings.append({
'commit': commit.hexsha[:8],
'file': file_path,
'author': commit.author.name,
'date': commit.committed_datetime,
'message': 'SECURITY: .env file in commit history'
})
return findingsGitignore Validation
Ensure Proper Gitignore Coverage
def validate_gitignore(project_dir: Path) -> Dict:
"""Validate .gitignore covers sensitive files."""
gitignore_path = project_dir / '.gitignore'
if not gitignore_path.exists():
return {
'valid': False,
'errors': ['.gitignore file not found']
}
required_patterns = [
'.env',
'.env.local',
'.env.*.local',
'*.env'
]
with open(gitignore_path) as f:
gitignore_content = f.read()
missing = []
for pattern in required_patterns:
if pattern not in gitignore_content:
missing.append(pattern)
# Check if any .env files are tracked
tracked_env_files = []
try:
import git
repo = git.Repo(project_dir)
for item in repo.tree().traverse():
if '.env' in item.path and not item.path.endswith('.example'):
tracked_env_files.append(item.path)
except:
pass
return {
'valid': len(missing) == 0 and len(tracked_env_files) == 0,
'missing_patterns': missing,
'tracked_env_files': tracked_env_files
}Format Validation
Validate Secret Formats
def validate_secret_formats(env_file: Path) -> List[Dict]:
"""Validate that secrets match expected formats."""
errors = []
format_rules = {
'DATABASE_URL': r'^(postgres|mysql|mongodb)://',
'JWT_SECRET': lambda v: len(v) >= 32,
'API_KEY': lambda v: len(v) >= 20,
'STRIPE_KEY': r'^sk_(test|live)_',
'AWS_ACCESS_KEY_ID': r'^AKIA[0-9A-Z]{16}$'
}
with open(env_file) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
value = value.strip().strip('"\'')
if key in format_rules:
rule = format_rules[key]
if callable(rule):
if not rule(value):
errors.append({
'line': line_num,
'key': key,
'error': f'{key} validation failed'
})
elif isinstance(rule, str):
if not re.match(rule, value):
errors.append({
'line': line_num,
'key': key,
'error': f'{key} format invalid'
})
return errorsSecurity Best Practices
Environment-Specific Secrets
Development:
# .env.local (gitignored, local development only)
DATABASE_URL=postgres://localhost:5432/dev
JWT_SECRET=dev-secret-not-for-productionProduction:
# Set via platform (Vercel, Railway, etc.)
# NEVER commit production secrets
DATABASE_URL=<from_secret_manager>
JWT_SECRET=<from_secret_manager>Secret Rotation Procedures
def check_secret_age(env_file: Path) -> Dict:
"""Check when secrets were last rotated."""
import os
from datetime import datetime, timedelta
file_modified = datetime.fromtimestamp(os.path.getmtime(env_file))
age_days = (datetime.now() - file_modified).days
recommendations = []
if age_days > 90:
recommendations.append({
'severity': 'warning',
'message': f'Secrets are {age_days} days old. Consider rotation.'
})
if age_days > 180:
recommendations.append({
'severity': 'error',
'message': f'Secrets are {age_days} days old. MUST rotate.'
})
return {
'last_modified': file_modified.isoformat(),
'age_days': age_days,
'recommendations': recommendations
}Incident Response
Secret Exposure Recovery
If secrets are exposed:
1. Immediate Actions:
# Revoke exposed credentials
# Rotate all affected secrets
# Check access logs for unauthorized use2. Git History Cleanup:
# Use BFG Repo-Cleaner to remove secrets from history
bfg --replace-text passwords.txt
git reflog expire --expire=now --all
git gc --prune=now --aggressive3. Platform Updates:
# Update all deployment platforms
python scripts/sync_secrets.py --platform vercel --sync
python scripts/sync_secrets.py --platform railway --syncValidation Error Messages
CRITICAL: Never Expose Values in Error Messages
Security Fix (2025-11-13): All validation error messages have been hardened to prevent accidental secret exposure.
Problem: Error messages that include actual variable values can leak secrets in:
- CI/CD logs
- Error tracking systems (Sentry, etc.)
- Terminal output screenshots
- Bug reports
Solution: Error messages NEVER include actual values, only validation criteria.
# ❌ NEVER DO THIS - Exposes actual value
f'Invalid value "{vars_dict["NODE_ENV"]}", expected one of {valid_values}'
# ✅ ALWAYS DO THIS - Safe message
f'Invalid value for NODE_ENV, expected one of {valid_values}'Validation Script Protection: The validate_env.py script has been hardened against value exposure:
- Line 365: NODE_ENV validation error message sanitized
- All error messages verified to exclude variable values
- Test coverage added:
test_no_secret_exposure_in_errors
Testing:
# Verify no secret exposure
echo 'NODE_ENV=sk-proj-fake-secret' > test.env
python validate_env.py test.env --framework nodejs
# Output: "Invalid value for NODE_ENV, expected..."
# NOT: "Invalid value 'sk-proj-fake-secret', expected..."Summary
Security Checklist:
- [ ] Never log actual secret values
- [ ] Never expose values in error messages
- [ ] .env files in .gitignore
- [ ] No secrets in git history
- [ ] Pattern-based scanning enabled
- [ ] Entropy analysis for unknowns
- [ ] Secret format validation
- [ ] Regular secret rotation (90 days)
- [ ] Incident response plan ready
Key Patterns:
- ✅ Pattern-based detection (AWS, GitHub, etc.)
- ✅ Entropy analysis for random secrets
- ✅ Git history scanning
- ✅ .gitignore validation
- ✅ Format validation
- ✅ Secret masking in output
Related References
- Validation: Environment validation workflows
- Synchronization: Secure platform sync
- Troubleshooting: Security issue recovery
--- Lines: 267 ✓ 200-280 range
Environment Synchronization Patterns
Part of: env-manager
Category: infrastructure
Reading Level: Advanced
Purpose
Platform synchronization workflows: comparing local vs deployed environments, generating platform configs, safe sync patterns, and secret manager integration.
Synchronization Principles
Safety First: Dry-Run Default
Always default to dry-run for safety:
def sync_to_platform(env_file: Path, platform: str, dry_run: bool = True):
"""Sync environment to platform. Defaults to dry-run."""
changes = calculate_changes(env_file, platform)
if dry_run:
print("🔍 DRY-RUN MODE: No changes will be applied")
print("\nProposed changes:")
display_changes(changes)
print("\nTo apply: add --confirm flag")
return
# Actual sync only if dry_run=False
apply_changes(changes, platform)Three-Way Comparison
Compare across local, platform, and secret manager:
Local (.env) ←→ Platform (Vercel/Railway) ←→ Secret Manager (1Password/AWS)Platform Comparison Workflows
Compare Local vs Platform
from typing import Dict, Set
from pathlib import Path
def compare_environments(
local_env: Path,
platform_env: Dict[str, str]
) -> Dict:
"""Compare local .env against platform environment."""
local_vars = parse_env_file(local_env)
return {
'only_local': set(local_vars.keys()) - set(platform_env.keys()),
'only_platform': set(platform_env.keys()) - set(local_vars.keys()),
'different_values': {
key for key in local_vars.keys() & platform_env.keys()
if local_vars[key] != platform_env[key]
},
'identical': {
key for key in local_vars.keys() & platform_env.keys()
if local_vars[key] == platform_env[key]
}
}Display Comparison Results
def display_comparison(comparison: Dict):
"""Display comparison in readable format."""
print("\n📊 Environment Comparison\n")
if comparison['only_local']:
print("⚠️ Variables only in LOCAL:")
for var in sorted(comparison['only_local']):
print(f" + {var}")
if comparison['only_platform']:
print("\n⚠️ Variables only in PLATFORM:")
for var in sorted(comparison['only_platform']):
print(f" - {var}")
if comparison['different_values']:
print("\n🔄 Variables with DIFFERENT values:")
for var in sorted(comparison['different_values']):
print(f" ≠ {var}")
if comparison['identical']:
print(f"\n✅ {len(comparison['identical'])} variables identical")Platform-Specific Integration
Vercel
Fetch Current Environment:
import requests
def fetch_vercel_env(project_id: str, token: str) -> Dict[str, str]:
"""Fetch environment variables from Vercel."""
url = f"https://api.vercel.com/v9/projects/{project_id}/env"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
response.raise_for_status()
# Vercel returns array of {key, value, target, type}
env_vars = {}
for var in response.json()['envs']:
if 'production' in var.get('target', []):
env_vars[var['key']] = var['value']
return env_varsGenerate Vercel Config:
def generate_vercel_config(env_file: Path) -> str:
"""Generate vercel.json env configuration."""
vars_dict = parse_env_file(env_file)
# Separate public vs private
public_vars = {k: v for k, v in vars_dict.items() if k.startswith('NEXT_PUBLIC_')}
private_vars = {k: v for k, v in vars_dict.items() if not k.startswith('NEXT_PUBLIC_')}
config = {
"env": {k: v for k, v in public_vars.items()},
"build": {
"env": {k: v for k, v in private_vars.items() if not is_secret(k)}
}
}
return json.dumps(config, indent=2)Sync to Vercel:
def sync_to_vercel(env_file: Path, project_id: str, token: str, dry_run: bool = True):
"""Sync environment variables to Vercel."""
local_vars = parse_env_file(env_file)
remote_vars = fetch_vercel_env(project_id, token)
changes = compare_environments(env_file, remote_vars)
if dry_run:
display_comparison(changes)
return
# Apply changes
url = f"https://api.vercel.com/v10/projects/{project_id}/env"
headers = {"Authorization": f"Bearer {token}"}
for key in changes['only_local']:
data = {
"key": key,
"value": local_vars[key],
"target": ["production"],
"type": "encrypted"
}
requests.post(url, headers=headers, json=data)
print("✅ Synced to Vercel")Railway
Fetch Railway Environment:
def fetch_railway_env(project_id: str, token: str) -> Dict[str, str]:
"""Fetch environment variables from Railway."""
# Railway uses GraphQL API
query = """
query($projectId: String!) {
project(id: $projectId) {
environments {
variables {
name
value
}
}
}
}
"""
response = requests.post(
"https://backboard.railway.app/graphql/v2",
headers={"Authorization": f"Bearer {token}"},
json={"query": query, "variables": {"projectId": project_id}}
)
data = response.json()
env_vars = {}
for var in data['data']['project']['environments'][0]['variables']:
env_vars[var['name']] = var['value']
return env_varsSync to Railway:
def sync_to_railway(env_file: Path, project_id: str, token: str, dry_run: bool = True):
"""Sync environment variables to Railway."""
local_vars = parse_env_file(env_file)
if dry_run:
print("🔍 DRY-RUN: Would sync to Railway:")
for key in local_vars:
print(f" {key}={mask_secret(local_vars[key])}")
return
# Use Railway CLI (more reliable than API)
import subprocess
for key, value in local_vars.items():
subprocess.run(
["railway", "variables", "set", f"{key}={value}"],
check=True
)
print("✅ Synced to Railway")Heroku
Sync to Heroku:
def sync_to_heroku(env_file: Path, app_name: str, dry_run: bool = True):
"""Sync environment variables to Heroku."""
import subprocess
local_vars = parse_env_file(env_file)
# Fetch current Heroku config
result = subprocess.run(
["heroku", "config", "--app", app_name, "--json"],
capture_output=True,
text=True
)
remote_vars = json.loads(result.stdout)
changes = compare_environments(env_file, remote_vars)
if dry_run:
display_comparison(changes)
return
# Apply changes using Heroku CLI
for key, value in local_vars.items():
subprocess.run(
["heroku", "config:set", f"{key}={value}", "--app", app_name],
check=True
)
print("✅ Synced to Heroku")Secret Manager Integration
1Password
Fetch from 1Password:
def fetch_from_1password(vault: str, item: str) -> Dict[str, str]:
"""Fetch secrets from 1Password CLI."""
import subprocess
import json
result = subprocess.run(
["op", "item", "get", item, "--vault", vault, "--format", "json"],
capture_output=True,
text=True,
check=True
)
data = json.loads(result.stdout)
env_vars = {}
for field in data['fields']:
if field['purpose'] == 'NOTES':
# Parse env format from notes
for line in field['value'].split('\n'):
if '=' in line:
key, value = line.split('=', 1)
env_vars[key.strip()] = value.strip()
else:
# Individual fields
env_vars[field['label'].upper()] = field['value']
return env_varsPush to 1Password:
def push_to_1password(env_file: Path, vault: str, item: str):
"""Push environment to 1Password."""
import subprocess
vars_dict = parse_env_file(env_file)
# Create notes field with all env vars
notes = '\n'.join([f"{k}={v}" for k, v in vars_dict.items()])
subprocess.run(
["op", "item", "edit", item, "--vault", vault, f"notes={notes}"],
check=True
)
print(f"✅ Pushed to 1Password vault '{vault}'")AWS Secrets Manager
Fetch from AWS:
def fetch_from_aws_secrets(secret_name: str, region: str = 'us-east-1') -> Dict[str, str]:
"""Fetch secrets from AWS Secrets Manager."""
import boto3
import json
client = boto3.client('secretsmanager', region_name=region)
response = client.get_secret_value(SecretId=secret_name)
# Secrets stored as JSON
return json.loads(response['SecretString'])Push to AWS:
def push_to_aws_secrets(env_file: Path, secret_name: str, region: str = 'us-east-1'):
"""Push environment to AWS Secrets Manager."""
import boto3
import json
vars_dict = parse_env_file(env_file)
client = boto3.client('secretsmanager', region_name=region)
try:
# Update existing secret
client.update_secret(
SecretId=secret_name,
SecretString=json.dumps(vars_dict)
)
except client.exceptions.ResourceNotFoundException:
# Create new secret
client.create_secret(
Name=secret_name,
SecretString=json.dumps(vars_dict)
)
print(f"✅ Pushed to AWS Secrets Manager: {secret_name}")Safe Sync Patterns
Three-Step Sync Process
def safe_sync_workflow(env_file: Path, platform: str):
"""Safe sync workflow with validation."""
# Step 1: Validate local environment
print("1️⃣ Validating local environment...")
validation = validate_structure(env_file)
if validation['errors']:
print("❌ Validation failed. Fix errors first.")
return
# Step 2: Dry-run comparison
print("\n2️⃣ Comparing with platform...")
comparison = compare_with_platform(env_file, platform)
display_comparison(comparison)
# Step 3: Confirm and sync
print("\n3️⃣ Ready to sync")
confirm = input("Apply changes? (yes/no): ")
if confirm.lower() != 'yes':
print("❌ Sync cancelled")
return
sync_to_platform(env_file, platform, dry_run=False)
print("✅ Sync complete")Rollback Procedures
def create_backup(platform: str, project_id: str) -> str:
"""Create backup before sync."""
import json
from datetime import datetime
timestamp = datetime.now().isoformat()
current_env = fetch_platform_env(platform, project_id)
backup_file = f".env.backup.{platform}.{timestamp}.json"
with open(backup_file, 'w') as f:
json.dump(current_env, f, indent=2)
return backup_file
def rollback(backup_file: str, platform: str, project_id: str):
"""Rollback to previous environment."""
import json
with open(backup_file) as f:
backup_env = json.load(f)
sync_dict_to_platform(backup_env, platform, project_id, dry_run=False)
print(f"✅ Rolled back to {backup_file}")Summary
Synchronization Workflow: 1. Validate: Check local .env structure 2. Compare: Dry-run comparison with platform 3. Review: Display proposed changes 4. Backup: Save current platform state 5. Sync: Apply changes with confirmation 6. Verify: Check platform reflects changes
Key Patterns:
- ✅ Always dry-run first
- ✅ Three-way comparison (local/platform/secret manager)
- ✅ Never auto-apply changes
- ✅ Create backups before sync
- ✅ Rollback capability
- ✅ Platform-specific handling
Supported Platforms:
- Vercel (API + CLI)
- Railway (GraphQL API + CLI)
- Heroku (CLI)
Supported Secret Managers:
- 1Password (CLI)
- AWS Secrets Manager (boto3)
Related References
- Validation: Environment validation
- Security: Secret protection during sync
- Troubleshooting: Sync issue resolution
--- Lines: 277 ✓ 200-280 range
Environment Troubleshooting Guide
Part of: env-manager
Category: infrastructure
Reading Level: Intermediate
Purpose
Solutions to common environment variable issues: "works locally, not in production", missing variables, framework-specific problems, and platform quirks.
Common Issues
Issue 1: Works Locally, Not in Production
Symptoms:
- Application works fine locally
- Fails or behaves incorrectly in production
- Error messages about missing or undefined variables
Root Causes:
A. Variables Not Synced to Platform
# Check local vs platform
python scripts/validate_env.py .env --compare-platform vercel
# Common issue: forgot to sync
python scripts/sync_secrets.py --platform vercel --sync --dry-runB. Wrong Environment File Loaded
# Next.js example
# Local: .env.local (gitignored, has all secrets)
# Production: Platform env vars (might be missing some)
# Solution: Ensure all vars from .env.local are in platformC. Build-Time vs Runtime Variables
# Vite/Next.js: VITE_* and NEXT_PUBLIC_* are build-time
# If you change them in production, you must rebuild
# Vercel: Redeploy after changing NEXT_PUBLIC_ vars
vercel --prodIssue 2: Missing Variables
Debug Workflow:
# 1. Check if variable is defined
def check_variable(var_name: str, env_file: Path):
"""Check if variable exists and has value."""
vars_dict = parse_env_file(env_file)
if var_name not in vars_dict:
print(f"❌ {var_name} not defined in {env_file}")
return False
if not vars_dict[var_name]:
print(f"⚠️ {var_name} defined but empty")
return False
print(f"✅ {var_name}={mask_secret(vars_dict[var_name])}")
return True
# 2. Check file precedence (Next.js)
def check_nextjs_precedence(project_dir: Path, var_name: str):
"""Check which file defines variable in Next.js."""
env_files = [
'.env.production.local',
'.env.local',
'.env.production',
'.env'
]
for env_file in env_files:
file_path = project_dir / env_file
if file_path.exists():
vars_dict = parse_env_file(file_path)
if var_name in vars_dict:
print(f"Found in: {env_file}")
print(f"Value: {mask_secret(vars_dict[var_name])}")
return
print(f"❌ {var_name} not found in any .env file")Common Mistakes:
# ❌ Wrong: Variable name typo
DATABASE_URL=postgres://...
# Code accessing: DATABASE_ULR (typo)
# ❌ Wrong: Not loaded in .env file
# Missing from .env entirely
# ❌ Wrong: Empty value
DATABASE_URL=
# ✅ Correct:
DATABASE_URL=postgres://localhost:5432/mydbIssue 3: Framework-Specific Issues
Next.js Issues:
A. NEXT_PUBLIC_ Variable Not Available Client-Side
# Issue: Variable defined but undefined in browser
# Cause: Missing NEXT_PUBLIC_ prefix
# ❌ Wrong:
API_URL=https://api.example.com
# ✅ Correct:
NEXT_PUBLIC_API_URL=https://api.example.comB. File Precedence Confusion
# If .env.local defines DATABASE_URL=local
# But .env defines DATABASE_URL=remote
# .env.local wins (higher precedence)
# Solution: Check all .env files
ls -la .env*Express/Node.js Issues:
A. process.env.VAR Undefined
// Issue: Variable not loaded
// Cause: Forgot to load dotenv
// ❌ Wrong: No dotenv
const dbUrl = process.env.DATABASE_URL; // undefined
// ✅ Correct: Load dotenv first
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;Flask Issues:
A. Variable Not Available in Flask App
# Issue: os.getenv() returns None
# Cause: dotenv not loaded before app initialization
# ❌ Wrong: Config before load_dotenv
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') # None
load_dotenv()
# ✅ Correct: load_dotenv first
from dotenv import load_dotenv
load_dotenv()
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')Issue 4: Platform-Specific Issues
Vercel Issues:
A. Environment Variable Not Applied
# Issue: Changed env var in Vercel UI, but app still uses old value
# Cause: Vercel caches NEXT_PUBLIC_ vars at build time
# Solution: Redeploy
vercel --prod
# Or: Use non-NEXT_PUBLIC_ var and read server-sideB. Variable Shows in Preview, Not Production
# Issue: Variable works in preview deployments
# Cause: Variable only set for "Preview" environment in Vercel
# Solution: Set for "Production" environment too
# Vercel UI: Settings → Environment Variables → ProductionRailway Issues:
A. Variable Not Found After Deployment
# Issue: App can't find variable
# Cause: Railway uses exact syntax, no dotenv loading
# Solution: Set variables in Railway dashboard or CLI
railway variables set DATABASE_URL=postgres://...Heroku Issues:
A. Config Vars Not Applied
# Check current config
heroku config --app myapp
# Set missing var
heroku config:set DATABASE_URL=postgres://... --app myapp
# Restart app (sometimes needed)
heroku restart --app myappIssue 5: Secret Exposure
If Secrets Are Committed to Git:
# Immediate actions:
# 1. Revoke/rotate exposed secrets immediately
# 2. Remove from git history
# 3. Update all deployments
# Remove from git history (DESTRUCTIVE)
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
# Or use BFG Repo-Cleaner (recommended)
bfg --delete-files .env
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push (careful!)
git push origin --force --allIf Secrets Are in Public Logs:
# 1. Clear deployment logs (if possible)
# 2. Rotate all exposed secrets
# 3. Add logging safeguards
# Update code to never log secrets
# ❌ Wrong:
logger.info(f"Connecting with {db_password}")
# ✅ Correct:
logger.info(f"Connecting to database (credentials masked)")Debugging Checklist
Local Development
# 1. Check .env file exists
ls -la .env*
# 2. Validate structure
python scripts/validate_env.py .env
# 3. Check for duplicates
python scripts/validate_env.py .env --check-duplicates
# 4. Verify framework detection
python scripts/validate_env.py .env --detect-framework
# 5. Test variable loading
node -e "require('dotenv').config(); console.log(process.env.DATABASE_URL)"Production Debugging
# 1. Compare local vs production
python scripts/sync_secrets.py --platform vercel --compare
# 2. Check platform variables
vercel env ls
# 3. Check build logs for errors
vercel logs --follow
# 4. Verify deployment used correct branch
vercel inspect <deployment-url>Quick Fixes
Fix 1: Sync Local to Platform
# 1. Validate local .env
python scripts/validate_env.py .env
# 2. Compare with platform
python scripts/sync_secrets.py --platform vercel --compare
# 3. Sync (dry-run first)
python scripts/sync_secrets.py --platform vercel --sync --dry-run
# 4. Actually sync
python scripts/sync_secrets.py --platform vercel --sync --confirmFix 2: Regenerate .env.example
# Generate from current .env
python scripts/validate_env.py .env --generate-example
# Review and commit
git add .env.example
git commit -m "docs: update .env.example"Fix 3: Check All .env Files (Next.js)
# List all .env files
find . -name ".env*" -not -path "*/node_modules/*"
# Check each file
for file in .env*; do
echo "=== $file ==="
python scripts/validate_env.py $file
donePrevention Tips
Tip 1: Use .env.example
# Always maintain .env.example with structure
# Never include actual values
# .env.example
DATABASE_URL=postgres://localhost:5432/mydb
JWT_SECRET=your-secret-here
API_KEY=your-api-key-hereTip 2: Validate Before Deploy
# Add to CI/CD pipeline
# .github/workflows/validate.yml
- name: Validate Environment
run: python scripts/validate_env.py .env.exampleTip 3: Document Platform-Specific Setup
# README.md
## Environment Setup
### Local Development
1. Copy `.env.example` to `.env.local`
2. Fill in actual values
3. Run `python scripts/validate_env.py .env.local`
### Vercel Deployment
1. Go to Settings → Environment Variables
2. Add variables from `.env.example`
3. Set for Production environment
4. DeploySummary
Common Issues:
- Works locally, not in production → Check platform sync
- Missing variables → Check file precedence
- Framework-specific → Check prefix requirements
- Platform quirks → Check platform documentation
Debugging Workflow: 1. Validate local .env structure 2. Check file precedence (Next.js) 3. Compare local vs platform 4. Check build logs 5. Verify variable access in code
Quick Fixes:
- Sync to platform with dry-run
- Regenerate .env.example
- Check all .env files
- Rotate exposed secrets
Related References
- Validation: Environment validation workflows
- Security: Secret exposure recovery
- Synchronization: Platform sync procedures
- Frameworks: Framework-specific patterns
--- Lines: 245 ✓ 180-250 range
Environment Validation Workflows
Part of: env-manager
Category: infrastructure
Reading Level: Intermediate
Purpose
Complete validation workflows for environment variables: structure checks, completeness verification, naming conventions, and framework-specific validation patterns.
Validation Hierarchy
Level 1: Structure Validation (Basic)
Validates file format and syntax.
Level 2: Semantic Validation (Intermediate)
Validates naming, completeness, framework conventions.
Level 3: Integration Validation (Advanced)
Validates across environments, platforms, and services.
Structure Validation
Check 1: Valid Key-Value Format
What to Check:
# Valid formats:
KEY=value
KEY="value with spaces"
KEY='single quoted'
KEY=
# Invalid formats:
key=value # lowercase
KEY = value # spaces around =
KEY=value # comment # inline comments (parser-dependent)
=value # missing key
KEY # missing =Validation Script:
import re
from pathlib import Path
def validate_structure(env_file: Path) -> list[str]:
"""Validate .env file structure."""
errors = []
valid_line_pattern = re.compile(r'^[A-Z_][A-Z0-9_]*=.*$')
with open(env_file) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith('#'):
continue
# Check format
if not valid_line_pattern.match(line):
errors.append(f"Line {line_num}: Invalid format: {line}")
# Check for inline comments (warning)
if '#' in line and not line.startswith('#'):
# Check if # is inside quotes
key, value = line.split('=', 1)
if '#' in value and not (value.startswith('"') or value.startswith("'")):
errors.append(f"Line {line_num}: WARNING: Possible inline comment: {line}")
return errorsCheck 2: No Duplicate Keys
Issue:
# .env file
DATABASE_URL=postgres://local
DATABASE_URL=postgres://production # Duplicate! Which wins?Validation:
def check_duplicates(env_file: Path) -> dict[str, list[int]]:
"""Find duplicate keys and their line numbers."""
keys = {}
with open(env_file) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key = line.split('=', 1)[0]
if key in keys:
keys[key].append(line_num)
else:
keys[key] = [line_num]
# Return only duplicates
return {k: v for k, v in keys.items() if len(v) > 1}Check 3: Proper Quoting
Valid Quoting Patterns:
# No quotes needed
SIMPLE_VALUE=hello
# Quotes required for spaces
WITH_SPACES="hello world"
# Quotes required for special chars
SPECIAL_CHARS="value with = or # chars"
# Escape quotes inside quotes
ESCAPED="He said \"hello\""
ESCAPED_SINGLE='It'\''s working'Validation:
def validate_quoting(env_file: Path) -> list[str]:
"""Check for proper quoting."""
warnings = []
with open(env_file) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
# Check for spaces without quotes
if ' ' in value and not (value.startswith('"') or value.startswith("'")):
warnings.append(f"Line {line_num}: Value with spaces should be quoted: {key}")
# Check for special chars without quotes
if any(char in value for char in ['#', '=', '$']) and not value.startswith(('"', "'")):
warnings.append(f"Line {line_num}: Value with special chars should be quoted: {key}")
return warningsCompleteness Validation
Compare Against .env.example
Workflow:
def compare_env_files(env_file: Path, example_file: Path) -> dict:
"""Compare .env against .env.example."""
def parse_keys(file_path: Path) -> set[str]:
keys = set()
with open(file_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key = line.split('=', 1)[0]
keys.add(key)
return keys
env_keys = parse_keys(env_file)
example_keys = parse_keys(example_file)
return {
'missing': example_keys - env_keys, # Required but missing
'extra': env_keys - example_keys, # Present but not documented
'common': env_keys & example_keys # Properly documented
}Usage:
# Find missing required variables
python validate_env.py .env --compare .env.example
# Output:
# Missing variables (in .env.example but not .env):
# - DATABASE_URL
# - JWT_SECRET
# - SMTP_HOST
#
# Extra variables (in .env but not documented):
# - DEBUG_MODE
# - TEMP_API_KEYCheck Required Variables by Environment
Pattern:
REQUIRED_VARS = {
'development': [
'NODE_ENV',
'DATABASE_URL',
'PORT'
],
'production': [
'NODE_ENV',
'DATABASE_URL',
'PORT',
'JWT_SECRET',
'API_KEY',
'REDIS_URL'
],
'test': [
'NODE_ENV',
'TEST_DATABASE_URL'
]
}
def validate_required_vars(env_file: Path, environment: str) -> list[str]:
"""Check if all required variables are present."""
required = REQUIRED_VARS.get(environment, [])
present = parse_keys(env_file)
missing = [var for var in required if var not in present]
return missingNaming Convention Validation
Standard Conventions
Valid Names:
# Standard format: UPPERCASE_WITH_UNDERSCORES
DATABASE_URL=value
API_KEY=value
MAX_CONNECTIONS=10
# Framework-specific prefixes
NEXT_PUBLIC_API_URL=value # Next.js client-side
VITE_API_URL=value # Vite client-side
REACT_APP_API_URL=value # Create React AppInvalid Names:
# Bad patterns
databaseUrl=value # camelCase
database-url=value # kebab-case
database.url=value # dots
123_KEY=value # starts with number
_PRIVATE_KEY=value # leading underscore (convention warning)Validation:
import re
def validate_naming_conventions(env_file: Path, framework: str = None) -> list[str]:
"""Validate variable naming conventions."""
errors = []
# Standard pattern
standard_pattern = re.compile(r'^[A-Z][A-Z0-9_]*$')
# Framework-specific patterns
framework_patterns = {
'nextjs': re.compile(r'^(NEXT_PUBLIC_|NEXT_)[A-Z0-9_]+$'),
'vite': re.compile(r'^(VITE_)?[A-Z0-9_]+$'),
'react': re.compile(r'^(REACT_APP_)?[A-Z0-9_]+$'),
}
with open(env_file) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key = line.split('=', 1)[0]
# Check standard pattern
if not standard_pattern.match(key):
errors.append(f"Line {line_num}: Invalid naming: {key} (use UPPERCASE_WITH_UNDERSCORES)")
# Check framework-specific patterns
if framework and framework in framework_patterns:
pattern = framework_patterns[framework]
if not pattern.match(key):
errors.append(f"Line {line_num}: {framework} convention violation: {key}")
return errorsFramework-Specific Validation
Next.js Validation
Rules:
def validate_nextjs_env(env_file: Path) -> list[str]:
"""Validate Next.js environment variables."""
errors = []
warnings = []
with open(env_file) as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
# Check NEXT_PUBLIC_ prefix rules
if key.startswith('NEXT_PUBLIC_'):
# Warn about secrets in public vars
if any(secret in key.lower() for secret in ['secret', 'key', 'password', 'token']):
errors.append(f"Line {line_num}: SECURITY: Secret in NEXT_PUBLIC_ var: {key}")
# Check for client-side vars without NEXT_PUBLIC_
if any(client in key.lower() for client in ['api_url', 'api_endpoint']):
if not key.startswith('NEXT_PUBLIC_'):
warnings.append(f"Line {line_num}: Client-side var without NEXT_PUBLIC_: {key}")
return errors, warningsFile Precedence Check:
def check_nextjs_file_precedence(project_dir: Path) -> dict:
"""Check Next.js .env file precedence."""
env_files = [
'.env.local',
'.env.development.local',
'.env.production.local',
'.env.development',
'.env.production',
'.env'
]
found_files = []
for env_file in env_files:
if (project_dir / env_file).exists():
found_files.append(env_file)
# Parse and check for conflicts
all_vars = {}
for env_file in found_files:
vars_in_file = parse_env_file(project_dir / env_file)
for key, value in vars_in_file.items():
if key in all_vars:
all_vars[key].append((env_file, value))
else:
all_vars[key] = [(env_file, value)]
# Find variables defined in multiple files
conflicts = {k: v for k, v in all_vars.items() if len(v) > 1}
return {
'files_found': found_files,
'conflicts': conflicts,
'precedence_order': env_files
}Express/Node.js Validation
Standard Variables:
NODE_STANDARD_VARS = {
'NODE_ENV': ['development', 'production', 'test'],
'PORT': r'^\d+$', # Must be a number
'DATABASE_URL': r'^postgres://|mysql://|mongodb://', # Must be valid connection string
}
def validate_nodejs_env(env_file: Path) -> list[str]:
"""Validate Node.js environment variables."""
errors = []
vars_dict = parse_env_file(env_file)
for key, value in vars_dict.items():
if key in NODE_STANDARD_VARS:
expected = NODE_STANDARD_VARS[key]
if isinstance(expected, list):
# Check enumeration
if value not in expected:
errors.append(f"{key}: Invalid value '{value}', expected one of {expected}")
elif isinstance(expected, str):
# Check regex pattern
if not re.match(expected, value):
errors.append(f"{key}: Value doesn't match expected format")
return errorsSummary
Validation Workflow: 1. Structure: Valid format, no duplicates, proper quoting 2. Completeness: All required vars present, compare with .env.example 3. Naming: UPPERCASE_WITH_UNDERSCORES, framework prefixes 4. Framework: Next.js NEXT_PUBLIC_, file precedence 5. Platform: Vercel/Railway/Heroku conventions
Key Validations:
- ✅ Valid key-value format
- ✅ No duplicate keys
- ✅ Proper quoting for spaces/special chars
- ✅ All required variables present
- ✅ Consistent naming conventions
- ✅ Framework-specific rules followed
- ✅ No secrets in client-side vars (NEXT_PUBLIC_)
Related References
- Security: Secret scanning and exposure detection
- Synchronization: Platform sync validation
- Frameworks: Complete framework patterns
--- Lines: 285 ✓ 150-300 range