proteus-diff
proteus-diff is a command-line tool that detects and classifies changes between two .proto schemas. It tells you whether a schema change is safe, potentially breaking, or definitely breaking — useful for CI checks, code reviews, or local validation before committing.
It's distributed as a standalone native binary — no JVM required. If you work with Protobuf, you can use it regardless of your stack.
TIP
proteus-diff focuses exclusively on proto3 syntax. proto2 files are rejected.
Installation
Linux (x86_64):
curl -sL https://github.com/ghostdogpr/proteus/releases/latest/download/proteus-diff-linux-x86_64 > proteus-diff && chmod +x proteus-diff && ./proteus-diff --helpmacOS (Apple Silicon / Intel):
ARCH=$(uname -m | sed 's/arm64/aarch64/') && curl -sL https://github.com/ghostdogpr/proteus/releases/latest/download/proteus-diff-macos-${ARCH} > proteus-diff && chmod +x proteus-diff && ./proteus-diff --helpThis downloads the binary to the current directory. Move it somewhere on your PATH (e.g. sudo mv proteus-diff /usr/local/bin/) to use it from anywhere.
Quick start
Compare two proto files:
proteus-diff old.proto new.protoCompare two directories of proto files (recursive):
proteus-diff old/ new/The tool prints a grouped report and exits with code 1 if any breaking change is found, 0 otherwise — so it plugs directly into CI.
Comparing git refs
You can pass git refs directly — branches, tags, or commits — instead of filesystem paths:
proteus-diff main . # main vs current working tree (including uncommitted changes)
proteus-diff HEAD ./proto # last commit vs current state of proto/ (uncommitted-only diff)
proteus-diff main HEAD # compare PR against main
proteus-diff HEAD~1 HEAD # what changed in the last commit
proteus-diff v0.1.0 v0.2.0 # compare two releasesTIP
Mix git refs and filesystem paths freely. Passing . or any directory path for the "new" side compares against your current working tree, so you can preview uncommitted changes before committing.
Resolution rule: if the argument exists as a file or directory, it's treated as a filesystem path. Otherwise, proteus-diff tries to resolve it as a git ref with git rev-parse. If neither works, you get an error.
TIP
If a file or directory happens to share a name with a ref (e.g. a folder named main), the filesystem wins by default. Use the git: prefix to force git mode:
proteus-diff git:main ./proto # force git refGit submodules
If your protos live in a git submodule, cd into the submodule and run proteus-diff with the submodule's own refs:
cd path/to/submodule
proteus-diff main HEADGit commands resolve correctly against the submodule's repository.
Compatibility modes
proteus-diff evaluates each change against one of three compatibility axes. Pick the one that matches what your consumers rely on:
- wire — field numbers, wire types, and enum numeric values matter. Renaming a field is fine (binary compatibility is preserved); changing a field number is not.
- source — field names, type names, and declaration order matter. Renaming a field is breaking (source code referencing it will fail); reordering fields is a warning.
- strictest — reports the worst of the two. This is the default and recommended when your consumers include both binary-only and source-level clients.
Set the mode with -m:
proteus-diff old.proto new.proto -m wireOutput
Text format (default)
Changes are grouped by file, then by enclosing message/enum/service. Within each group, entries are sorted with errors first. Each entry shows the severity, the change kind in brackets (the same name you'd use with -o), and a description:
Proto changes (3): 1 error, 1 warning, 1 info
user.proto
User (3)
error [FieldRemoved] field 'email' removed
warning [FieldAdded] field 'phone' added
info [CommentChanged] comment changed on 'User'When connected to a terminal, the file header is bold cyan, the type name is bold, and the severity word is colored red / yellow / blue. Colors are auto-disabled when piped, so the change kind in brackets stays the primary lookup key for -o overrides.
JSON format
Use -f json for machine-consumable output:
proteus-diff old.proto new.proto -f json[
{"type": "FieldRemoved", "severity": "error", "path": ["user.proto", "User"], "message": "user.proto.User: field 'email' removed"},
{"type": "FieldAdded", "severity": "warning", "path": ["user.proto", "User"], "message": "user.proto.User: field 'phone' added"}
]Pipe this into jq or any JSON-aware tool to build custom reporters or CI checks.
Markdown format
Use -f markdown to produce GitHub/GitLab-friendly output, ideal for PR comments:
proteus-diff main HEAD -f markdown## Proto changes (3)
- 🔴 1 error
- 🟡 1 warning
- 🔵 1 info
### `user.proto`
#### `User`
- 🔴 **FieldRemoved** — field 'email' removed
- 🟡 **FieldAdded** — field 'phone' added
- 🔵 **CommentChanged** — comment changed on 'User'The structure mirrors the text format: H3 per file, H4 per enclosing message/enum/service, errors first within each group. File-level events (FileAdded/FileRemoved/PackageChanged/import changes) appear directly under the file heading, before any type sub-sections.
Options
| Flag | Description | Default |
|---|---|---|
-m, --mode <mode> | Compatibility axis: wire, source, or strictest | strictest |
-s, --severity <severity> | Minimum severity to display: error, warning, or info | warning |
-f, --format <format> | Output format: text, json, or markdown | text |
--fail-on <severity> | Exit code 1 if any change at this severity or above is reported | error |
-o, --override <entry> | Severity override (repeatable) — see below | — |
--color <mode> | Color output: auto, always, or never | auto |
-v, --version | Print version and exit | — |
-h, --help | Print help and exit | — |
Exit codes
0— no changes at or above the--fail-onseverity1— at least one change at or above the--fail-onseverity2— invalid arguments or file/parse errors
Severity overrides
The built-in severity mapping reflects common expectations (removing a field is an error, adding one is a warning, etc.), but your team may disagree on some cases. Use -o to override individual change types per mode:
proteus-diff old.proto new.proto \
-o wire.FieldRemoved=info \
-o source.FieldRenamed=warningThe format is mode.ChangeType=severity, where:
modeiswireorsource(strictestis derived from the other two)ChangeTypeis one of the values belowseverityiserror,warning, orinfo
The flag is repeatable. Unspecified change types keep their defaults.
Change types
| Category | Types |
|---|---|
| File / package | FileAdded, FileRemoved, PackageChanged |
| Import | ImportAdded, ImportRemoved, ImportModifierChanged |
| Message | MessageAdded, MessageRemoved, MessageRenamed, MessageMoved |
| Field | FieldAdded, FieldRemoved, FieldNumberChanged, FieldRenamed, FieldTypeChanged, FieldTypeRefRenamed, FieldOptionalityChanged, FieldOrderChanged, FieldOneOfChanged, OneOfRenamed |
| Enum | EnumAdded, EnumRemoved, EnumRenamed, EnumMoved, EnumValueAdded, EnumValueRemoved, EnumValueNumberChanged, EnumValueRenamed |
| Reserved / options | ReservedAdded, ReservedRemoved, OptionAdded, OptionRemoved, OptionChanged |
| Service / RPC | ServiceAdded, ServiceRemoved, RpcAdded, RpcRemoved, RpcRequestTypeChanged, RpcResponseTypeChanged, RpcStreamingChanged |
| Comments | CommentAdded, CommentRemoved, CommentChanged |
Examples
CI check — fail on any breaking change:
proteus-diff proto/main proto/prFail on warnings too (stricter policy):
proteus-diff old/ new/ --fail-on warningShow everything including info-level changes:
proteus-diff old/ new/ -s infoWire-only compatibility check (good for binary-only consumers):
proteus-diff old/ new/ -m wireMachine-readable output for a custom reporter:
proteus-diff old/ new/ -f json | jq '.[] | select(.severity == "error")'Per-team severity policy — renaming fields is fine in source mode:
proteus-diff old/ new/ -m source -o source.FieldRenamed=infoNotes on detection
- Message / enum renames are detected by comparing the full structure (fields, values, options). A renamed type with identical contents is reported as
MessageRenamed/EnumRenamedrather than add + remove. - Cross-file moves are detected when comparing directories: a top-level type that moved between files is reported as
MessageMoved/EnumMoved. - Type-ref renames (e.g. a field referring to a renamed message) are reported as
FieldTypeRefRenamed, notFieldTypeChanged, so they don't block wire compatibility. - Removing a field and reserving its number is treated as a safe change on the wire axis:
FieldRemoved/EnumValueRemoveddrops from error to info when the same number appears in areserveddeclaration in the new schema. Removing without reserving stays an error, since the number could later be reused with a different type and break existing clients.