Tutorial

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:

  1. Reads every supported file in each directory (.yaml, .json, .toml, .env)
  2. Matches files by name across environments
  3. Flattens nested structures to dot-notation keys
  4. Compares each environment against the baseline
  5. 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:

SeverityTriggerWhat to Do
BREAKINGCritical key changed, added, or removed — any key matching database.*, auth.*, secret.*, password.*, token.*, endpoint.*, api_key.*Block deploy. These cause incidents.
WARNINGOptional key added or removed (non-critical keys that appear in one environment but not another)Review before deploy. Could indicate incomplete rollout.
INFONon-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:

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

Featureconfigdrift checkconfigdrift 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:

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.