by M2Toolkit / v0.1.0 / Framework-agnostic / Node.js 22.12+

Catch the config change that takes prod down.

Structural comparison of env, JSON, YAML and TOML, with risk-aware findings, policy enforcement and built-in secret redaction. Works offline.

npm install --save-dev configdiff-js

Why ConfigDiff

A text diff shows what moved.
It can’t tell you what matters.

Learn more

A diff cannot tell you that key order changed but nothing else did, that an array was reordered, that 3000 became "3000", which change is dangerous, or whether pasting the result into CI logs leaks a password.

ConfigDiff compares structure, explains each change, flags the risky ones with documented rules, and keeps secrets out of every report. It parses your config but never executes it. Use it as a CLI or as a library.

ts
import { analyzeConfig, parseConfig } from "configdiff-js";

const report = analyzeConfig({
  baseline: parseConfig(stagingText, "env"),
  candidate: parseConfig(productionText, "env"),
  policy: { rules: { "environment-mismatch": { baseline: "staging" } } },
});

report.status; // "pass" | "review" | "fail"
report.findings; // safe to publish: sensitive values are not in it

Compare. Flag. Redact.

Structure, not text

Key order, whitespace and comments never show up as changes. A reordered array, or 3000 turning into "3000", does. Normalization such as unordered, trim and coerce is opt-in.

ts
import { compareConfigs } from "configdiff-js";

compareConfigs({ b: 2, a: 1 }, { a: 1, b: 2 }); // no changes: key order is not a change
compareConfigs({ port: "3000" }, { port: 3000 }); // modified port: string became number
compareConfigs({ l: [1, 2] }, { l: [2, 1] }); // l[0] and l[1] modified: arrays are ordered
compareConfigs({ l: [1, 2] }, { l: [2, 1] }, { unordered: ["l"] }); // no changes

Risk-aware findings

15 documented rules, each with a stable id, a remediation and its known false positives: authentication switched off, staging values in production, secrets in public variables.

text
[ERROR] ENABLE_AUTH  (watch-paths)
  Authentication must stay enabled. Value changed from "true" to "false".
[ERROR] NEXT_PUBLIC_API_SECRET  (public-secret-exposure)
  "NEXT_PUBLIC_" variables are exposed to client code, but this one looks secret.
[WARNING] API_URL  (environment-mismatch)
  Candidate value contains the marker "staging" (value not shown).
[WARNING] DATABASE_URL  (credential-in-url)
  Value is a URL with embedded credentials (value hidden).

Secrets stay out

Redaction happens when changes are built, not when they are printed. Public objects hold redacted: true instead of the value, so text, JSON and error paths cannot leak what they never received.

json
{
  "type": "modified",
  "path": "JWT_SECRET",
  "baseline": { "present": true, "type": "string", "redacted": true },
  "candidate": { "present": true, "type": "string", "redacted": true },
  "explanation": "Value changed (values hidden)."
}

Policy as a file

Required keys, forbidden values, watched paths, coercions and severity overrides live in one reviewable configdiff.policy.json. It is data, never code, and unknown keys are errors.

json
{
  "version": 1,
  "failOn": "error",
  "coerce": { "PORT": "integer" },
  "sensitive": { "keys": ["DATABASE_URL"] },
  "rules": {
    "required-keys": { "keys": ["DATABASE_URL", "REDIS_URL"] },
    "watch-paths": {
      "paths": [{ "path": "ENABLE_AUTH", "severity": "error" }]
    }
  }
}

Built for CI

Exit code 0, 1 or 2, a --fail-on threshold, JSON output, and plain text with severities spelled out, so the log is safe to read and safe to share.

yaml
# .github/workflows/config.yml
on: pull_request
jobs:
  configdiff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: >
          npx --yes configdiff-js@0.1 compare
          --baseline config/staging.yaml
          --candidate config/production.yaml
          --policy configdiff.policy.json
          --fail-on warning

Validate a single config

Check one file against a built-in schema or any Standard Schema validator such as Zod, Valibot or ArkType. Messages from third-party validators are scrubbed of sensitive values.

ts
import { parseConfig, validateConfig } from "configdiff-js";

validateConfig(parseConfig(text, "env"), {
  properties: { PORT: { type: "integer", min: 1, max: 65535, required: true } },
  unknownKeys: "warn",
});
// schema-violation PORT: Value is above the maximum of 65535
  • Built-in schema
  • Standard Schema
  • Zod · Valibot · ArkType

Playground

Paste two configs. See what a diff would miss.

Everything below runs in your browser on the same ConfigDiff source this page documents. Your text never leaves the page.

Baselinestaging
Candidateproduction
Policyconfigdiff.policy.json
✗ Failed2 added · 1 removed · 5 modified · 3 redacted

Findings (9)

  • warningAPI_URLenvironment-mismatch

    Candidate value contains the marker "staging" (value not shown).

  • warningDATABASE_URLcredential-in-url

    Value is a URL with embedded credentials (value hidden).

  • errorENABLE_AUTHwatch-paths

    Authentication must stay enabled. Value changed from "true" to "false".

  • errorNEXT_PUBLIC_API_SECRETpublic-secret-exposure

    "NEXT_PUBLIC_" variables are exposed to client code, but this one looks secret.

  • warningOLD_NAMEmissing-in-candidate

    Key exists in the baseline but is missing from the candidate.

  • infoOLD_NAMEpossible-rename

    Possibly renamed to "NEW_NAME": both hold an identical value (value not shown).

  • warningPORTcoercion-failed

    The candidate value could not be coerced to integer; it was compared unmodified.

  • warningPORTtype-mismatch

    Type changed from number to string.

  • errorREDIS_URLrequired-keys

    Required key is missing.

Changes (8)

  • ~API_URL"https://staging.example.com" → "https://staging.example.com/v2"
  • ~DATABASE_URL[REDACTED] → [REDACTED]
  • ~ENABLE_AUTH"true" → "false"
  • ~JWT_SECRET[REDACTED] → [REDACTED]
  • +NEW_NAME"some-long-shared-value"
  • +NEXT_PUBLIC_API_SECRET[REDACTED]
  • −OLD_NAME"some-long-shared-value"
  • ~PORT3000 → "abc"

Secrets are redacted even though they changed, authentication switched off is an error, and PORT=abc fails its integer coercion. Edit the policy to turn rules on or off. Read more →

15Documented rules
4Formats: env, JSON, YAML, TOML
2Runtime dependencies
0Network calls, ever

Comparison

Where it fits next to the tools you already use.

The others are better at what they target; “–” means it is not what the tool is for. Offline by design, ConfigDiff is meant to sit beside them, not replace them. Read the full comparison.

CapabilityConfigDiffmicrodiff / deep-diffdotenv-diffenvalid / T3 Env
Best atConfig across environmentsRaw object diff.env vs .env.exampleValidating process.env
Compares two configsyesyes (objects)yes (env files)no
Reads env, JSON, YAML, TOMLyesyou parse firstenv filesenv only
Risk rules and policy fileyes––schema only
Redacted, CI-safe reportsyes–––
100k leaves, comparedabout 0.3 s10–100× faster––

Documentation

From your first diff to a gated pull request.

Review config changes like you review code

One command in CI, one policy file in the repo, and no secret ever lands in a log.