From sdlc
Type-safe, self-documenting environment configuration with idempotent sync
How this skill is triggered — by the user, by Claude, or both
Slash command
/sdlc:centralized-configurationsonnetThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
A cross-cutting pattern for managing environment variables across polyglot repositories with type safety, self-documentation, and idempotent synchronization.
A cross-cutting pattern for managing environment variables across polyglot repositories with type safety, self-documentation, and idempotent synchronization.
Environment configuration is often an afterthought, leading to:
.env.example doesn't match actual requirementsCode is the source of truth. Environment variables are defined in typed configuration classes, and everything else is generated from that single source.
┌─────────────────────────┐
│ Configuration Code │ ← Single source of truth
│ (typed, documented) │
└───────────┬─────────────┘
│ generate
▼
┌─────────────────────────┐
│ .env.example │ ← Always fresh, committed
└───────────┬─────────────┘
│ idempotent sync
▼
┌─────────────────────────┐
│ .env │ ← User secrets, gitignored
├─────────────────────────┤
│ ✓ Existing values │ ← Never overwritten
│ + New variables │ ← Added with defaults
│ ⚠️ External vars │ ← Detected & preserved
└─────────────────────────┘
Configuration is defined in code, not in .env files:
# Instead of this:
.env.example → manually maintained → gets outdated
# Do this:
ConfigClass → generates → .env.example → syncs → .env
Validate environment variables at startup, not at usage:
Every variable includes its purpose and where to get it:
api_key: str = Field(
description="API key for ExampleService. Get from: https://example.com/keys"
)
This description becomes a comment in .env.example:
# API key for ExampleService. Get from: https://example.com/keys
API_KEY=
Sensitive values are:
Running the generator is always safe:
| Scenario | Behavior |
|---|---|
Variable exists in .env | Preserved - never overwritten |
| New variable in config | Added with default value |
| Secret field | Added with empty value |
| Removed from config | Moved to "External" section |
| Unknown variable | Preserved with warning |
Related variables share a prefix:
| Module | Prefix | Example |
|---|---|---|
| Database | DB_ | DB_HOST, DB_PORT |
| GitHub App | GITHUB_ | GITHUB_APP_ID |
| AWS | AWS_ | AWS_ACCESS_KEY_ID |
| Logging | LOG_ | LOG_LEVEL, LOG_FORMAT |
This prevents collisions and makes grouping obvious.
Define configuration in typed classes with:
A script that:
.env.example with comments.envLoad and validate all settings at application startup:
# main.py - validate immediately
settings = get_settings() # Crashes here if invalid
app = create_app(settings)
Not lazily at usage:
# ❌ Bad - crashes at runtime when accessed
def some_function():
api_key = os.getenv("API_KEY") # Might be None!
project/
├── config/ # or settings/, src/config/
│ ├── __init__.py # Exports get_settings()
│ ├── settings.py # Main Settings class
│ ├── database.py # DatabaseSettings (DB_*)
│ ├── github.py # GitHubSettings (GITHUB_*)
│ └── logging.py # LoggingSettings (LOG_*)
├── scripts/
│ └── generate_env.py # Generator script
├── .env.example # Generated, committed
├── .env # User secrets, gitignored
└── justfile / Makefile # gen-env command
The gold standard for Python configuration.
Dependencies:
# pyproject.toml
dependencies = [
"pydantic>=2.0",
"pydantic-settings>=2.0",
]
Main Settings Class:
# config/settings.py
from functools import lru_cache
from typing import TYPE_CHECKING
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
if TYPE_CHECKING:
from config.github import GitHubSettings
class Settings(BaseSettings):
"""Application settings - validated on startup."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# Application
app_name: str = Field(
default="my-app",
description="Application name for logging",
)
debug: bool = Field(
default=False,
description="Enable debug mode. Never in production.",
)
# Database
database_url: str | None = Field(
default=None,
description=(
"PostgreSQL connection URL. "
"Format: postgresql://user:pass@host:port/db"
),
)
# Secrets
api_key: SecretStr | None = Field(
default=None,
description="External API key. Get from: https://example.com/keys",
)
# Nested settings via property
@property
def github(self) -> "GitHubSettings":
from config.github import GitHubSettings
return GitHubSettings()
@lru_cache
def get_settings() -> Settings:
"""Cached settings - validates on first access."""
return Settings()
def reset_settings() -> None:
"""Clear cache for testing."""
get_settings.cache_clear()
Modular Settings with Prefix:
# config/github.py
from typing import Self
from pydantic import Field, SecretStr, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class GitHubSettings(BaseSettings):
"""GitHub App settings with GITHUB_ prefix."""
model_config = SettingsConfigDict(
env_prefix="GITHUB_", # All vars become GITHUB_*
env_file=".env",
extra="ignore",
)
app_id: str | None = Field(
default=None,
description="GitHub App ID from app settings page",
)
app_name: str | None = Field(
default=None,
description="App slug for commit attribution",
)
installation_id: str | None = Field(
default=None,
description="Installation ID per organization",
)
private_key: SecretStr | None = Field(
default=None,
description="RSA private key (PEM format)",
)
@property
def is_configured(self) -> bool:
return bool(self.app_id and self.installation_id and self.private_key)
@property
def bot_username(self) -> str | None:
return f"{self.app_name}[bot]" if self.app_name else None
@model_validator(mode="after")
def validate_complete(self) -> Self:
"""Ensure all-or-nothing configuration."""
required = [self.app_id, self.installation_id, self.private_key]
provided = sum(1 for f in required if f is not None)
if 0 < provided < 3:
missing = [
"GITHUB_APP_ID" if not self.app_id else None,
"GITHUB_INSTALLATION_ID" if not self.installation_id else None,
"GITHUB_PRIVATE_KEY" if not self.private_key else None,
]
raise ValueError(f"Incomplete config: {[m for m in missing if m]}")
return self
Generator Script:
#!/usr/bin/env python3
# scripts/generate_env.py
"""Generate .env.example and sync .env idempotently."""
from pathlib import Path
from pydantic import SecretStr
from pydantic_settings import BaseSettings
PROJECT_ROOT = Path(__file__).parent.parent
def get_default(field_info) -> str:
from pydantic_core import PydanticUndefined
d = field_info.default
if d is None or d is PydanticUndefined:
return ""
if isinstance(d, bool):
return str(d).lower()
if hasattr(d, "value"):
return str(d.value)
return str(d)
def is_secret(field_type) -> bool:
from typing import get_args, get_origin
if field_type is SecretStr:
return True
origin = get_origin(field_type)
return origin and SecretStr in get_args(field_type)
def generate_section(cls: type[BaseSettings], name: str, prefix: str = "") -> list[str]:
import textwrap
lines = ["", "# " + "=" * 70, f"# {name}", "# " + "=" * 70, ""]
for fname, finfo in cls.model_fields.items():
ftype = cls.__annotations__.get(fname, str)
env_name = f"{prefix}{fname.upper()}"
if finfo.description:
for line in textwrap.wrap(finfo.description, 70):
lines.append(f"# {line}")
lines.append(f"{env_name}={'' if is_secret(ftype) else get_default(finfo)}")
lines.append("")
return lines
def generate() -> str:
from config.settings import Settings
from config.github import GitHubSettings
lines = [
"# " + "=" * 70,
"# ENVIRONMENT CONFIGURATION",
"# " + "=" * 70,
"# AUTO-GENERATED - run: just gen-env",
"# " + "=" * 70,
]
lines.extend(generate_section(Settings, "APPLICATION"))
lines.extend(generate_section(GitHubSettings, "GITHUB", "GITHUB_"))
return "\n".join(lines)
def parse_env(path: Path) -> dict[str, str]:
if not path.exists():
return {}
env = {}
for line in path.read_text().split("\n"):
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
env[k.strip()] = v.strip()
return env
def sync(example: Path, env: Path) -> tuple[int, int, list[str]]:
existing = parse_env(env)
content = example.read_text()
template_keys = set()
new_vars = []
out = []
for line in content.split("\n"):
s = line.strip()
if not s or s.startswith("#"):
out.append(line)
continue
if "=" in line:
k, _, default = line.partition("=")
k = k.strip()
template_keys.add(k)
out.append(f"{k}={existing.get(k, default.strip())}")
if k not in existing:
new_vars.append(k)
else:
out.append(line)
extra = [k for k in existing if k not in template_keys]
if extra:
out.extend(["", "# " + "=" * 70, "# EXTERNAL VARIABLES", "# " + "=" * 70, ""])
for k in sorted(extra):
out.append(f"{k}={existing[k]}")
env.write_text("\n".join(out))
return len(new_vars), len(extra), extra
def main():
content = generate()
example = PROJECT_ROOT / ".env.example"
env = PROJECT_ROOT / ".env"
example.write_text(content)
print(f"✅ Generated {example.name}")
if env.exists():
new, ext, ext_vars = sync(example, env)
print(f"✅ Synced .env ({new} new)" if new else "✅ .env up to date")
if ext_vars:
print(f"⚠️ {ext} external: {', '.join(ext_vars)}")
else:
env.write_text(content)
print("✅ Created .env")
if __name__ == "__main__":
main()
Usage:
from config import get_settings
settings = get_settings()
# Direct access
print(settings.app_name)
# Nested with prefix
if settings.github.is_configured:
print(settings.github.bot_username)
# Secrets require explicit access
if settings.api_key:
key = settings.api_key.get_secret_value()
(Future implementation)
// config/settings.ts
import { z } from 'zod';
import { config } from 'dotenv';
config();
const SettingsSchema = z.object({
APP_NAME: z.string().default('my-app'),
DEBUG: z.coerce.boolean().default(false),
DATABASE_URL: z.string().url().optional(),
API_KEY: z.string().optional(),
});
export const settings = SettingsSchema.parse(process.env);
(Future implementation)
// config/config.go
package config
import "github.com/kelseyhightower/envconfig"
type Settings struct {
AppName string `envconfig:"APP_NAME" default:"my-app"`
Debug bool `envconfig:"DEBUG" default:"false"`
DatabaseURL string `envconfig:"DATABASE_URL"`
APIKey string `envconfig:"API_KEY"`
}
func Load() (*Settings, error) {
var s Settings
err := envconfig.Process("", &s)
return &s, err
}
gen-env command documented.env ignored, .env.example committeddevops/logging - Structured logging configurationdevops/secrets-management - Vault, AWS Secrets Manager integrationdevops/docker-compose - Container environment configurationnpx claudepluginhub agentparadise/agentic-primitives --plugin sdlcSets up Python configuration management with environment variables and pydantic-settings for typed validation, secrets handling, and environment-specific settings.
Manages full lifecycle of secrets and environment variables: decides placement (constant, .env, CI secret, env var), scaffolds .env.example/.gitignore, add/update/rotate/remove/migrate/audit/provision across envs. Language-agnostic.
Analyzes environment variables in code, generates .env.example templates, validates configurations and types, documents variables with examples, and provides naming and security best practices.