How to audit differences between environment config files
- Step 1Dump the resolved config for both environments — Use your app's config-dump command (or read the merged config the runtime actually sees) for the two environments you are auditing — dev vs staging, or staging vs prod. You want the fully resolved JSON, not just the override layer, unless you are specifically auditing overrides.
- Step 2Redact secrets before pasting — Replace real secret values with a placeholder so you audit structure and non-secret values, not credentials. The JSON Key Filter at /tool/json-key-filter can strip secret keys entirely, or set their values to
"[SECRET]"in both files so they diff clean. - Step 3Paste the base environment on the left — Paste the lower environment (dev or staging) into
JSON A (base). It must be a single valid JSON object. - Step 4Paste the target environment on the right — Paste the higher environment (staging or prod) into
JSON B (modified). Both panels are required. - Step 5Click Compare and separate intended from accidental — Review the diff list. Intended differences (different DB hostnames, log levels, public URLs) are expected. Anything else — a feature flag with the wrong state, a missing key, a timeout that drifted — is a finding.
- Step 6Copy the diff into a drift ticket — Use
Copy JSONto capture the difference list and paste it into a ticket. After you fix the unintended drift, re-run the diff; a clean result for the non-intentional keys confirms the environments are realigned.
Reading the diff as a config-drift report
How each entry type maps to a config-audit verdict. 'Expected?' depends on whether the key is one you deliberately vary per environment.
| Entry type | Means | Likely expected | Likely a finding |
|---|---|---|---|
added (in B, not A) | Key exists in the target env only | A new prod-only setting you added intentionally | A flag staging has that prod is missing — or vice-versa |
removed (in A, not B) | Key exists in the base env only | A debug-only key intentionally absent in prod | A required key dropped from the higher environment |
changed | Same key, different value | DB host, public URL, log level | Feature flag state, rate limit, timeout, a "true" vs true type drift |
Common config drift patterns and what the diff shows
Verified against the diff engine. Paths use dot notation; array settings use bracket indices.
| Drift pattern | A (staging) → B (prod) | Diff output |
|---|---|---|
| Debug left on in prod | {"debug":true} → {"debug":true} | No difference — and that's the finding (debug should be false in prod) |
| Flag missing in prod | {"flags":{"newUI":true}} → {"flags":{}} | removed flags.newUI with - true |
| Type drift on a flag | {"cache":"true"} → {"cache":true} | changed cache with - "true" / + true |
| Reordered CORS allowlist | ["a.com","b.com"] → ["b.com","a.com"] | changed [0] and changed [1] — array compared by index |
| Intended host difference | {"db":"stg.db"} → {"db":"prod.db"} | changed db — expected; verify it's the only host change |
Cookbook
Real (anonymised) environment-config audits. Left panel is the base env, right is the target. Secret values are shown as placeholders because you should redact before pasting.
Feature flag accidentally disabled in production
ExampleStaging has the new checkout enabled; prod's override file missed it. The diff makes the gap explicit instead of you scrolling two 300-line files.
JSON A (staging): JSON B (prod):
{ {
"flags": { "flags": {
"newCheckout": true, "newCheckout": false,
"darkMode": true "darkMode": true
} }
} }
Diff:
~1 changed
changed flags.newCheckout
- true
+ falseA type-drift bug: boolean stored as a string
ExampleAn env-var-to-config layer left cacheEnabled as the string "false" in prod, which is truthy in many languages. The diff catches the type shift as a single changed entry.
JSON A (staging): JSON B (prod):
{ {
"cacheEnabled": false "cacheEnabled": "false"
} }
Diff:
~1 changed
changed cacheEnabled
- false
+ "false"
→ "false" (string) is truthy in JS/Ruby — a real prod bug.Missing required key in the higher environment
ExampleProd's config is missing the retryPolicy block that staging has. A removal at a nested path pinpoints it.
JSON A (staging): JSON B (prod):
{ {
"retryPolicy": { "timeoutMs": 30000
"max": 3, }
"backoffMs": 200
},
"timeoutMs": 30000
}
Diff:
-2 removed
removed retryPolicy.max - 3
removed retryPolicy.backoffMs - 200Auditing override layers only
ExampleIf you keep a base config plus per-env overrides, diff the two override files. Any key that appears with the same value in both override files is a candidate to move into the shared base — and any shared key is worth checking for unintended drift.
JSON A (staging.override.json): JSON B (prod.override.json):
{ {
"logLevel": "debug", "logLevel": "warn",
"db": "stg.db" "db": "prod.db"
} }
Diff:
~2 changed
changed logLevel - "debug" + "warn"
changed db - "stg.db" + "prod.db"
→ Both differences are intended; nothing unexpected leaked.Confirming a redaction round-trips clean
ExampleAfter replacing secrets with the same placeholder in both files, those keys diff clean, leaving only the structural and non-secret differences for review.
JSON A: JSON B:
{ {
"apiKey": "[SECRET]", "apiKey": "[SECRET]",
"region": "eu-west-1" "region": "us-east-1"
} }
Diff:
~1 changed
changed region - "eu-west-1" + "us-east-1"
→ Secret key is silent; only the intended region differs.Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
A type-drifted flag (`"true"` vs `true`) is a `changed` entry
By designThe diff fires on JSON.stringify(a) !== JSON.stringify(b), so a boolean stored as a string vs a real boolean is caught — as a changed entry, not a special type label. Read the from/to values; a string "false" next to a boolean false is one of the highest-value findings in a config audit.
Reordered allowlist / array shows index-wise changes
Positional by designArrays (CORS origins, IP allowlists, plugin lists) are compared by index. A reordered allowlist with identical contents reports changed [0], changed [1], and so on. Sort both arrays consistently before pasting if order is not significant for that setting.
Secrets visible because you forgot to redact
Redact firstThe tool runs locally, so secrets are never uploaded — but they will be visible on your screen and in the copied diff. Redact before pasting: strip secret keys with /tool/json-key-filter or set them to a shared placeholder in both files so they diff clean.
Comments or trailing commas in the config (JSONC / .env-derived)
Parse errorInputs go through strict JSON.parse. JSONC comments, trailing commas, or unquoted keys throw a parser error. Convert the config to strict JSON first — run it through /tool/json-format-fixer to drop comments and fix near-JSON before diffing.
`null` value vs an absent key
Removed / addedA key set to null in one env and absent in the other is reported as removed (or added) of a null value, because null is a present value. If your config layer treats null as 'use default', treat these entries as semantically equal during review.
Identical configs
SupportedWhen two environments are aligned (after key-order normalisation), the panel shows The two JSON values are identical. That is the clean signal you want for keys that should never differ between environments.
Free-plan config over 2 MB per side
Upgrade requiredEach pasted config is capped at 2 MB on the free plan; over that returns Free plan supports JSON inputs up to 2 MB. Upgrade to Pro for unlimited input size. Diff only the relevant sub-tree (extract it first with the path extractor) or upgrade to Pro for unlimited size.
Only one config pasted
InvalidBoth JSON A and JSON B are required; a whitespace-only or empty side returns Please provide both JSON A and JSON B. There is no 'compare against last input' memory between runs.
A block changed from object to array (or scalar)
ChangedIf a config block was an object in one env and an array (or scalar) in the other, the tool reports a single changed entry at that path with the whole old and new value, rather than descending. Container-shape changes in config almost always indicate a serialization or schema mistake.
Frequently asked questions
How should I structure config files to make intentional differences easy to track?
Use a layered approach: one base config with shared settings, plus a small per-environment override file containing only the intended differences. Diff the two override files here — the result should be only your expected differences. Any key that appears in both override files with the same value is a candidate to move into the shared base.
How do I compare configs without exposing secrets in the diff?
Redact before pasting. Strip secret keys entirely with /tool/json-key-filter, or replace their values with a shared placeholder like "[SECRET]" in both files. Then you audit key presence and non-secret values; the secret keys diff clean and never appear in the output.
Will a reordered allowlist show as drift?
Yes — arrays are compared by index, so ["a.com","b.com"] vs ["b.com","a.com"] reports two changed entries even though the set is identical. If order is not significant for that setting, sort both arrays before pasting so only true membership changes surface.
Does the order of keys in my config matter?
No. Objects are compared by key, not position, so a config generator that emits keys in a different order between environments produces no false drift. Only genuine added/removed keys and value changes appear.
How do I catch a flag that's the wrong type, not just the wrong value?
The diff fires on serialized inequality, so "true" (string) versus true (boolean) shows as a changed entry whose from/to reveal the type. This is the classic env-var-to-config bug where everything 'looks' set but a string slipped in where a boolean belongs.
Can I diff a whole config directory at once?
No — this is a two-input paste tool, one base and one target. For multiple files, diff them pairwise, or merge the per-environment files into one object first with /tool/json-object-merger and diff the merged objects. There is no batch/folder mode here.
My config has comments (JSONC). Why won't it parse?
The tool uses strict JSON.parse, which rejects comments and trailing commas. Strip them first with /tool/json-format-fixer, then paste the cleaned strict JSON. The diff itself does not care about formatting — only that both inputs are valid JSON.
Is there a file upload for config files?
No. The tool has two textareas and is paste-only. Open each config, copy the JSON, and paste it into the base and target panels. There is no file picker — which is also why your config never leaves the browser.
How do I prove drift is fixed?
After resolving the unintended differences, re-run the diff. For keys that should match across environments, a clean (or expected-only) result is your evidence. Copy the before-and-after diffs into the drift ticket as proof of remediation.
Can I focus the audit on one section of a big config?
Yes. Extract the sub-tree from both configs first with /tool/json-path-extractor — pull $.services.payments from each — then diff the two extracts. This keeps the audit focused and avoids the 2 MB free cap on large configs.
Is the config data transmitted to JAD Apps?
No. Parsing and diffing happen in your browser. Configuration values — including connection strings and any secrets you forgot to redact — are never sent to a server. Clicking Compare makes no network request.
What's the difference between this and a plain text diff of the files?
A text diff flags reordered keys and whitespace as changes and can't see a type drift like "true" vs true as anything but a quoted-character change. This tool compares the parsed structure: key order and formatting are ignored, and you get a path-indexed list of real config differences.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.