← All posts

Code ownership without slow reviews - how to design CODEOWNERS so review stays fast

A CODEOWNERS file can protect the code that matters or make every pull request wait on the same two people. How to design ownership around risk, teams and fewer approvals for low-risk work, and how Rezee enforces it.

Oct 18, 2026 · 15 min read · Kash Gohil · LinkedIn · X

It usually starts with a good intention and one line:

*   @kash @aki

Two senior people want to see every change. For a month it works. Then the team grows, and every pull request waits on the same two people. One of them goes on holiday, and half the branches stop. Authors start to ping in chat to get a review. The owners start to approve without reading, because the queue is the problem now, not the code. Six months later somebody proposes to delete the file.

The file was not the problem. Its design was. Code ownership is one of the cheapest controls a team can add, and it can also be the most expensive. The difference is a handful of decisions about what to own, who owns it, and what you ask of them. This post covers those decisions. They work with any tool that reads a CODEOWNERS file. The last section shows how Rezee enforces them.

What ownership is for

Code review asks "is this change good?" Anybody on the team can answer that. Ownership asks a narrower question: "did the people accountable for this part of the system agree?"

Those are two different questions. When a team mixes them up, ownership grows to cover everything, because every part of the code could use a good review. But a good review does not need an owner. It needs a second pair of eyes, and your normal approval count already asks for that.

So ownership is for the places where the wrong change is expensive or hard to undo, and where specific people carry the consequences:

  • authentication, sessions and secrets
  • database migrations
  • billing and anything that moves money
  • CI, deploy and infrastructure configuration
  • public API contracts
  • the CODEOWNERS file itself

Everything else can have no owner. That is not neglect. It is a decision that normal review is enough there.

Why ownership makes review slow

The same failures appear in most teams, and each one makes the next one worse.

The catch-all. A line like * @leads makes every change wait on the same people. Their queue becomes the team's speed limit.

Names instead of teams. A path owned by one person stops when that person is away. The work does not need that person. It needs somebody who knows the area.

The sole owner. If only one person owns a path, nobody can approve that person's own changes to it. The team then blocks its most knowledgeable person, or somebody turns the rule off.

Pings on every push. Some setups ask the owners again each time the branch changes. Owners learn to ignore the notification, and then it is useless for the review that needs them.

Approvals you cannot see. A pull request changes 30 files. The author cannot tell which files wait on whom. The reviewer cannot tell why they were asked. Both guess, and both wait.

One price for every change. A typo fix in the README needs the same two approvals as a migration. People learn that review is a toll, not a check.

None of these is a reason to drop ownership. Each one is a design choice with a better option.

How a CODEOWNERS file is read

Before the design rules, here are the reading rules. Most surprises come from them. A CODEOWNERS file has one rule per line: a path pattern, then zero or more owners. Lines that start with # are comments.

The last matching line wins. For each changed path, the tool reads the file from the bottom up. The first line whose pattern matches decides. Earlier lines do not add to it. So put general rules first and specific rules last. A specific rule above a general one never applies.

Patterns follow a subset of gitignore:

  • A / at the start, or anywhere inside the pattern, anchors it to the repository root. Without one, the pattern matches at any depth.
  • * matches inside one directory. ** crosses directories.
  • A trailing / means a directory and everything under it.
  • dir/* means the files directly in dir, not its subdirectories.
  • ! (negation) and [ ] (character ranges) are gitignore features that CODEOWNERS does not support.

A line with no owners clears ownership. That is how you carve generated or vendored files out of an owned directory.

Owners are handles. An owner is @user, @org/team or an email address.

1. Own risk, not territory

Start from the list in the first section, not from your org chart. For each path, ask: if a wrong change lands here, how bad is it, and how hard is it to undo? Own the paths where the answer is "bad" or "hard". Leave the rest unowned.

A short file is a feature. Each line is a promise that specific people will look at specific changes quickly. Every extra line makes that promise weaker.

2. Name teams, not people

Write @security, not @aki. With a team, one approval from any member is enough. That spreads the load and survives holidays.

A team of one is a person with a different name. Make every owning team at least two people who can actually review the area. If you only have one such person, that is a real risk in the team, and a rule in a file does not fix it. It just makes the risk visible every day.

3. Write general first, specific last, and carve out what nobody reads

Because the last match wins, order is the design. Here is a file for a web repository with a runner client, CI configuration and some generated code:

# Most of this repository has no owners. Normal review covers it.

# Secret handling in the runner client
/src/runner/                 @security

# Generated from the API schema: nobody reviews it line by line
/src/runner/generated/

# CI, deploys and dependencies
/.rezee/                     @platform
/package.json                @platform

# Who owns what is itself owned
/.rezee/CODEOWNERS           @platform @security

Read it bottom up for a few paths:

  • src/runner/mask.ts matches /src/runner/. Owner: @security.
  • src/runner/generated/types.ts matches the empty line first. No owner.
  • .rezee/CODEOWNERS matches its own line before /.rezee/. Owners: @platform or @security, and either one is enough.
  • src/surfaces/ship.tsx matches nothing. No owner.

4. Read the file from the target branch

A pull request can change the CODEOWNERS file. If the tool reads the file from the pull request's own branch, the pull request can choose who approves it. Delete a line, and the owners disappear for that change.

So the owners of a change must come from the file on the branch it merges into, at the commit it merges into. A change to the ownership rules then needs approval from the current owners, like any other change to an owned path.

5. Make a typo fail closed, and tell people about it

Handles go out of date. A team is renamed, a person leaves, somebody types @securty. The question is what happens to the paths on that line.

If the tool ignores a handle it cannot resolve, the path has no owner. A typo then quietly removes protection from the most sensitive code you have. We think that is the wrong default. A handle that names nobody should keep the path owed, so the pull request stays blocked until somebody fixes the file. The tool should also tell you about the bad line before a pull request runs into it, with the line number.

Blocking is annoying for one afternoon. A protection that silently fell open is a problem for as long as nobody notices.

6. Let a sole owner land their own work

Sometimes one person really is the only owner of a path, for a while. Their own pull requests to that path can never get an owner's approval, because an author cannot approve their own change.

There are two honest fixes. Add a second owner, or let a path whose only possible approver is the author pass the owner check. The second rule is safe if it is narrow. It applies only when every owner, and every member of every owning team, is the author. It does not apply to a line whose owners name nobody. That case is a typo, not a sole owner, and it stays blocked.

Note what the rule does not remove. The pull request still needs your normal approval count from somebody. Only the owner requirement goes away for that path.

7. Ask owners once, and ask a team as one request

When a pull request opens, or leaves draft, ask the owners of the changed paths for review. Do not ask anybody while it is a draft. A draft is not asking yet.

After that, do not ask the same owners again on every push. If a new push touches a path with new owners, ask those owners. People who were already asked stay asked, and they do not get a second notification.

Ask a team as one request, not as one request per member. Tell each member once. When any member gives a verdict, the team's request is answered. Otherwise five people each think that somebody else has it, or all five review the same change.

8. Show what is owed, file by file

The author and the reviewers should never have to work out ownership by reading the CODEOWNERS file. A good pull request page shows:

  • the owners beside each changed file, and which files an owner already approved
  • under the merge button, which approvals are still owed, grouped by who can give them
  • beside each requested reviewer, how many of the changed files they own

With this, the reviewer knows why they are there. The author knows whom to ask. Neither of them waits on a guess.

9. Ask fewer approvals of low-risk changes, but never zero

If your branch asks for two approvals, ask whether every change needs two. A docs fix or a change that only edits comments does not carry the risk of a schema change. Asking the same of both teaches people that review is a toll.

A lower count for low-risk work is fine if the rules are strict:

  • Never below one. Somebody other than the author still reads it.
  • Low-risk means every changed file. One risky file in the diff, and the full count applies.
  • Be careful what counts as docs. Plain Markdown and text are safe by default. MDX is not, because MDX files can contain and run components.
  • Be careful what counts as a comment. Some comments change how code is checked or built: eslint-disable, @ts-ignore, prettier-ignore, a license header that a tool keeps. A change to one of those is a code change.
  • Owners still apply. A lower count never removes an owner's approval. If the README is owned, its owners still approve.
  • Not for agents. A pull request that an agent wrote gets the full count. An agent can write a large "docs only" change faster than anybody can read it. A person should read that change with the full count behind it.

10. Keep approvals through cosmetic pushes, and only those

An approval belongs to the version of the change that the reviewer read. If the author then pushes real code, the approval should not count anymore. If the author only fixes a comment or reformats the file, asking for the review again wastes everybody's time.

So keep an approval when the base is the same and the new push changes only comments or formatting. Drop it on any other change. If your team wants every push reviewed again, make that a setting on the branch, not a habit.

A worked example

Take the web repository and the file above. The branch rule for main asks for two approvals, asks for code owners' approval, and allows one approval for low-risk changes.

kobeni, an agent, opens web#131, "Mask secret values in the runner env". It changes six files:

  • src/runner/mask.ts, src/runner/mask.test.ts and src/runner/secrets.ts, which @security owns
  • .rezee/actions.yml, which @platform owns
  • src/surfaces/ship.tsx and README.md, which have no owner

When the pull request opens, the security and platform teams each get one request, and each member hears about it once. Under the merge button the page says "Needs approval from code owners for 4 files". Under that are two groups, the three runner files waiting on @security and the CI file waiting on @platform.

aki is on the security team and approves. One approval covers every path that aki owns, so all three runner files are done. power is on the platform team and approves the CI file. The owner check is now satisfied, and so are the two approvals. kobeni is an agent, so the low-risk count never applied.

Now kash opens a pull request that fixes a typo in README.md, and nothing else. No owner owns the file, and every changed file is low-risk. The pull request needs one approval instead of two. The page says why: "Low-risk paths only: 1 approval needed".

Finally, somebody renames the security team and forgets the file. The line /src/runner/ @security now names nobody. The repository's settings show the problem with its line number. A pull request that changes the three runner files now says "Needs approval from @security for 3 files". It stays blocked until somebody fixes the file. Nothing falls open.

What to drop

  • The catch-all line. If you want a second pair of eyes on everything, that is your approval count, not ownership.
  • Individuals as owners. Use teams of at least two.
  • Re-requesting review after every push. Ask once. Ask again only when you change something that matters to that reviewer.
  • Ownership by org chart. Own the places where a mistake is expensive.

How Rezee does it

Rezee reads a CODEOWNERS file by the gitignore subset above, so a file written for GitHub means the same thing. It looks for .rezee/CODEOWNERS first, then .github/CODEOWNERS, CODEOWNERS and docs/CODEOWNERS in the order GitHub uses, and the first one it finds is the file.

Owners resolve to people and teams in the workspace. @name is a member's username, or a team with that name. @org/team is the team named after the slash. An email address is the member with that address. Team names match without case, with spaces read as hyphens. Rezee resolves handles each time it asks, so a person who joins a team counts at once, with no push to the file.

The file comes from the base. For a pull request, Rezee reads the file at the tip of the branch it merges into. A pull request cannot change its own owners.

Problems show in settings. The repository's branch protection settings show the file that Rezee found on the default branch and how many rules it has. They also list each problem with its line number. Examples are a negation pattern, a handle that is not a valid owner, and "@securty names nobody in this workspace".

Owner approval is a branch rule. A protection rule can turn on "Require approval from code owners". Then each changed path's owners must approve:

  • One approval covers every path the approver owns, directly or through a team.
  • The author cannot approve their own pull request.
  • A path whose owners name nobody stays owed.
  • A path that only the author could approve needs no owner's approval.
  • If Rezee cannot run the check, the merge is blocked with "Could not check code owners; try again shortly". It is a rule somebody set, so it fails closed.
  • If a pull request changes too many files for Rezee to check, the merge is blocked with "Too many changed files to check code owners".

Requests go out by themselves. When a pull request opens, gets a push, or leaves draft, Rezee asks the owners of its changed paths. Drafts ask nobody. An owner already asked, answered or not, is not asked again. A team is one request, its members are told once, and any member's verdict answers it.

The pull request page shows ownership. Owner chips sit beside each changed file and turn into a check when an owner approves. A file that only its author owns says "Only the author owns this file, so it needs no other approval". The owed approvals are listed under the merge button. Each requested reviewer shows how many files they own, for example "owns 3 files". Agents read the same thing: the pull_mergeability MCP tool returns the owed paths, grouped by the handles that can approve them.

Low-risk approvals are a branch rule too. A protection rule can set "Low-risk approvals" and "Low-risk paths":

  • With no paths set, low-risk means *.md, *.markdown and *.txt. MDX is not on that list.
  • The count is at least 1, and the database refuses anything lower.
  • It applies only to a pull request from a person, never from an agent.
  • It applies when every changed file is a low-risk path, or when the whole diff changes only comments and formatting.
  • Rezee judges "comments and formatting" by parsing the code. Go, JavaScript, TypeScript and JSON are judged this way. A change to any other kind of file counts as a real change. A change to a directive comment, such as eslint-disable or @ts-ignore, also counts as a real change.
  • If several protection rules match a branch, each of them must allow the lower count. A path is low-risk only if it is low-risk under every rule.
  • Code owners still apply. The lower count never removes an owner's approval.

Approvals survive cosmetic pushes. A review counts for the version it read. It also counts for a later version if the base is the same and the push changed only comments or formatting, judged the same way. A branch rule, "Review again after comment or formatting changes", turns that off, so that any push needs a new review.

For the review habits that sit on top of all this, see code review best practices. For how branches and pull requests fit with the rest of the workspace, see Code & Ship.

FAQ

Do I need Rezee to use anything in this guide?

No. The design rules work with any tool that reads a CODEOWNERS file. Some of them, like the sole owner rule and the lower count for low-risk work, depend on what your tool supports. If it does not support them, the advice still holds: keep the file short, own risk, and name teams.

Should every file have an owner?

No. A file with no owner still gets your normal review. Ownership is for the paths where specific people must agree. A file that owns everything protects nothing in particular, and it makes every pull request slower.

What happens when a pull request changes the CODEOWNERS file?

The file's own line decides who approves it, and that line is read from the branch the pull request merges into. So the current owners of the file approve the change. The new rules apply only after it merges.

Is a lower approval count for docs a security risk?

It can be, if "docs" is defined loosely. Keep it to plain text formats. Never go below one approval. Never apply it to an agent's pull request, and never let it remove an owner's approval. With those rules, the risk is a typo in a README that one person read, not code that nobody read.