Scan Multi-Environment Configs in One Command: ConfigDrift scan Walkthrough
You know configdrift check for comparing two files. But real projects have entire directories of YAML, JSON, TOML, and .env files spread across dev, staging, and prod. Here's how to scan all of them against a baseline in a single command.
The configdrift check command is great for quick two-file diffs. But when your project has 8 config files per environment — a database YAML, an app JSON, a secrets .env, a feature-flag TOML — running check on every pair gets old fast.
That's what configdrift scan solves. Point it at your environment directories, pick a baseline, and it compares every config file it finds — matching by filename, flattening nested structures, and triaging differences by severity.
This tutorial walks through a realistic 3-environment setup from scratch: sample configs, the scan command, interpreting results, and the config-driven workflow for larger projects.
Installation
pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
Or via Homebrew (macOS/Linux):
brew tap Coding-Dev-Tools/tap
brew install configdrift
Or via Scoop (Windows):
scoop bucket add Coding-Dev-Tools https://github.com/Coding-Dev-Tools/scoop-bucket
scoop install configdrift
The Setup: A Real 3-Environment Project
Let's build a realistic scenario. Your project has three environments, each with its own directory of config files:
config/
├── dev/
│ ├── database.yaml
│ ├── app.json
│ ├── features.toml
│ └── secrets.env
├── staging/
│ ├── database.yaml
│ ├── app.json
│ ├── features.toml
│ └── secrets.env
└── prod/
├── database.yaml
├── app.json
├── features.toml
└── secrets.env
Here's what each file looks like in dev:
config/dev/database.yaml
database:
endpoint: db-dev.internal:5432
pool_size: 5
timeout: 30
auth:
token_expiry: 3600
refresh_enabled: true
config/dev/app.json
{
"app": {
"name": "myapp-dev",
"port": 3000,
"debug": true,
"log_level": "debug"
},
"rate_limit": {
"max_requests": 1000,
"window_seconds": 60
}
}
config/dev/features.toml
[features]
new_dashboard = true
api_v2 = false
beta_export = true
[rate_limit]
enabled = false
max_rps = 100
config/dev/secrets.env
STRIPE_API_KEY=sk_test_abc123
DATABASE_URL=postgresql://dev:devpass@db-dev.internal:5432/myapp
SESSION_SECRET=dev-session-secret-32chars
REDIS_URL=redis://localhost:6379
Now staging and prod have mostly the same files — but with critical differences. That's where drift hides.
The One Command: configdrift scan
configdrift scan ./config/dev ./config/staging ./config/prod --baseline dev
That's it. ConfigDrift:
- Reads every supported file in each directory (
.yaml,.json,.toml,.env) - Matches files by name across environments
- Flattens nested structures to dot-notation keys
- Compares each environment against the baseline
- Triages every difference by severity
Output for our scenario:
Scanning 3 environments: dev (baseline), staging, prod
Found 4 config files per environment
Config Drift: dev → staging (database.yaml)
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ database.endpoint │ ~ changed│ db-dev.internal:5432 │ db-staging.internal │ BREAKING │
│ database.pool_size │ ~ changed│ 5 │ 10 │ INFO │
│ auth.token_expiry │ ~ changed│ 3600 │ 7200 │ BREAKING │
│ auth.refresh_enabled │ ~ changed│ true │ true │ INFO │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Config Drift: dev → staging (secrets.env)
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ STRIPE_API_KEY │ ~ changed│ sk_test_abc123 │ sk_live_staging_*** │ BREAKING │
│ DATABASE_URL │ ~ changed│ ...db-dev.internal… │ ...db-staging.int… │ BREAKING │
│ SESSION_SECRET │ ~ changed│ dev-session-secret │ staging-session-sec │ BREAKING │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Config Drift: dev → staging (app.json)
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ app.debug │ ~ changed│ true │ false │ INFO │
│ app.log_level │ ~ changed│ debug │ info │ INFO │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Config Drift: dev → prod (database.yaml)
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ database.endpoint │ ~ changed│ db-dev.internal:5432 │ db-prod.internal │ BREAKING │
│ database.pool_size │ ~ changed│ 5 │ 50 │ INFO │
│ auth.token_expiry │ ~ changed│ 3600 │ 86400 │ BREAKING │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Config Drift: dev → prod (secrets.env)
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ STRIPE_API_KEY │ ~ changed│ sk_test_abc123 │ sk_live_prod_*** │ BREAKING │
│ DATABASE_URL │ ~ changed│ ...db-dev.internal… │ ...db-prod.int… │ BREAKING │
│ SESSION_SECRET │ ~ changed│ dev-session-secret │ prod-session-secret │ BREAKING │
│ REDIS_URL │ - removed│ redis://localhost… │ │ BREAKING │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
Config Drift: dev → prod (features.toml)
┌──────────────────────────┬──────────┬──────────────────────┬──────────────────────┬───────────┐
│ Key │ Change │ Old Value │ New Value │ Severity │
├──────────────────────────┼──────────┼──────────────────────┼──────────────────────┼───────────┤
│ features.new_dashboard │ ~ changed│ true │ false │ INFO │
│ features.api_v2 │ ~ changed│ false │ true │ INFO │
│ rate_limit.enabled │ ~ changed│ false │ true │ INFO │
│ rate_limit.max_rps │ ~ changed│ 100 │ 10000 │ INFO │
└──────────────────────────┴──────────┴──────────────────────┴──────────────────────┴───────────┘
That one command compared 12 config files (4 files × 3 environments) and surfaced every difference, triaged by severity.
Interpreting Severity Levels
Not all drift is equal. ConfigDrift's severity triage is what makes the output actionable instead of overwhelming:
| Severity | Trigger | What to Do |
|---|---|---|
| BREAKING | Critical key changed, added, or removed — any key matching database.*, auth.*, secret.*, password.*, token.*, endpoint.*, api_key.* | Block deploy. These cause incidents. |
| WARNING | Optional key added or removed (non-critical keys that appear in one environment but not another) | Review before deploy. Could indicate incomplete rollout. |
| INFO | Non-critical value changed (log levels, feature flags, pool sizes, debug flags) | Informational. Expected in multi-environment setups. |
Looking at our scan results, the triage tells a clear story:
- BREAKING:
database.endpoint,STRIPE_API_KEY,DATABASE_URL— these should differ between environments (dev vs prod endpoints are expected). But theREDIS_URLbeing removed from prod is a real problem — that service is about to break. - INFO:
app.debug=falsein staging,database.pool_size=50in prod — these are expected environment-specific values. No action needed. - WARNING: If a key exists in staging but not prod, that's a signal that a feature was partially rolled out — worth reviewing.
The key insight: not all drift is a problem, but you can't manage what you can't see. ConfigDrift scan surfaces everything so you can make informed decisions about which differences are intentional and which are time bombs.
JSON Output: Pipe Results into Your Pipeline
For CI/CD integration or custom scripts, use --output json:
configdrift scan ./config/dev ./config/staging ./config/prod --baseline dev --output json
Returns structured JSON you can filter with jq or Python:
{
"staging": {
"database.yaml": {
"changes": [
{
"key": "database.endpoint",
"change_type": "changed",
"old_value": "db-dev.internal:5432",
"new_value": "db-staging.internal:5432",
"severity": "breaking"
}
],
"has_breaking": true
}
},
"prod": {
"secrets.env": {
"changes": [
{
"key": "REDIS_URL",
"change_type": "removed",
"old_value": "redis://localhost:6379",
"new_value": null,
"severity": "breaking"
}
],
"has_breaking": true
}
}
}
Filter for only removed keys — the most dangerous drift:
configdrift scan ./config/dev ./config/staging ./config/prod \
--baseline dev --output json \
| python3 -c "import sys,json; d=json.load(sys.stdin); \
[print(f'{env}/{file}: {c[\"key\"]} REMOVED') \
for env,files in d.items() for file,data in files.items() \
for c in data['changes'] if c['change_type']=='removed']"
Output:
prod/secrets.env: REDIS_URL REMOVED
That's the signal you need — a secret that exists in dev and staging but was accidentally dropped from prod.
Silent Mode: CI Gate on Multi-Environment Scans
Use --output silent to get a pass/fail exit code with no output — perfect for CI pipelines:
configdrift scan ./config/dev ./config/staging ./config/prod \
--baseline dev --output silent
Exit code 0 = no breaking drift. Exit code 1 = at least one breaking difference found. Add this to any CI pipeline:
# .github/workflows/config-parity.yml
name: Config Parity
on:
pull_request:
paths:
- 'config/**'
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ConfigDrift
run: pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
- name: Scan all environments
run: |
configdrift scan ./config/dev ./config/staging ./config/prod \
--baseline dev --output silent
This gates your PRs on config parity across all environments — not just two files, but every config file in every directory.
Config-Driven Workflow: configdrift init
For projects with complex directory layouts, typing out every environment path gets repetitive. ConfigDrift has a config-driven workflow:
Step 1: Generate a config template
configdrift init .
# Creates .configdrift.yaml
Step 2: Define your environments
# .configdrift.yaml
environments:
dev:
- ./config/dev
staging:
- ./config/staging
- ./secrets/staging # Multiple directories per environment
prod:
- ./config/prod
- ./secrets/prod
Each environment can reference multiple directories. ConfigDrift merges all files from all listed directories for that environment before comparing. This handles the common pattern where secrets live in a separate directory from application configs.
Step 3: Run the scan
configdrift scan --config .configdrift.yaml --baseline dev
No need to type directory paths. The config file becomes your source of truth for environment layout — commit it, review it, and let your CI pipeline use it.
CI integration with config file
# .github/workflows/config-parity.yml
name: Config Parity
on:
pull_request:
paths:
- 'config/**'
- 'secrets/**'
- '.configdrift.yaml'
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ConfigDrift
run: pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
- name: Scan environments
run: configdrift scan --config .configdrift.yaml --baseline dev --output silent
Now any PR that touches config files, secrets, or the ConfigDrift config itself automatically triggers a full environment parity check.
What scan Does That check Doesn't
| Feature | configdrift check | configdrift scan |
|---|---|---|
| Compare two files | ✓ | — |
| Compare entire directories | — | ✓ |
| 3+ environments at once | — | ✓ |
| Auto-discover all config files | — | ✓ |
| Match files by name across envs | — | ✓ |
| Multiple dirs per environment | — | ✓ |
| Config-driven workflow | — | ✓ |
| Severity triage | ✓ | ✓ |
| JSON / silent output | ✓ | ✓ |
Use check for quick two-file diffs. Use scan for everything else — it's the command you'll run in CI and the command you'll run before every deploy.
Supported Config Formats
configdrift scan reads all common config file formats automatically:
- YAML (
.yaml,.yml) — nested structures flattened to dot-notation keys - JSON (
.json) — nested objects flattened like YAML - TOML (
.toml) — Python 3.10+ withtomlifallback - .env (
.env) — standardKEY=VALUEformat with quote stripping
Nested keys are flattened automatically. This YAML:
database:
endpoint: db-prod.internal:5432
pool_size: 50
auth:
token_expiry: 86400
Becomes these comparison keys:
database.endpoint = db-prod.internal:5432
database.pool_size = 50
auth.token_expiry = 86400
Keys matching critical prefixes (database.*, auth.*, secret.*, endpoint.*, api_key.*) automatically get BREAKING severity — no custom rules needed on the free tier.
Getting Started
Stop flying blind on config drift
Scan all your environment directories in one command. ConfigDrift surfaces every difference, triages by severity, and gives you the exit code to gate your pipeline.
pip install git+https://github.com/Coding-Dev-Tools/configdrift.git
cd your-project
configdrift scan ./config/dev ./config/staging ./config/prod --baseline dev
View on GitHub →
ConfigDrift is part of the DevForge developer tool ecosystem — 11 CLI tools built autonomously for developers who ship.