Signal from noise: the three-layer filter that keeps reviews readable.
Built-in defaults, `.commitbriefignore`, and `COMMITBRIEF.md` semantic filtering — how they compose, last-wins semantics, and how negative patterns revert a default.
Code review tools tend to fail in one of two directions. They either show you too much — every test fixture flagged, every vendored file commented on, every generated stub treated as your work — or they show you too little, hiding behind aggressive defaults that filter out things you actually wanted to see. The middle ground is harder than it looks because the right line is not the same for every project.
CommitBrief’s filter has three layers, applied in order. Each layer can override the one before it. Together they give you a precise control surface without forcing you to write your own ignore logic from scratch. This post walks through the three layers, the last-wins semantics that connects them, and a working example from a Go monorepo.
The three layers
The filter applies left to right, last layer wins:
-
Built-in defaults — compiled into the binary. Filters obvious noise: binaries, lock files,
vendor/**,node_modules/**, generated code, build artifacts, IDE metadata, OS noise (.DS_Store,Thumbs.db). You don’t have to configure this; it’s what makes “first run on day one” feel sensible. -
.commitbriefignore— a file at the repo root, gitignore syntax, team-shared. This is your project-specific filter. You can ignore additional paths the built-ins didn’t anticipate, and you can use negative patterns (!path) to revert built-in exclusions on a per-project basis. -
Semantic filter in
COMMITBRIEF.md— natural-language rules the LLM applies to whatever survives the first two layers. “Don’t flagtesting.Tusage in_test.gofiles.” “Skip findings on generated protobuf code.” The first two layers operate on paths; this one operates on content and intent.
The first two layers prune what the LLM ever sees. The third layer prunes what the LLM bothers reporting.
Why “last wins” matters
Negative patterns are the part most users underuse. The built-in filter skips vendor/** because most teams genuinely don’t want review on vendored code. But some teams maintain an internal fork of a library inside their vendor tree and treat that fork as first-party code. The built-in filter doesn’t know about your fork.
The .commitbriefignore for such a project looks like:
# Re-include our internal fork even though built-in excludes vendor/.
!vendor/internal/our-fork/**
The leading ! flips the rule. The fork is back in scope without you having to disable the entire built-in filter and re-list every other vendor exclusion.
This works the other way too. If the built-in includes a path you don’t want reviewed, you add it to .commitbriefignore with a standard pattern:
# This directory is auto-generated; reviewing it just produces noise.
internal/codegen/**
# Test fixtures we never modify by hand.
testdata/**
The semantic layer: where the LLM’s judgment lives
The path-based layers can answer “should this file ever be looked at?” They can’t answer “this file is in scope but findings of type X on it are nonsense.” That’s the semantic layer’s job, expressed in plain English inside COMMITBRIEF.md:
## Filtering
- Don't flag `testing.T` usage in `_test.go` files. It is intended.
- Generated protobuf code in `internal/pb/` is in scope, but only flag
changes to hand-written `.go` files in that directory, not the
regenerated `.pb.go` stubs.
- In migration files (`migrations/*.sql`), do not flag missing
rollback statements. We deliberately don't write them.
A path-based filter can’t make this distinction without producing a coarser version of it. The semantic layer is what closes the gap between “this kind of file” and “this kind of finding on this kind of file.”
See what each layer dropped: dry-run
When you change .commitbriefignore or add a semantic rule, you want to confirm it did what you intended before spending tokens on a real review. commitbrief dry-run --staged walks the whole pipeline without an API call and reports per-layer counts:
files in staged diff: 12
built-in ignore filtered: 4 (vendor/, node_modules/, go.sum)
.commitbriefignore net: -1 (re-included vendor/internal/our-fork/x.go)
files going to LLM: 9
The interesting line is the third one. The negative number means a pattern in your .commitbriefignore brought a file back into scope that the built-in had excluded. If you saw a positive number there, your .commitbriefignore was net-pruning rather than net-adding. Both are valid; both are visible.
A worked example, end to end
A Go monorepo with these properties:
- Vendors most of its dependencies the standard way, but maintains an internal fork of one library under
vendor/internal/. - Generates gRPC stubs under
internal/pb/and treats hand-written code in the same directory as first-party. - Writes table-driven tests heavily, and uses
testing.Teverywhere. - Has a SQL migration directory and intentionally doesn’t write down migrations.
The two configuration files for this repo:
# .commitbriefignore
!vendor/internal/our-fork/**
testdata/**
# COMMITBRIEF.md (filtering section only)
- `testing.T` usage in `_test.go` is expected; don't flag it.
- In `internal/pb/`, ignore findings on `*.pb.go` (regenerated). Do flag
hand-written files in the same directory.
- In `migrations/*.sql`, missing rollback statements are intentional.
The result: vendored libraries skipped except the internal fork; test fixtures skipped; the LLM only reports on hand-written code in internal/pb/, and the team doesn’t get a wall of “missing rollback” findings on every migration PR.
This setup took maybe ten minutes to write. It will save hours of reviewer attention across a quarter.
Calibration is the actual skill
A common mistake is to configure all three layers on day one based on what you think you’ll want. The better workflow:
- Run
commitbrief --stagedfor a week with no.commitbriefignoreand the defaultCOMMITBRIEF.md. - Note which findings you consistently ignore. Those are signal-to-noise problems.
- Decide whether each one is a path problem (add to
.commitbriefignore) or a semantic problem (add toCOMMITBRIEF.md). - Run
dry-runafter each change to confirm the impact.
Filter calibration is a real skill, and like most skills it improves with the volume of data you’ve seen. The COMMITBRIEF.md anatomy post covers the semantic-rule side in more depth; this post covered the path side. Together they give you the surface area to keep your reviews dense — every finding worth reading.
If you haven’t yet wired commitbrief --staged into the pre-PR flow, the workflow post is the starting point. The filter is only useful in proportion to how often you actually run the tool.
Related reading
- Aug 19, 2026Blocking vs. nitpicking: shaping review tone with `OUTPUT.md`.
Encoding team norms into the review output. Severity scales, finding format, what gets surfaced and what gets quietly suppressed.
- Jun 5, 2026Anatomy of a `COMMITBRIEF.md`: turning team tribal knowledge into a system prompt.
What a good rules file looks like, what doesn't belong in it, and why the LLM's effectiveness as a zeroth reviewer depends on the file being written well.