---
title: Giving the Consumer a Representative in the Working Tree (brake)
url: https://www.grahambrooks.com/post/brake-exit-criteria-for-the-consumer/
date: 2026-08-24T09:30:00-06:00
author: Graham Brooks
description: Every signal an agent uses to decide it is finished — compiler, tests, linter — is a question about the repository it is standing in. The consumers of the API it just changed are somewhere else entirely. brake makes 'who does this break?' answerable from files already in the tree, with no network, no broker, and no running service.
tags:
- ai-agents
- developer-tools
- architecture
- mcp
- rust
series:
- architecture-the-agent-can-check
---

# Giving the Consumer a Representative in the Working Tree (brake)

*An agent decides it is done when the checks it can run come back green. Every one of those checks is a question about the repository it is standing in. The team that generates a client from your OpenAPI file is not in that repository, is not in the test suite, and is not in the agent's context window — so nothing in the loop objects when a field quietly disappears from a response.*

I've been building [brake](https://github.com/grahambrooks/brake), a breaking-change gate for API contracts — OpenAPI, protobuf and GraphQL, one ruleset, no network. This post is about the specific hole it's aimed at: **exit criteria in the agentic loop are all self-referential**, and API evolution is the place where that assumption is most obviously wrong. The most recent work is the part that closes it properly, so most of this post arrives at that.

## Every exit criterion an agent has is about your repository

A coding agent finishing a task runs through some version of the same list. It builds. It runs the type checker. It runs the tests. It runs the linter. If it's disciplined, it reads the diff back before claiming completion.

Notice the shape they share, which is different from the one I wrote about with [tropism](https://www.grahambrooks.com/post/tropism-architecture-in-the-agentic-loop/). There the observation was that every signal is *local and about correctness*. Here it's narrower and sharper:

**Every one of those checks is a question whose entire answer lives inside the working tree.**

- Does this compile? — a question about these files.
- Do the tests pass? — a question about these files and the tests in them.
- Is this well-formed? — a question about these files.

Change `customer_id` to `customerId` in a response schema, update the handler, update the tests, and the whole list goes green. It compiles, because your own code changed with it. The tests pass, because you changed those too — the agent will happily do all three in one pass, and it will be *right* about each of them. Nothing anywhere in that loop is a question about the four services and the mobile client that deserialise that response into a struct with a non-optional field.

The consumer has no representative in the room. There is no test that fails on its behalf, because its tests live in its repository. There is no compile error, because it compiles against a version of the contract it fetched last month.

This is worse with agents than with people for a reason that isn't about capability. A human changing a response schema has a background process that fires — *hang on, who reads this?* — even if the answer takes a Slack message to find out. That process is memory of consequences, and it's the exact thing an agent starting from zero every session does not have. It has the contract file open in front of it and no idea that the file is a promise.

## The first move: the contract is the consumer's stand-in

The obvious answer is to go look at the consumers. That's what a schema registry does — Confluent's `BACKWARD_TRANSITIVE`, Pact's `can-i-deploy`, an internal catalogue that knows who calls what.

Those are good tools and they're all wrong for this slot in the loop, for one reason: **they need something that isn't in the repository.** A registry needs a server and a network call. `can-i-deploy` needs a broker that knows what's deployed where. A catalogue needs to be populated and current. The agent's sandbox has none of them, and a check that isn't available at the moment the code is written isn't an exit criterion, it's a code review.

So the first move brake makes is to stop trying to reach the consumer at all:

> **The contract file is the consumer's stand-in, and its previous version is the promise you already made.**

You don't need to know *who* reads `customer_id` to know that removing it breaks anyone who does. That question is answerable from two documents — the contract as it is now, and the contract as it was at the baseline — both already in the repository, one of them in git history. No network, no running service, no toolchain.

That constraint is the product argument, exactly as it was for tropism. brake works on a fresh checkout with nothing installed, which is what lets it run:

- in the second before a commit, as a pre-commit hook;
- inside an agent's sandbox, which typically has no network and no credentials;
- against a repository whose service you have never run and whose consumers you have never met.

It's also a weaker claim than a registry makes, and worth being precise about. A registry answers *did I break **our** consumers*. This answers *would this break **a** consumer* — a universally quantified claim over anyone honouring the contract.

That was where the tool sat until recently, and it was useful. It was also, in the loop, one step short of the thing you actually want to say to an agent.

## A break is a positive fact about a diff

Before the second move, the soundness story, because it's what decides which checks may block a commit.

A check that detects the **presence** of something is reliable offline. A check that detects the **absence** of something is not, because absence requires having seen every possible channel. Tropism's `unused-dep` check has a 63% false-positive rate for exactly this reason, and its architecture rules are sound for the opposite one.

Breaking-change detection sits firmly on the good side of that line:

| Question | Shape | Sound offline? |
| --- | --- | --- |
| Is `customer_id` still in the response? | presence in one document, absence in the next — both read | yes |
| Did a required parameter appear? | presence in the head document | yes |
| Does `web-checkout` read `customer_id`? | presence in a file the consumer wrote | **yes — if that file is in the tree** |
| Does *anybody* read `customer_id`? | absence, across repositories you cannot see | no |

The first two are facts about two files. There is no hidden channel through which a field can be removed invisibly, because the removal *is* the diff.

The interesting row is the third, and it's the one the rest of this post is about. "Who reads this field?" *looks* like the unanswerable fourth question. It isn't — not if the consumer has written down what it uses, and not if that writing-down is a file.

## The second move: the consumer's demands are a file, and brake already reads files

The original design document ruled out "Pact-style consumer-driven verification" on the grounds that it needs both sides running. That reasoning is correct about *provider verification* and wrong about the artifact.

**A pact is a JSON document sitting in a directory. Reading it is the same act as reading an OpenAPI file.**

That distinction is the whole of the new feature, and it's worth laying out precisely, because everything depends on where the line falls:

| | Needs a running service | brake does it |
| --- | --- | --- |
| Replay a consumer's recorded requests against the provider, compare real responses | yes | **no** |
| Check the provider's *published contract* against the consumer's recorded expectations | no | **yes** |
| Ask a broker what is deployed where (`can-i-deploy`) | needs a server | **no** |
| Name which declared consumers a diff would break | no | **yes** |

Declare what your consumers use, and the finding stops hedging:

```toml
[[consumer]]
name = "web-checkout"                       # optional — a pact names itself
format = "pact"                             # pact | graphql-operations | manifest
source = "pacts/web-checkout-*.json"        # globs allowed, expanded and sorted
provider = "payments"                       # optional — defaults to the declared provider
```

```
error[response-field-removed]: response field `customer_id` was removed
  --> api/payments-openapi.yaml:142:9
   |
   = note: breaks web-checkout — pacts/web-checkout-payments.json:88
```

"This might break somebody" became "this breaks `web-checkout`, and here is the line where it said so." No network call, no subprocess, no server was added to get there.

### Three formats, and they are not equally trustworthy

| Format | Source | Fidelity |
| --- | --- | --- |
| `pact` | Pact v2/v3/v4 HTTP interactions, JSON | High for what the consumer's tests assert — a pact only knows what its tests exercised |
| `graphql-operations` | The consumer's own `.graphql` query documents | Exact. A selection set *is* the field list, with no inference at all |
| `manifest` | A hand- or codegen-written `*.brake-uses.toml` | Whatever the author wrote — the fallback for gRPC, for consumers with no pact tests, and for third parties who will only tell you in prose |

The ordering matters and the tool is explicit about it. A pact records **one example value**, not a schema, so a field appearing in the example body is *evidence* the consumer reads it — good evidence, since pact's own verification fails when the provider omits it, but evidence. A GraphQL selection set is a statement.

That difference shows up in a detail I like a lot, because it's the sort of thing that decides whether a tool survives its first week. A demand is silent about formats, bounds, nullability and enum membership — a pact fixture just has `"id": "9f01e2b7-…"` in it. So brake copies those from the contract before comparing. Without that, every `format: uuid` in your spec becomes a false `consumer-request-rejected`, and the hook is uninstalled by Friday.

### Binding, and refusing to guess

A pact's paths are concrete — `/payments/42`. A contract's are templates — `/payments/{id}`. Joining them is a separate phase with its own failure modes:

1. Exactly one template matches → bind, and record each `{param}` value as a usage. A consumer calling `/payments/abc` has declared that it sends `abc` for `id`, which is what lets a later narrowing of `id` to `integer` be reported as breaking *that consumer* rather than in the abstract.
2. Several match → prefer the most literal. Still tied → `consumer-path-ambiguous`, and **no guess**. A guessed binding attributes a break to the wrong endpoint, which is worse than declining to attribute it.
3. None match → `consumer-endpoint-unmet`. The consumer calls something the contract doesn't document.

### Verification is the existing comparator, run sideways

This is the part I'd point at if someone asked what the design is actually made of.

The type comparator already answers "does head still satisfy what base promised?" in both directions. Put the consumer's expectation on the **base** side and the head contract on the head side, and the ordinary rules answer consumer questions:

| Comparison | Reads as |
| --- | --- |
| `compare(expectation, head, Response)` | Does the contract still produce everything the consumer reads? |
| `compare(expectation, head, Request)` | Would the contract still accept what the consumer sends? |

A response field removal on that comparison means the contract doesn't document a field the consumer expects. A newly-required request field means the contract now demands something the consumer doesn't send. There is one notion of "incompatible" in the tool rather than two that drift apart, and the whole feature is a projection of an engine that already existed.

**No baseline is involved.** Demand is compared against `head` only — brake doesn't version consumer declarations, doesn't diff a pact against its previous self, and doesn't store which consumer expected what when. That's the registry it deliberately isn't building, and it stays on the right side of the line by never needing history: a consumer's expectation is a statement about the contract as it is now.

One consequence is a genuinely new capability rather than a better version of an old one. These rules work on a **brand-new contract with no history at all** — the first commit of an API file, where a baseline diff can only shrug and say `contract-new`, can still be checked against what its consumers say they need.

### Attribution is evidence, not a finding

There is no `consumer-break` rule, and its absence is deliberate. A break is already a finding; the consumers it affects are attached to it as an `affects` list, rendered in text, JSON and SARIF (as a SARIF `relatedLocation`, which is what related locations are for).

One broken field must not produce one `response-field-removed` plus three `consumer-break`s, because a developer then has to work out that four findings are one problem.

### Where the files come from, with no network

The honest part, and the workflows differ in how much they can be trusted:

| Workflow | How the declaration arrives | Trust |
| --- | --- | --- |
| **Monorepo** | The consumer's own tests write it into `services/<name>/pacts/`; brake globs it | Strongest. As fresh as the consumer's last test run |
| **Vendored** | Consumer CI opens a pull request against the provider repository | Reviewed like code, and staleness is visible in `git log` |
| **Pulled in CI** | A prior step (`pact-broker pull`, `curl`) writes the directory; brake reads it | The network stays in the pipeline, outside the tool |

brake will not pull the files itself, under any flag, and this isn't a capability waiting for a use case. The moment a contract gate can be pointed at a URL it stops being reproducible on a laptop, in an air-gapped build, or three years from now when the broker has been decommissioned. A `source` that looks like a URL is refused when `brake.toml` is parsed. A URL *inside* a pact — the `_links` a broker stamps into every document it publishes — is data, never an instruction, and there's a self-defence test that fails if anything ever dereferences one.

The failure mode of the third row is the one that matters: a failed pull leaves the declared file absent, which is `consumer-unreachable` and exit `1`. Loud, not clean.

**brake also does not measure freshness.** A pact from eighteen months ago and one from this morning are the same bytes to a file reader, and any heuristic over mtime would break the determinism guarantees. Instead `brake consumers` prints every declaration it used with its path and a content digest:

```
payments — api/payments-openapi.yaml

  web-checkout   pacts/web-checkout-payments.json  sha256:d2f56af7
    GET  /payments              200     reads: items.amount, items.id, items.status
    GET  /payments/{id}         200,404 reads: amount, id, status

  2 of 2 endpoints have a declared consumer.
  brake knows about the consumers declared in brake.toml and no others.
```

That last line is not decoration. Without it the inventory reads as a complete census, and it is a list of files somebody remembered to declare.

## Asking before the edit, not after

Everything above still describes a gate — something that tells you afterwards. The version that changes what an agent writes is a question it can ask first.

**`who_consumes { contract, endpoint?, field? }`** over MCP returns the declared consumers of an endpoint or field, with the interaction that declares it. An agent about to delete a response field can ask who reads it *before* drafting the edit.

That's a different kind of exit criterion, and I think it's the more valuable one. A gate converts a bad change into rework. A pre-edit query converts it into a different change — and the agent is perfectly capable of choosing `expand-then-contract` over a straight removal if it knows, at planning time, that two named services read the field.

The rule that has to travel with it: **an empty answer means nobody *declared* it, not that nobody uses it.** Every surface that can produce that answer says so at the point it's produced, which turns out to matter more than it sounds — see below.

## Two questions, and an agent only ever asks one of them

Worth keeping in view, because it's the other place the loop's exit criterion and the team's differ.

```toml
[[contract]]
name = "payments"
source = "api/payments-openapi.yaml"
baseline = { git-merge-base = "origin/main" }   # did *this change* break anything?

[[contract]]
name = "payments-released"
source = "api/payments-openapi.yaml"
compatibility = "surface"
baseline = { latest-tag = "v*" }                # is the delta since the last release safe?
```

Two contracts over one artifact is the intended shape, not a workaround.

`git-merge-base` forgives anything already on the trunk. That's what makes it adoptable — a repository with two hundred existing findings still passes a commit that doesn't add a two-hundred-and-first, with no baseline file and nothing to regenerate after a refactor. Scope is a better ratchet than a snapshot, which is a lesson tropism learned first and this inherited outright.

It's also the *wrong* question at release time. A break merged three weeks ago is still a break for anyone upgrading from the last tag, and the merge-base has long since forgiven it. `latest-tag = "v*"` resolves the newest matching tag **that HEAD descends from** — ancestry matters, because a tag cut on an unrelated release branch is not a version this commit evolved from, and comparing against it reports a divergence as a break.

An agent working a task is always asking the first question. It should be. But it means the agentic exit criterion and the release exit criterion are genuinely different checks, and a team running only the first has a gate permanently one merge behind reality.

## Silence is not a pass, and this is where agents differ from people

The single most important thing in the design has nothing to do with the ruleset.

**A human skims a warning. An agent acts on the absence of one.**

A developer who runs a check and sees no output glances at the summary and notices if something looks off. An agent reads an empty findings array, concludes success, and reports the work complete. If your tool can return "I couldn't verify this" in a shape that looks like "this is fine", you've built a machine for manufacturing false confidence, and the automated consumer will hit it far more consistently than a person would.

So the verdict is structural rather than inferred:

- **`verdict` is a required field**, with `clean`, `findings` and `unavailable` as its values. A caller cannot read the findings array and skip the caveat.
- **`unverified` is a separate key**, not a low-severity finding mixed into the list. An empty `findings` with a non-empty `unverified` is *not* a pass. When the ingester meets a construct it can't model — a `discriminator`, a `not`, a pact matcher like `arrayContains`, a v4 interaction deferred to a plugin — the compared path is reported as **partial**, naming the construct and its pointer. A tool that silently ignores what it cannot parse is worse than no tool, because it manufactures confidence.
- **Exit code `1` and exit code `2` are different things.** `1` is "your API broke". `2` is "the gate is broken". Conflating them trains a team to ignore both, and over MCP the same split is `isError: false` for a finding and `isError: true` only when brake could not determine an answer.
- **A configured-but-unresolvable baseline is never treated as "no changes."** An *unconfigured* baseline exits `0` with a note, because that's a user who hasn't opted in. A *missing* one exits `2`.

### The policy that could lie, and the four constraints on it

Consumer declarations create a new way to be dishonest, and the design is at its most careful exactly there. Three policies:

| `policy` | Effect |
| --- | --- |
| `annotate` | **Default.** Severities unchanged; affected consumers are named on the finding |
| `escalate` | A `warning` becomes an `error` when a declared consumer is affected |
| `triage` | An `error` no declared consumer can observe is downgraded to a `warning` |

`escalate` is the satisfying one. `param-removed` and `security-removed` are warnings *precisely because* brake couldn't tell whether anyone relied on them — the undecidable cases from the honest-limits list. Given a declaration, it can, and the severity stops being a hedge.

`triage` is the one that can lie. Downgrading a break because no consumer declared it is the exact shape of the false clean the whole tool exists to forbid. It's offered anyway, because a team that genuinely has every consumer in one repository is otherwise being asked to treat a break nobody can observe as a blocker — and that's how a gate gets uninstalled. Four constraints make it honest:

1. It requires `completeness = "closed-world"` — an explicit, reviewable assertion by a human that the declared set is exhaustive. **brake cannot verify that claim and does not pretend to.**
2. It applies only to rules the catalogue marks *observable by demand*. A pact says nothing about `operation-id-changed` or `path-parameter-renamed` — those break generated client *code*, which no declaration models. A rule demand cannot see is never downgraded on the strength of demand's silence.
3. The floor is `warning`. Nothing is downgraded to nothing, and nothing is suppressed — a suppression still requires a written reason.
4. Every downgraded finding renders the assumption it rests on: `no declared consumer uses this — 3 consumers declared, and brake cannot know that is all of them`.

There's one further rule, `consumer-surface-unused`, that reports a suspected *absence* — an endpoint nobody declares. It's excluded from the commit gate entirely and gated behind the closed-world declaration, because "nobody uses this" is not something a file reader can know. That's the presence/absence line from earlier, applied to the tool's own new feature rather than to somebody else's.

If you take one thing from this post and apply it to a tool you own: the difference between "checked and clean" and "did not check" has to be visible in the type, not in the prose. Your agentic consumer will never read the prose.

## Being blocked is not the useful part

A finding tells the agent it broke something. That's half a job, and the less valuable half — the reason people ship breaking changes is almost never that they wanted to, it's that the safe path didn't occur to them at the moment they were blocked.

Every rule that reports a break carries an ordered list of **evolution strategies**, bound to the specific field, each with its cost. `deprecate-then-remove`, `expand-then-contract`, `version-the-endpoint`, `optional-with-default`, `widen-dont-narrow`, `dual-accept-credentials`, `reserve-the-number`. Three properties, each deliberate:

**Catalogued, not generated.** A strategy is a named technique with fixed text and a subject placeholder. Nothing is composed per call, which is what lets a test assert the wording and lets the tool stand behind it.

**Costed.** A list of options with no costs reads as a list of things that are all free, which is not a decision anyone can make. `deprecate-then-remove` is cheap unless you needed the removal this week. `version-the-endpoint` is always available and always expensive.

**Not chosen between** — and this is the interesting one. brake names the applicable strategies and stops, because which one fits depends on whether you control every consumer and whether you have a version scheme.

That refusal is usually where a tool gets criticised for being unhelpful. In an agentic loop it's the opposite, because of a real inversion of who knows what: **the agent is in a better position to choose than the tool is.** brake sees two documents and a directory of declarations. The agent can read `AGENTS.md`, the deployment config, the monorepo the consumers might be sitting in, and the versioning policy in the contributing guide. Handing it a single confident recommendation would override the one participant with more context; handing it three named, costed options with an explicit note that the choice isn't brake's is the shape that uses what the agent knows.

There's a smaller lesson buried in the implementation. A remediation names the field it's about, and that subject is carried explicitly on the change record rather than recovered from the JSON pointer — because a parameter's pointer ends in its *index*, so deriving it produced the instruction ``keep `0` optional``. Confident, wrong, and arriving in the part of the output meant to help. That same explicit subject is what the consumer attribution join later reused: the field name, used a second time.

## The same gate, one step earlier

`brake check` runs at the moment of commit, which is the last moment the change can be stopped and the worst moment to learn about it. An agent editing a contract has the intent in hand and hasn't written the change yet — and it is exactly the kind of consumer that will confidently ship `customer_id` → `customerId` because nothing told it not to.

```sh
claude mcp add brake -- brake mcp /path/to/your/repo
```

Five tools — `check_change`, `compare_contracts`, `who_consumes`, `explain_rule`, `check_repository` — and four resources: the rule catalogue, the evolution strategies, the resolved configuration, and the consumer inventory with each declaration's file and digest. Three details matter more than the list:

**`check_change` takes document text, not a path.** An agent holds an unsaved draft, and asking "is what I am about to write safe?" before writing it is the entire reason the interface exists.

**`compare_contracts` needs no configuration at all.** Two documents, no `brake.toml`, no repository — the case an agent hits when reviewing a diff in a repository nobody has set up. Configuration is an enrichment, never a prerequisite; a gate that requires onboarding before it can answer anything gets skipped.

**`brake://strategies` is readable before anything has broken.** An agent that knows `expand-then-contract` while drafting is more useful than one told about it after being blocked.

The MCP server is the same ruleset consulted earlier. It adds no rule, changes no verdict and relaxes no guarantee — if a tool here would need a rule `brake check` doesn't have, it doesn't belong here either. That's the constraint that stops it becoming a second product with a second set of opinions.

### Four skills, which are mostly warnings

Shipping alongside are four agent skills in `.claude/skills/` — plain Markdown with YAML frontmatter, so they work in Claude Code and are readable by anything else consuming that format. `api-compatibility` for editing a contract, `brake-consumer-impact` for "who uses this?", `brake-triage` for a failed hook, `brake-adopt` for adding the gate to a repository.

The interesting thing is what they contain, which is not a CLI wrapper — an agent can read `--help`. They carry the things an agent gets wrong when it works the tool out for itself:

- `unavailable` is not clean, and rounding it down to a pass manufactures exactly the confidence the tool exists to refuse.
- **An empty consumer answer means nobody *declared* it. Not that nobody uses it.**
- Exit `2` is not exit `1` — a broken gate and a broken API need different responses, and an agent that conflates them fixes the wrong file.
- brake doesn't choose between the ways out, so the agent shouldn't either — *silently*.
- A suppression is a dated, reasoned decision, never a way to get a build green.

Every one of those is a way to over-claim. That's a slightly odd thing to spend a documentation budget on, and I think it's correct: an agent handed a tool assumes the tool is right, and the failure mode of this particular tool is a confident green.

### One exclusion worth stating out loud

`--drift` runs a command out of a config file to check generated output against the committed artifact. It's the only place brake ever executes a subprocess, it's opt-in per contract behind an explicit flag, and **it is not exposed over MCP at all.**

An agent that can write `brake.toml` — which any agent editing a repository can — and then call a tool that honours a `command` field from it has arbitrary command execution, obtained through a tool whose stated purpose is reading files. There's a test named `no_tool_call_can_execute_a_declared_generator` that drives the real binary over stdio and tries to smuggle the flag through every tool, consumer arguments included. Drift stays a CLI concern.

This isn't hardening to add later. A server that exposes it is a different and much more dangerous product, and the difference is invisible from the outside.

## What it can't do

The honest section, because it's the part that determines whether you should trust a green result. The consumer feature moved this list rather than emptying it, and the item at the top is the same one, one level up.

**It sees the consumers you declared, and cannot know that is all of them.** This is the permanent limit. brake reads a directory of files; whether that directory is the whole world is a claim only a human can make, which is why `closed-world` is an explicit declaration and why the inventory ends with the sentence about it. An undeclared consumer is invisible, and the tool says so everywhere it could be misread rather than once in a footnote.

**A green run is not a passing pact verification, and is never reported as one.** brake checks that the *specification* still satisfies what consumers declared. Whether the implementation matches its own specification is what `--drift` and your test suite are for. These are easy to conflate, and conflating them is how a team ends up trusting a gate for something it never claimed.

**It never measures whether a declaration is current.** A stale pact is bytes. The content digest in `brake consumers` is what lets a reviewer notice; the tool itself will not, and pretending otherwise would manufacture the confidence it exists to refuse.

**Not a broker client, not a pact generator, not a consumer registry.** No `can-i-deploy`, no environments, no deployment state, no stored expectation timeline. brake reads demand and never writes it — generating a pact from an OpenAPI file would invert the direction that makes consumer-driven contracts worth anything.

**It compares exactly two versions.** No transitive compatibility mode. If your consumers skip versions, brake checked a hop they may not be making.

**A pact example body probably over-declares.** Consumers are told to include only what they assert on, and routinely paste whole payloads. Over-attribution errs toward blocking, which is the safe direction for a brake — but it makes `escalate` noisier than it looks, and it's an open question flagged as needing measurement against a real pact directory before `escalate` gets recommended anywhere.

**A remote `$ref` is refused, not fetched.** No network under any flag. A gate whose verdict depends on someone else's uptime is a flaky test with a good reputation. If your spec depends on one, brake reports it unreachable rather than guessing.

**`latest-tag` needs the tags to be there.** A shallow or `--no-tags` clone has none, which is the one place identical file contents can produce a different verdict on two machines. It's a reported failure rather than a silent clean result, and CI needs `fetch-depth: 0` — but it's a real footgun, and it's on the release gate, which is the one you'll notice last.

**Multi-file protobuf** needs an import-root story that OpenAPI's single-root model doesn't, and it's still an open question in the design.

**The MCP server costs 33 crates and 2.9 MB of binary,** because `rmcp`'s server feature needs `tokio` and brake is otherwise synchronous. It's behind a non-default feature for that reason. The subcommand is registered on every build regardless — a build without the feature exits `2` naming the feature rather than pretending the command doesn't exist, because a capability that silently doesn't exist is one nobody can discover.

## Taking its own medicine

One footnote that isn't really a footnote. Adding `affects` to the `Finding` struct is a breaking change to a public Rust type, and it breaks every downstream struct literal — including [forge](https://github.com/grahambrooks/forge), which consumes brake as a library.

It shipped as a deliberate, announced break, with `Finding` marked `#[non_exhaustive]` in the same change so it's the last one of its kind. Which is precisely what the tool tells everyone else to do, applied by the tool to itself, in the release that added the feature for naming who a change breaks.

## Where this goes

One sentence: **a brake on breaking API changes — one compatibility ruleset, enforced at commit time and over the whole repository, with no network, no toolchain, and no running service.**

The agentic angle isn't a feature bolted onto that; it's what the constraint buys. A check that needs nothing installed is a check that can run inside the loop, and a check that runs inside the loop is the only kind that changes what gets written rather than what gets reverted.

The generalisation I'd defend beyond this tool: **the exit criteria an agent can reach are a strict subset of the things that have to be true for the work to be done, and the gap is almost entirely made of other people.** Consumers of an API. Operators of a service. The team that owns the module you imported. Every one is a party with a stake in the change and no representative in the working tree.

The move, then, is not to reach them — you can't, not from a sandbox with no network in the second before a commit. It's to find the artifact they already wrote down, and read it. A pact, a query document, a hand-written manifest, an architecture ruleset, a schema. Turn their interest into a file, and it becomes something the loop can check. Where no such artifact exists, no amount of prompting will substitute — and the tool's job at that point is to say so, in the shape of an answer the agent cannot round down to a pass.

```yaml
repos:
  - repo: https://github.com/grahambrooks/brake
    rev: v2026.8.4
    hooks:
      - id: brake
```

```sh
brew tap grahambrooks/brake https://github.com/grahambrooks/brake
brew install brake
brake init         # finds your contracts by parsing them, and writes brake.toml
brake consumers    # who uses what, and what of it
```

The documentation grew up alongside the feature: [getting started](https://github.com/grahambrooks/brake/blob/main/docs/getting-started.md), [configuration](https://github.com/grahambrooks/brake/blob/main/docs/configuration.md), [consumer demand](https://github.com/grahambrooks/brake/blob/main/docs/consumers.md), [CI and hooks](https://github.com/grahambrooks/brake/blob/main/docs/ci.md), [the MCP server](https://github.com/grahambrooks/brake/blob/main/docs/mcp.md), [the agent skills](https://github.com/grahambrooks/brake/blob/main/docs/agent-skills.md), and the [rule catalogue](https://github.com/grahambrooks/brake/blob/main/docs/rules.md) — generated from the same source the CLI and the MCP resources read, so all three say the same thing or a test fails. [design/05-consumer-demand.md](https://github.com/grahambrooks/brake/blob/main/design/05-consumer-demand.md) carries the specification for everything above, including the argument for why `triage` is constrained the way it is.

The thing I'd most like to be wrong about: whether `who_consumes` before an edit actually changes what an agent writes, or whether it just moves the moment of being blocked half a step earlier. That's a question about model behaviour, and one round of real use settles it better than any amount of design.
