AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Confdiff

mcp-esperanza-volkov-confdiff · by esperanza-volkov

Semantic, format-aware diff for config & structured-data files (JSON, YAML, TOML, INI, .env, .properties, CSV, XML). See what actually changed — keys and values, not text noise.

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add mcp-esperanza-volkov-confdiff

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-esperanza-volkov-confdiff)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
yesterday

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Confdiff? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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) became port: "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-commentstsconfig.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.json against 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-entropy also 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.

  • --json output masks the value too and adds "redacted": true on 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.