Skip to content

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):

bash
curl -sL https://github.com/ghostdogpr/proteus/releases/latest/download/proteus-diff-linux-x86_64 > proteus-diff && chmod +x proteus-diff && ./proteus-diff --help

macOS (Apple Silicon / Intel):

bash
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 --help

This 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:

bash
proteus-diff old.proto new.proto

Compare two directories of proto files (recursive):

bash
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:

bash
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 releases

TIP

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:

bash
proteus-diff git:main ./proto     # force git ref

Git submodules

If your protos live in a git submodule, cd into the submodule and run proteus-diff with the submodule's own refs:

bash
cd path/to/submodule
proteus-diff main HEAD

Git 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:

bash
proteus-diff old.proto new.proto -m wire

Output

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:

bash
proteus-diff old.proto new.proto -f json
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:

bash
proteus-diff main HEAD -f markdown
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

FlagDescriptionDefault
-m, --mode <mode>Compatibility axis: wire, source, or stricteststrictest
-s, --severity <severity>Minimum severity to display: error, warning, or infowarning
-f, --format <format>Output format: text, json, or markdowntext
--fail-on <severity>Exit code 1 if any change at this severity or above is reportederror
-o, --override <entry>Severity override (repeatable) — see below
--color <mode>Color output: auto, always, or neverauto
-v, --versionPrint version and exit
-h, --helpPrint help and exit

Exit codes

  • 0 — no changes at or above the --fail-on severity
  • 1 — at least one change at or above the --fail-on severity
  • 2 — 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:

bash
proteus-diff old.proto new.proto \
  -o wire.FieldRemoved=info \
  -o source.FieldRenamed=warning

The format is mode.ChangeType=severity, where:

  • mode is wire or source (strictest is derived from the other two)
  • ChangeType is one of the values below
  • severity is error, warning, or info

The flag is repeatable. Unspecified change types keep their defaults.

Change types

CategoryTypes
File / packageFileAdded, FileRemoved, PackageChanged
ImportImportAdded, ImportRemoved, ImportModifierChanged
MessageMessageAdded, MessageRemoved, MessageRenamed, MessageMoved
FieldFieldAdded, FieldRemoved, FieldNumberChanged, FieldRenamed, FieldTypeChanged, FieldTypeRefRenamed, FieldOptionalityChanged, FieldOrderChanged, FieldOneOfChanged, OneOfRenamed
EnumEnumAdded, EnumRemoved, EnumRenamed, EnumMoved, EnumValueAdded, EnumValueRemoved, EnumValueNumberChanged, EnumValueRenamed
Reserved / optionsReservedAdded, ReservedRemoved, OptionAdded, OptionRemoved, OptionChanged
Service / RPCServiceAdded, ServiceRemoved, RpcAdded, RpcRemoved, RpcRequestTypeChanged, RpcResponseTypeChanged, RpcStreamingChanged
CommentsCommentAdded, CommentRemoved, CommentChanged

Examples

CI check — fail on any breaking change:

bash
proteus-diff proto/main proto/pr

Fail on warnings too (stricter policy):

bash
proteus-diff old/ new/ --fail-on warning

Show everything including info-level changes:

bash
proteus-diff old/ new/ -s info

Wire-only compatibility check (good for binary-only consumers):

bash
proteus-diff old/ new/ -m wire

Machine-readable output for a custom reporter:

bash
proteus-diff old/ new/ -f json | jq '.[] | select(.severity == "error")'

Per-team severity policy — renaming fields is fine in source mode:

bash
proteus-diff old/ new/ -m source -o source.FieldRenamed=info

Notes 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 / EnumRenamed rather 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, not FieldTypeChanged, 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 / EnumValueRemoved drops from error to info when the same number appears in a reserved declaration 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.