Skip to content

Repository files navigation

diffyml

Release OpenSSF Scorecard Go Report Card codecov Mutation testing badge Quality Gate Status FOSSA Status Go Reference License: MIT

A fast, structural YAML diff tool with built-in Kubernetes intelligence. One dependency, minimal attack surface, native CI annotations for GitHub, GitLab, and Gitea.

diffyml output

diffyml compares YAML files and shows meaningful, structured differences — not line-by-line text diffs.

📖 Full documentation: szhekpisov.github.io/diffyml

Table of Contents

Why diffyml?

Fastest structural YAML diff tool at scale. On large (5K lines) and xlarge (50K lines) inputs diffyml is 1.5–1.9× faster than the nearest YAML-aware competitor. On small and medium files it ties within a few milliseconds — the residual overhead comes from capabilities the simpler tools lack (x509 certificate inspection, remote URL fetching, AI-powered summaries). See PERFORMANCE.md for methodology and results.

One dependency, zero surprises. A single module dependency (yaml.v3) and pure Go stdlib. Minimal attack surface, auditable in minutes.

Gets YAML right. Dotted keys, type preservation, mixed-type lists, nil values — concrete edge cases other tools get wrong. diffyml treats YAML semantics as first-class, not an afterthought.

How It Compares

Feature diffyml dyff plain diff
YAML-aware (structural diff) Yes Yes No (line-based)
Kubernetes resource matching By apiVersion + kind + name (or generateName) By apiVersion + kind + name No
Rename detection Yes (content similarity — handles name changes) Yes (identifier-based — name must stay the same) No
API version migration Yes (--ignore-api-version) No No
CI annotation formats 3 (GitHub, GitLab, Gitea) 0 0
Module dependencies 1 (yaml.v3) 23 0
Directory comparison Yes No Yes
Git external diff (GIT_EXTERNAL_DIFF) Yes (auto-detect) Manual (wrapper script) N/A
Inline diff highlighting Yes (word-level) Yes (character-level) No
Custom colors Yes (hex, env vars) No No
Configuration file Yes (.diffyml.yml) No No
Performance (78 KB) 19 ms 120 ms (6.4x) 6 ms
Performance (780 KB) 129 ms 1,146 ms (8.9x) 45 ms

Comparison based on dyff v1.11.3 and diffyml v1.5.23. See PERFORMANCE.md for full benchmark methodology and results against 5 competitors. Open an issue if anything is outdated.

Installation

Homebrew

brew tap szhekpisov/diffyml
brew install diffyml

Scoop (Windows)

scoop bucket add diffyml https://github.com/szhekpisov/scoop-diffyml
scoop install diffyml

Nixpkgs

Available in nixpkgs on the unstable channel. It landed after the 26.05 branch-off, so it isn't in the current stable release yet — it will be in the next one.

# Ephemeral shell
nix shell nixpkgs#diffyml

# Run without installing
nix run nixpkgs#diffyml -- old.yaml new.yaml

# Install into your profile
nix profile install nixpkgs#diffyml

On NixOS, add it to environment.systemPackages:

environment.systemPackages = with pkgs; [ diffyml ];

Without flakes:

nix-env -iA nixpkgs.diffyml   # or: nix-shell -p diffyml

Go Install

go install github.com/szhekpisov/diffyml@latest

Make sure $GOPATH/bin is in your PATH:

export PATH="$(go env GOPATH)/bin:$PATH"

Install script (Linux / macOS)

curl -fsSL https://szhekpisov.github.io/diffyml/install.sh | sh

Detects your OS and architecture, downloads the matching release archive, verifies its SHA256 against the signed checksums.txt, and installs to /usr/local/bin/diffyml. Customizable via env vars:

Variable Default Notes
DIFFYML_VERSION latest release Pin a specific version, e.g. 1.6.1. Recommended in CI — avoids the unauthenticated GitHub API call (60 req/hr per IP) used to resolve the latest tag.
INSTALL_DIR /usr/local/bin Falls back to sudo if the directory isn't writable.
VERIFY sha256 Set cosign to verify the cosign signature on checksums.txt first (requires cosign in PATH), or none to skip verification.
GITHUB_TOKEN unset If set, used to authenticate the GitHub API call when resolving the latest version. Useful on shared CI egress IPs.
# Pin a version, install into ~/bin, verify cosign signature too:
DIFFYML_VERSION=1.6.1 INSTALL_DIR="$HOME/bin" VERIFY=cosign \
  sh -c "$(curl -fsSL https://szhekpisov.github.io/diffyml/install.sh)"

Linux packages (.deb / .rpm / .apk)

Native packages for Debian/Ubuntu, RHEL/Fedora, and Alpine (amd64 and arm64) are attached to every release. All package archives are listed in the cosign-signed checksums.txt, so you can verify before installing — see Verifying Releases. The .apk uses --allow-untrusted because nfpm-built apks aren't GPG-signed; verify the SHA256 from checksums.txt instead.

# Debian / Ubuntu
curl -fLO "https://github.com/szhekpisov/diffyml/releases/download/v1.6.1/diffyml_1.6.1_linux_amd64.deb"
sudo dpkg -i diffyml_1.6.1_linux_amd64.deb

# RHEL / Fedora / openSUSE
curl -fLO "https://github.com/szhekpisov/diffyml/releases/download/v1.6.1/diffyml_1.6.1_linux_amd64.rpm"
sudo rpm -i diffyml_1.6.1_linux_amd64.rpm

# Alpine
curl -fLO "https://github.com/szhekpisov/diffyml/releases/download/v1.6.1/diffyml_1.6.1_linux_amd64.apk"
sudo apk add --allow-untrusted diffyml_1.6.1_linux_amd64.apk

The binary is installed to /usr/bin/diffyml.

Direct binary download

If you'd rather not pipe a script to sh, the same archives are attached to every release for Linux, macOS, and Windows (amd64 and arm64). Linux/macOS ship as .tar.gz, Windows as .zip:

VERSION=1.6.1  # check the releases page for the latest
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
curl -fL "https://github.com/szhekpisov/diffyml/releases/download/v${VERSION}/diffyml_${VERSION}_${OS}_${ARCH}.tar.gz" \
  | tar -xz
sudo mv diffyml /usr/local/bin/

On Windows (PowerShell):

$Version = "1.6.1"  # check the releases page for the latest
$Arch = if ($Env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "amd64" }
curl.exe -fLO "https://github.com/szhekpisov/diffyml/releases/download/v$Version/diffyml_${Version}_windows_$Arch.zip"
Expand-Archive "diffyml_${Version}_windows_$Arch.zip" -DestinationPath .
# move diffyml.exe somewhere on your PATH

See Verifying Releases below to check signatures and provenance before installing.

Docker

Multi-arch images (linux/amd64, linux/arm64) are published to GitHub Container Registry:

docker pull ghcr.io/szhekpisov/diffyml:latest

# Compare two files from the current directory
docker run --rm -v "$PWD:/work" -w /work ghcr.io/szhekpisov/diffyml:latest old.yaml new.yaml

Images are built from a distroless base and run as a non-root user. Use :latest or pin to a specific version (e.g. :1.5.25).

From Source

git clone https://github.com/szhekpisov/diffyml.git
cd diffyml
go build -o diffyml

Verifying Releases

Published release artifacts are never modified or re-uploaded — each version is a one-time, append-only event. Every release includes:

  • Checksums (checksums.txt) — SHA256 hashes for all archives
  • Cosign signature (checksums.txt.sigstore.json) — keyless Sigstore signature
  • SBOMs (*.spdx.json) — SPDX Software Bill of Materials for each archive
  • SLSA provenance — Level 3 provenance attestation
Verification commands

Verify the checksums signature:

cosign verify-blob checksums.txt \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp 'https://github.com/szhekpisov/diffyml/' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com'

# Linux
sha256sum --check checksums.txt --ignore-missing
# macOS
shasum -a 256 --check checksums.txt --ignore-missing

Verify SLSA provenance:

gh attestation verify diffyml_<VERSION>_linux_amd64.tar.gz \
  --repo szhekpisov/diffyml

Verify the container image signature:

cosign verify \
  --registry-referrers-mode=oci-1-1 \
  --certificate-identity-regexp 'https://github.com/szhekpisov/diffyml/' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/szhekpisov/diffyml:<VERSION>

Quick Start

# Compare two local files
diffyml old.yaml new.yaml

# Compare local file against a remote URL
diffyml local.yaml https://example.com/remote.yaml

# Use in CI — exit code 1 when differences found
diffyml -s deployment-old.yaml deployment-new.yaml

# Use as kubectl external diff provider:
export KUBECTL_EXTERNAL_DIFF="diffyml --omit-header --set-exit-code"

kubectl diff with diffyml

Features

  • 7 output formats — detailed, compact, brief, GitHub, GitLab, Gitea, JSON
  • Path filtering — include/exclude paths with exact match or regex
  • Remote files — compare directly from HTTP/HTTPS URLs
  • Certificate inspection — inspects and compares embedded x509 certificates
  • Chroot navigation — focus comparison on a specific YAML subtree
  • Git integration — use as GIT_EXTERNAL_DIFF or via .gitattributes for YAML-only scoping
  • Inline diff highlighting — highlights only the changed parts within scalar values (version tags, IPs, ports) for quick scanning
  • Custom colors — configurable color palette for accessibility (colorblind-friendly)
  • Configuration file — project-level defaults via .diffyml.yml (all flags supported)
  • Sensitive value masking — opt-in redaction of Kubernetes Secret data / stringData and arbitrary paths; applies to every output format
  • AI-powered summaries ⭐ — natural language summaries of changes via Anthropic API

Usage

diffyml [flags] <from> <to>

Output Formats

Format Flag Use case
detailed -o detailed (default) Human review — full context
compact -o compact Quick scan of changes
brief -o brief Summary only
github -o github GitHub Actions annotations
gitlab -o gitlab GitLab CI annotations
gitea -o gitea Gitea CI annotations
json -o json Machine-readable — piping, scripting, CI

CI Integration

Use -s / --set-exit-code to set the exit code based on differences:

Exit code Meaning
0 No differences (or success without -s)
1 Differences detected (only with -s)
255 Error occurred
diffyml -s before.yaml after.yaml || echo "Config drift detected"

GitHub Actions — use the diffyml-action composite action (no manual binary install):

- uses: szhekpisov/diffyml-action@v1
  with:
    from: old.yaml
    to: new.yaml

To inspect drift without failing the job, read the has-differences output:

- uses: szhekpisov/diffyml-action@v1
  id: diff
  with:
    from: old.yaml
    to: new.yaml
    fail-on-diff: 'false'
    output: github

- if: steps.diff.outputs.has-differences == 'true'
  run: echo "Configuration drift detected"

See the action repo for the full list of inputs and outputs.

Kubernetes Support

Resources are auto-detected and matched by apiVersion + kind + metadata.name (or metadata.generateName), so diffs stay meaningful even when document order changes.

Rename detection — when resources can't be matched by identifier (e.g., kustomize configMapGenerator hash-suffix changes like app-config-abc123app-config-def456), diffyml pairs unmatched documents by content similarity (60% threshold) and shows field-level diffs instead of bulk add/remove. Disable with --detect-renames=false.

API migration--ignore-api-version drops apiVersion from the matching key, so an upgrade from apps/v1beta1 to apps/v1 shows field-level diffs instead of a remove + add.

Opt out--detect-kubernetes=false disables K8s-aware matching entirely and compares documents by position.

# Compare two Kubernetes manifests
diffyml manifests-v1.yaml manifests-v2.yaml

# API migration — match by kind + name only
diffyml --ignore-api-version manifests-v1.yaml manifests-v2.yaml

# Disable Kubernetes detection
diffyml --detect-kubernetes=false file1.yaml file2.yaml

Directory Comparison

diffyml accepts two directories as positional arguments. It recursively discovers all regular files in each directory (regardless of extension), matches them by relative path, and shows aggregated differences. Files that cannot be parsed as YAML are silently skipped.

This makes diffyml a drop-in KUBECTL_EXTERNAL_DIFF provider — kubectl passes two temporary directories containing extensionless temp files (e.g. apps.v1.Deployment.default.nginx), and diffyml discovers them automatically:

export KUBECTL_EXTERNAL_DIFF="diffyml --omit-header --set-exit-code"
kubectl diff -f manifests/

Git Integration

diffyml can be used as a git external diff program. Git passes 7-9 positional arguments which diffyml auto-detects — non-YAML files are skipped with a warning.

# One-off: structural diff for YAML changes in the working tree
GIT_EXTERNAL_DIFF=diffyml git diff

# With flags (e.g. compact output)
GIT_EXTERNAL_DIFF='diffyml -o compact' git diff

For permanent setup, use .gitattributes — git's built-in diff handles all other file types normally:

*.yaml diff=diffyml
*.yml  diff=diffyml
git config diff.diffyml.command diffyml

Color and truecolor are auto-forced (git's pager makes stdout a pipe). Use --color never to disable. --set-exit-code is silently ignored — git aborts external diff programs that exit non-zero. Parse errors are non-fatal: a warning is printed and git continues to the next file.

AI Summary

Generate a natural language summary of changes using the Anthropic API:

export ANTHROPIC_API_KEY="sk-ant-..."

# Append AI summary after the diff output
diffyml --summary old.yaml new.yaml

# Use with brief format — replaces brief output with AI summary
diffyml --summary -o brief old.yaml new.yaml

# Use a different model
diffyml --summary --summary-model claude-sonnet-4-5-20250514 old.yaml new.yaml

The summary is appended after the standard diff output. If the API call fails, a warning is printed to stderr and the diff output is preserved. The exit code is never affected by summary success or failure.

Sensitive Value Masking

Masking is opt-in. When enabled, diffyml replaces the value of any matching diff with *** before rendering — applies to every output format (including json, json-patch, and the --summary prompt). Diffs still show that a value changed, just not what the value was.

# Auto-mask data/stringData of Kubernetes Secret resources
diffyml --mask-secrets secrets-old.yaml secrets-new.yaml

# Mask additional paths (e.g., a ConfigMap field that holds an API key)
diffyml --mask-path 'data.api_key' --mask-path 'data.db_url' old.yaml new.yaml

# Regex variant — mask any field whose path matches
diffyml --mask-path-regexp '(?i)password|token|secret' old.yaml new.yaml

# Custom placeholder
diffyml --mask-secrets --mask-placeholder '<REDACTED>' old.yaml new.yaml

Mask paths use the same dot-notation as --filter / --exclude (prefix match honored). The leading document index for multi-document files ([0], [1]) is automatically stripped before matching.

Configure defaults via .diffyml.yml so masking is always on for this repo:

mask-secrets: true
mask-path:
  - "data.api_key"
mask-path-regexp:
  - "(?i)password"
mask-placeholder: "***"

Filtering

# Show only changes under a specific path
diffyml --filter spec.replicas old.yaml new.yaml

# Exclude noisy paths
diffyml --exclude metadata.annotations old.yaml new.yaml

# Keys containing dots use bracket syntax
diffyml --exclude 'metadata.annotations[argocd.argoproj.io/tracking-id]' old.yaml new.yaml

# Regex filtering
diffyml --filter-regexp 'spec\.containers\[.*\]\.image' old.yaml new.yaml

Inverse Diff

-u, --unchanged inverts the report: instead of the differences, it lists the keys/values that are equal between the two files. Equal subtrees collapse to a single entry at the highest fully-equal node, and it honors every output format, --filter/--exclude, and masking.

Filters can target descendants of a collapsed equal subtree using either numeric list indices (containers.0.image) or list identifiers (containers.app.image). The collapsed subtree remains atomic in the output: matching or excluding any descendant keeps or removes the whole entry, respectively.

# Find values that already match the chart defaults (candidates to drop)
diffyml --unchanged values.yaml chart-defaults.yaml

Comparison is at key/value granularity — map keys, list items, and whole scalars. List items are matched the same way as in a normal diff: by identifier (name/id), order-independently under --ignore-order-changes or for heterogeneous lists, and otherwise positionally. A multi-line (block) string is compared as a single scalar: if any line inside it differs, the whole value is "changed" and none of its lines are reported as unchanged. Inverse mode does not line-diff inside strings (unlike the normal diff, which shows a line-by-line diff for modified multi-line strings).

Neat Mode

--neat excludes well-known noise paths injected by the Kubernetes API server, kubectl, Helm, ArgoCD, and Flux — metadata.managedFields, metadata.resourceVersion, the entire status subtree, meta.helm.sh/release-name, helm.sh/chart, argocd.argoproj.io/tracking-id, kustomize.toolkit.fluxcd.io/*, and similar paths. The full strip list lives in doc/neat.md.

# Compare a kube-apiserver-rendered manifest against your source-of-truth
# (using process substitution; diffyml reads files, not stdin)
diffyml --neat <(kubectl get deployment nginx -o yaml) source.yaml

# In a kubectl/helm/argocd diff workflow
KUBECTL_EXTERNAL_DIFF="diffyml --neat" kubectl diff -f deployment.yaml

# See what neat actually filtered
diffyml --neat --neat-explain old.yaml new.yaml

Per-layer opt-outs let you keep one bundle while stripping the rest:

# Show Helm chart-version diffs but still strip ArgoCD/Flux/managedFields
diffyml --neat --no-neat-helm old.yaml new.yaml

--neat-strip-path extends the bundle without rebuilding (requires --neat):

diffyml --neat --neat-strip-path '^metadata\.annotations\[my\.org/.*\]$' old.yaml new.yaml

--neat deliberately preserves spec.template.metadata.annotations[kubectl.kubernetes.io/restartedAt] (intentional rollout marker), spec.replicas, data/stringData (use --mask-secrets for those), and user-defined labels/annotations outside the canonical noise prefixes.

Configuration File

diffyml loads project-level defaults from .diffyml.yml (or .diffyml.yaml) in the current directory. CLI flags override config file values. Use --config to specify a custom path.

# .diffyml.yml
output: compact
ignore-order-changes: true
detect-kubernetes: false
filter:
  - "spec.containers"
exclude:
  - "status"

All CLI flags are supported as config keys (kebab-case, matching the long flag name). Unknown keys are rejected to catch typos. See .diffyml.yml.example for a complete reference with all keys and defaults.

Custom Colors

Diff colors can be customized for accessibility (e.g., colorblind-friendly palettes). Five color roles are configurable: added, removed, modified, context, and doc-name.

Colors are specified as hex (#rrggbb, #rgb).

Via config file:

# .diffyml.yml
colors:
  added: "#6aa3a5"
  removed: "#702d06"

Via environment variables (override config file):

export DIFFYML_COLOR_ADDED="#6aa3a5"
export DIFFYML_COLOR_REMOVED="#702d06"
diffyml old.yaml new.yaml

Available environment variables: DIFFYML_COLOR_ADDED, DIFFYML_COLOR_REMOVED, DIFFYML_COLOR_MODIFIED, DIFFYML_COLOR_CONTEXT, DIFFYML_COLOR_DOC_NAME.

All Flags

Complete flag reference

Output

Flag Description
-o, --output <style> Output style: detailed, compact, brief, github, gitlab, gitea, json (default detailed)
-c, --color <mode> Color usage: always, never, auto (default auto)
-t, --truecolor <mode> True color (24-bit): always, never, auto (default auto)
Comparison
Flag Description
-i, --ignore-order-changes Ignore order changes in lists
--ignore-whitespace-changes Ignore leading/trailing whitespace differences
--format-strings Canonicalize embedded JSON strings before comparison (suppresses formatting-only diffs)
-v, --ignore-value-changes Show only structural changes, exclude value changes
--detect-kubernetes Detect and match Kubernetes resources (default true)
--detect-renames Detect renamed/moved Kubernetes resources by content similarity (default true)
--ignore-api-version Ignore apiVersion when matching Kubernetes resources
-x, --no-cert-inspection Disable x509 certificate inspection
--swap Swap from/to files
-u, --unchanged Inverse diff: report keys/values equal between both files instead of differences

Filtering

Flag Description
--filter <path> Include only differences at specified paths (repeatable)
--exclude <path> Exclude differences at specified paths (repeatable)
--filter-regexp <pattern> Filter using regular expressions (repeatable)
--exclude-regexp <pattern> Exclude using regular expressions (repeatable)
--additional-identifier <field> Additional field for list item identification

Sensitive Value Masking

Flag Description
--mask-secrets Auto-mask data / stringData of Kubernetes Secret resources
--mask-path <path> Additional path to mask, dot-notation with prefix match (repeatable)
--mask-path-regexp <pattern> Additional path to mask, regex (repeatable)
--mask-placeholder <string> Placeholder for masked values (default ***)

Display

Flag Description
-b, --omit-header Omit summary header
-g, --use-go-patch-style Use Go-Patch style paths
--multi-line-context-lines <int> Context lines for multi-line strings (default 4)

Chroot

Flag Description
--chroot <path> Change root level for both files
--chroot-of-from <path> Change root level for the from file only
--chroot-of-to <path> Change root level for the to file only
--chroot-list-to-documents Treat chroot list as separate documents

AI Summary

Flag Description
-S, --summary Generate AI-powered natural language summary (requires ANTHROPIC_API_KEY)
--summary-model <model> Model for AI summary (default claude-haiku-4-5-20251001)

Other

Flag Description
--config <path> Path to config file (default .diffyml.yml in current directory)
-s, --set-exit-code Exit code 1 if differences found
-h, --help Show help
-V, --version Show version information

Library Usage

diffyml can be used as a Go library for programmatic YAML comparison.

import "github.com/szhekpisov/diffyml/pkg/diffyml"

// Compare two YAML documents
from, _ := diffyml.LoadContent("old.yaml")
to, _   := diffyml.LoadContent("new.yaml")

diffs, err := diffyml.Compare(from, to, &diffyml.Options{
    DetectKubernetes: true,
})
if err != nil {
    log.Fatal(err)
}

// Format the differences
formatter, _ := diffyml.FormatterByName("compact")
fmt.Print(formatter.Format(diffs, diffyml.DefaultFormatOptions()))

See the package documentation for the full API reference.

Security & Code Quality

Supply chain. Releases are signed with cosign (keyless Sigstore), ship SPDX SBOMs for every artifact, and carry SLSA Level 3 build provenance. Published tags are immutable. See Verifying Releases for verification commands. The repo is tracked by OpenSSF Scorecard (badge above).

Continuous checks. Every push and PR is scanned by:

Test quality. 1,500+ tests (unit, e2e, fuzz, property-based), 99%+ aggregate code coverage across the core and CLI packages, mutation testing gated per-PR (no LIVED mutant on changed lines) with an 85% post-merge efficacy floor. CI enforces a 99% aggregate coverage floor.

Reporting vulnerabilities. See SECURITY.md — preferred path is a private GitHub Security Advisory.

Contributing

Contributions welcome! Open an issue for bugs or feature requests.

Development setup

Prerequisites: Go 1.26.5+, pre-commit

git clone https://github.com/szhekpisov/diffyml.git
cd diffyml
pre-commit install

Pre-commit hooks run automatically on every commit:

Hook What it checks
gofmt Code formatting
go vet Static analysis
check-coverage Aggregate core + CLI coverage threshold (99% overall)
govulncheck Known vulnerabilities
golangci-lint 7 linters (errcheck, gocritic, gosec, govet, ineffassign, misspell, staticcheck)

Useful Make targets:

make test           # run all tests
make ci             # full CI pipeline locally (fmt + vet + test + coverage + security)
make bench          # run benchmarks
make bench-compare  # compare against alternative tools (see doc/PERFORMANCE.md)
make coverage       # generate HTML coverage report
make mutation       # run mutation testing (requires gomutants)

CI pipelines (run on every push and PR):

  • Tests — unit tests + coverage thresholds
  • Security & Static Analysis — govulncheck + golangci-lint (also runs weekly)
  • Benchmark — performance regression tracking
  • Mutation Testing — test quality validation via gomutants

Acknowledgments

This project is heavily inspired by dyff, and it wouldn't be possible without the hard work of the maintainers and contributors of that project.

License

MIT.


If you find this project useful, please consider giving it a ⭐ — it helps others discover it.

About

A fast, structural YAML diff tool — in a single-dependency binary

Topics

Resources

Security policy

Stars

61 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages