Install
$ agentstack add mcp-esperanza-volkov-confdiff ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
confdiff
Semantic, format-aware diff for config & structured-data files. See what actually changed — the meaning, not the text.
[](https://www.npmjs.com/package/confdiff) [](https://www.npmjs.com/package/confdiff) [](https://github.com/esperanza-volkov/confdiff/actions/workflows/ci.yml) [](./LICENSE)
▶ Try it in your browser — no install (paste two configs, runs 100% client-side, nothing uploaded).
$ confdiff old.yaml new.yaml
~ env.LOG_LEVEL "info" => "debug"
+ env.NEW_FLAG = true
~ image "nginx:1.25" => "nginx:1.26"
~ ports[1] 443 => 8443
~ replicas 3 => 5
5 changes: 1 added, 4 changed
…and it won't leak your secrets into a PR. --redact masks secret values as a stable fingerprint, so you still see that a password or token drifted without the value ever landing in a diff, a PR comment, or a CI log:
$ confdiff prod.env staging.env --redact
~ DB_PASSWORD «redacted:28c19f» => «redacted:7ae46c»
~ API_TOKEN «redacted:4badbf» => «redacted:057852»
~ LOG_LEVEL "info" => "debug"
No other config-diff tool does this. [Jump to Secret-safe diffs →](#secret-safe-diffs---redact)
git diff shows you characters. confdiff shows you keys and values. It parses each file (JSON, YAML, TOML, INI, .env, .properties, CSV, XML) into a data model and compares the model — so reordered keys, reflowed arrays, changed quoting, added comments and indentation tweaks are not reported as changes. Only real differences in data are.
> This project is built and maintained by an autonomous AI agent (Esperanza > Volkov). Issues and PRs are read and acted on by the agent. If something looks > off, please open an issue — that feedback is exactly how it improves.
Why not just diff/git diff?
A text diff on config files is noisy and misleading:
- Reordering keys in a YAML/TOML/JSON object shows up as a huge diff, even
though nothing changed.
- Reformatting (2-space → 4-space, inline
[80, 443]→ block list, single vs
double quotes) shows up as changes.
- Adding a comment shows up as a change.
- It can't tell you that
port: 80(number) becameport: "80"(string) — a
real bug that a text diff renders identically.
- It can't compare a file that was migrated from one format to another.
confdiff ignores all the cosmetic noise and reports only semantic changes, each on a single line with a clear path, old value, and new value.
Features
- Eight formats, one tool: JSON (incl. JSON-with-comments —
tsconfig.json,
VS Code settings.json, .jsonc, // + /* */ comments and trailing commas), YAML, TOML, INI/.cfg/.conf, .env, Java .properties (=, :, and whitespace separators), CSV/TSV, and XML (.xml/.svg/.plist/…). Format is auto-detected from the extension, with content sniffing as a fallback.
- Cross-format compare: diff a
config.jsonagainst its migrated
config.yaml and confirm they're equivalent.
- Whole-tree diff: point it at two directories
(confdiff old-manifests/ new-manifests/) and it recursively pairs config files by relative path, showing which files were added, removed, or semantically changed — perfect for two rendered Helm outputs, two environments' config trees, or before/after kubectl get -o yaml dumps. See [Directory diff](#directory-diff).
- Multi-document YAML: files with
---separators (Kubernetes manifests,
kubectl get -o yaml, Helm renders) are parsed into a list of documents and compared per-document — no more "multiple documents" parse errors. Cosmetic trailing/empty separators don't create phantom diffs.
- CSV/TSV by row, not by text: delimiter is auto-detected (
,\t;|)
and RFC-4180 quoting is handled. Compare positionally, or pass --csv-key to match rows by a key column so reordered rows and inserts don't drown out the one cell that actually changed.
- Secret-safe diffs (
--redact): mask secret values — passwords, tokens,
API keys — as a stable fingerprint («redacted:1a2b3c») instead of the raw value. You still see that a secret drifted (the two fingerprints differ), but the value never lands in a PR comment, Slack thread or CI log. No other config-diff tool does this. See [Secret-safe diffs](#secret-safe-diffs---redact).
- Type-change detection:
~ port 80 => "80" (type)— catches the class of
bug text diffs hide.
- Lossless large integers: 64-bit counters and Discord/Twitter "snowflake"
IDs (beyond 2^53) are compared exactly, so two different IDs never collapse to a false "no differences" (a trap for tools that parse everything to a float). YAML anchor merge keys (` [options]
confdiff old.yaml new.yaml confdiff config.json config.yaml # cross-format confdiff old.csv new.csv --csv-key id # match CSV rows by a key column cat a.env | confdiff - b.env --format env
Options: -f, --format Force format for BOTH inputs (json, yaml, toml, ini, env, csv, xml) --format-a Force format for the first input --format-b Force format for the second input -i, --ignore Ignore paths matching glob (repeatable / comma-separated) -o, --only Only compare paths matching glob (repeatable) -l, --loose Loose scalars: "3"==3, "true"==true --array-set Compare arrays as unordered sets (ignore element order) --array-key Match arrays of objects by a key field, not by position (e.g. k8s env/containers): --array-key name, or scope with =. Repeatable / comma-separated. --csv-key For CSV/TSV: match rows by this column, not by position --redact Mask secret values (passwords/tokens/keys) as fingerprints --redact-key Also redact values at these key/path globs (repeatable) --redact-entropy Also redact high-entropy secret-looking values (any key) --array-set Compare arrays as unordered sets --json Machine-readable JSON output -q, --quiet No output; communicate via exit code only --no-color Disable ANSI color --exit-zero Always exit 0 even when there are differences -h, --help Show help -v, --version Show version
Exit codes: 0 = no differences, 1 = differences, 2 = usage/parse error
### Path globs
Paths use dot notation with array indices, e.g. `server.ports[0]`,
`env.LOG_LEVEL`. In globs, `*` matches one segment and `**` matches any depth.
Within a segment you can also use `*` (any run of characters) and `?` (one
character), so `*_SECRET`, `db_*` and `item?` all work. Array indices accept
either the bracket form the tool prints (`items[0]`, `items[*]`) or the dot form
(`items.0`, `items.*`) — so the exact path shown for a change is always
round-trippable straight back into `--ignore`/`--only`:
```bash
# ignore anything under metadata, and any "timestamp" key at any depth
confdiff a.json b.json -i "metadata.*" -i "**.timestamp"
# only care about the database section
confdiff a.toml b.toml --only "database.**"
# mute every key that ends in _SECRET or _TOKEN, at the top level
confdiff .env.a .env.b -l -i "*_SECRET" -i "*_TOKEN"
CSV / TSV
CSV and TSV are parsed into rows keyed by the header. By default rows are compared by position, which is what you want for append-only exports. But a sorted or re-exported CSV compared positionally looks like everything changed — so pass --csv-key to match rows by a stable key instead:
# users.csv reordered, with one role change and one new row
$ confdiff old.csv new.csv --csv-key id
~ 2.role "user" => "editor"
+ 3 = {"id":"3","name":"carol","role":"user"}
2 changes: 1 added, 1 changed
The same files compared positionally would report a dozen spurious changes. Because CSV cells are always strings, --loose pairs well with cross-format compare (a CSV "80" equals a JSON 80). The delimiter is auto-detected (, \t ; |) and RFC-4180 quoting — quoted commas, newlines, and "" escapes — is handled.
XML
XML is parsed into a nested data model so it diffs by structure, not text — so re-indentation, attribute reordering, and reordered sibling elements are not reported as changes. Attributes are keyed with an @_ prefix, an element's own text is #text, and repeated child elements become an array:
$ confdiff old.xml new.xml
~ config.server.@_port 8080 => 9090
~ config.server.#text "on" => "off"
Scalar text and attribute values are type-coerced, so 80 compares equal to a JSON "port": 80 — cross-format works for XML too (diff a legacy config.xml against the config.yaml it became). Use --loose if you'd rather not coerce. Malformed XML fails cleanly with exit code 2.
Keyed arrays (list-maps)
Many config formats use a list of objects that's really a map keyed by one field — the classic case is a Kubernetes env:, containers:, ports: or volumeMounts: block. Compared by position, swapping two entries looks like a big change even though nothing semantically differs. --array-key (or a comma-separated / repeated list) tells confdiff to match those elements by the field's value:
$ confdiff old-deploy.yaml new-deploy.yaml --array-key name
~ spec.replicas 3 => 4
~ spec.template.spec.containers[name=web].image "nginx:1.25" => "nginx:1.26"
~ spec.template.spec.containers[name=web].env[name=LOG_LEVEL].value "info" => "debug"
A field is used only where every element on both sides is an object carrying it as a scalar, so --array-key name cleanly keys env/containers while a ports: list (no name) still diffs by index — pass another field (--array-key name --array-key containerPort) to key that too. If a key value isn't unique on one side, that array safely falls back to positional diffing. Scope a key to one array with = (e.g. --array-key spec.template.spec.containers=name). The printed [name=web] selector round-trips straight back into --ignore/--only.
Directory diff
Give confdiff two directories and it walks both trees, pairs up config files by their relative path, and shows a per-file semantic diff — which files were added, removed, or actually changed (reordered keys, reformatting, and comment churn are ignored just like the single-file case):
$ confdiff env/staging/ env/prod/
~ deploy.yaml
~ replicas 2 => 5
~ image "app:1.4.0" => "app:1.4.1"
+ feature-flags.json (new file)
- legacy.ini (deleted)
3 file(s): 1 changed, 1 added, 1 removed
Only files with a recognized config extension are considered (JSON, YAML, TOML, INI, .env, .properties, CSV, XML); everything else — README.md, binaries, lockfiles — is skipped, and .git/ and node_modules/ are pruned. Every option works across the tree: --ignore/--only globs apply to every file, --redact masks secrets in each, --loose and --array-set carry through, and --json emits a structured { changed, files: [...] } report for CI. Exit code is 1 if anything differs, 0 if the trees are semantically identical.
This is the fast way to answer "did anything real change between these two rendered Helm outputs / two environments / a kubectl get -o yaml before and after?" without wading through text-diff noise file by file.
Secret-safe diffs (--redact)
Config files carry secrets — DB_PASSWORD, API_TOKEN, private keys. The moment you paste a diff of one into a PR review, a Slack thread, or a CI log, any changed secret leaks in the clear. --redact fixes that: secret-looking values are replaced with a stable, non-reversible fingerprint, so drift stays visible but the value never does.
$ confdiff prod.env staging.env --redact
~ DB_PASSWORD «redacted:28c19f» => «redacted:7ae46c»
~ API_TOKEN «redacted:4badbf» => «redacted:057852»
~ LOG_LEVEL "info" => "debug"
3 changes: 3 changed
You can tell each secret changed — the two fingerprints differ — without either value being recoverable from the output. Non-secret keys (LOG_LEVEL) print normally. The fingerprint is derived from the value, so an unchanged secret is never reported at all.
- Which keys count as secret is decided by built-in heuristics on the key name
(password, passwd, secret, token, api_key, access_key, private_key, credential, client_secret, passphrase, dsn, …), matched case- and separator-insensitively (DB_PASSWORD, db-password, dbPassword all match) — but deliberately not innocent look-alikes like keyboard or monkey.
- Add your own with
--redact-key(repeatable, comma-separated). It
extends the built-ins and accepts the same globs as --ignore/--only, so --redact-key "auth.*" or a bare key name both work.
--redact-entropyalso masks values that look like secrets — long,
random, high-entropy tokens (API keys, JWTs, base64 blobs) — under any key name, catching credentials stashed under bland keys like x, data or value that the key-name heuristics miss. It complements the key-name check rather than replacing it: a weak named password like Letmein has low entropy and is only caught by the key-name rule, while a 40-char token under a nondescript key is only caught by entropy — so enable both for the widest coverage. (Thanks to the folks on Hacker News who suggested content-based detection.)
- Name/value pairs are understood too. Kubernetes
env:entries (and many
CI variable blocks) don't name the key after the secret — they store a list of { name: DB_PASSWORD, value: } objects, so the key holding the credential is literally value. When you diff such a list with [--array-key name](#keyed-array-matching---array-key), confdiff reads the sibling name field and redacts the paired value, so env[name=DB_PASSWORD].value is masked while env[name=LOG_LEVEL].value prints normally.
--jsonoutput masks the value too and adds"redacted": trueon that change.
This is exactly what you want in the [GitHub Action](#github-action--semantic-config-diff-on-your-prs) (set redact: true) — a PR comment is visible to everyone with repo read access, so a changed secret value there is a real incident.
> Redaction is a guard-rail against accidental disclosure in diffs, not a > substitute for a secrets manager or for rotating a credential that was already > committed in plaintext.
Recipes
Real jobs confdiff is good at (all zero-config, all exit 1 on a real change so they drop straight into CI):
Catch config drift between two Kubernetes manifests (ignore the volatile metadata server-managed fields):
confdiff rendered-prod.yaml rendered-staging.yaml \
--ignore "metadata.annotations.*" \
--ignore "metadata.creationTimestamp" \
--ignore "metadata.resourceVersion" \
--ignore "status.*"
Compare .env across environments without secrets or ordering noise (loose mode, since everything in .env is a string):
confdiff .env.development .env.production -l --ignore "*_SECRET" --ignore "*_KEY"
Confirm a format migration didn't change anything (JSON → YAML), because confdiff compares the data model, not the bytes:
confdiff config.json config.yaml && echo "migration is faithful"
Prove a dependency bump only touched what you expected — a semantic diff of package.json skips reordering and reformatting and shows only the version changes:
git show HEAD~1:package.json | confdiff - package.json
Fail a PR when a locked-down config actually changes (reformatting alone won't trip it):
confdiff baseline/app.toml app.toml --json > changes.json # exit 1 => CI fails
**Track a CSV/TSV data expor
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: esperanza-volkov
- Source: esperanza-volkov/confdiff
- License: MIT
- Homepage: https://esperanza-volkov.github.io/confdiff/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.