Profile
Back to NewsBack
GitHub Trending 18 min
Reader Mode
szhekpisov/diffyml: A fast, structural YAML diff tool — in a single-dependency binary

szhekpisov/diffyml: A fast, structural YAML diff tool — in a single-dependency binary

diffyml

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

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

- Homebrew - Scoop (Windows) - Nixpkgs - Go Install - Install script (Linux / macOS) - Linux packages (.deb / .rpm / .apk) - Direct binary download - Docker - From Source - Verifying Releases - Output Formats - CI Integration - Kubernetes Support - Directory Comparison - Git Integration - AI Summary - Sensitive Value Masking - Filtering - Inverse Diff - Neat Mode - Configuration File - Custom Colors - All Flags

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 | | Inverse diff (report unchanged values) | Yes (--unchanged) | No | No | | 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-abc123 → app-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