GitHub CODEOWNERS: a practical guide with an example file
How the GitHub CODEOWNERS file works: syntax, last-match-wins precedence, teams vs users, required code owner reviews, common mistakes and a realistic example.
The KnowWhyy team · · 9 min read
Someone changes the billing retry logic. The PR gets a quick approval from a teammate who has never touched billing, it merges on Friday, and on Monday the one person who knew why the retry delay was seven seconds is reading an incident channel. Nobody did anything wrong. GitHub just had no way to know who should have looked at that file.
A CODEOWNERS file is how you tell it. It's a plain text file that maps paths to people or teams, so the right reviewers are requested automatically and, if you want, required before merge. This guide covers the syntax, the precedence rule that trips most people up, teams versus users, branch protection, common mistakes, and how to use it to spread ownership instead of concentrating it.
Code owners are automatically requested for review when someone opens a pull request that changes files they own.
They are not requested on draft pull requests. When a draft is marked ready for review, code owners are notified then.
Anyone browsing a file in the repo can hover over the code owner icon to see who owns it.
With branch protection, you can make code owner approval required before merging.
Code owners are available in public repositories on GitHub Free, and in public and private repositories on GitHub Pro, Team, Enterprise Cloud and Enterprise Server.
Where the file lives
Create a file named in one of three places:
Ask your first question in minutes.
Create a workspace, connect a tool and choose what gets indexed. The free plan includes GitHub, Slack and Notion.
If more than one exists, GitHub searches in that order and uses the first one it finds. Pick .github/ and delete the others.
Three details from the docs that matter in practice:
The file is per branch. Each CODEOWNERS file assigns owners for the branch it's on.
The base branch wins. A PR uses the CODEOWNERS file from its base branch, the one it will merge into. Editing CODEOWNERS in your feature branch doesn't change who reviews that same PR.
There's a size limit. The file must be under 3 MB, or it won't load at all. You'd need a very large monorepo to hit this, but wildcards keep it small.
Syntax
Each line is a pattern followed by one or more owners:
# Comments start with #
<pattern> @user @org/team email@example.com
The patterns follow most of the same rules as .gitignore, with exceptions. GitHub's docs list three .gitignore features that don't work in CODEOWNERS:
! to negate a pattern
[ ] character ranges
escaping a leading # with \ to make it a pattern instead of a comment
Common patterns, with examples from GitHub's docs:
Pattern
Matches
*
Every file in the repo
*.js
All .js files anywhere
/docs/
The root docs directory and everything under it
docs/*
Files directly in docs/, but not docs/build-app/troubleshooting.md
apps/
Any directory named apps, anywhere in the repo
**/logs
Any logs directory, at any depth
/apps/github (no owner)
Removes ownership from that path, so anyone with write access can approve
Some other rules:
All owners for a pattern go on one line. If the same pattern appears on two lines with different owners, only the last line counts.
Owners can be users (@username), teams (@org/team-name) or email addresses linked to a GitHub account. Email addresses don't work for managed user accounts.
Paths are case sensitive, even if your laptop's file system isn't.
Invalid lines are skipped, not rejected. GitHub highlights errors when you view the file on GitHub, and the REST API lists them (more on this below).
Precedence: the last match wins
This is the rule that causes most confusion. When several patterns match a file, the last matching line in the file takes precedence, and only its owners apply. Owners are not merged.
web/app.js matches * and *.js → @acme/frontend only
api/handlers/user.js matches all three → @acme/backend only, because /api/ is the last match
The practical result: put broad rules at the top and specific rules at the bottom. A catch-all * at the end of the file overrides everything above it.
If you want both a specialist team and a broader team on a path, list both on the specific line:
/api/payments/ @acme/payments @acme/security
Teams vs users
Use teams for almost everything. GitHub's requirements:
Every owner must have write permission to the repository.
A team owner must be visible and have explicit write access to the repo. This applies even if every member already has write access some other way.
If an owner doesn't exist or lacks access, GitHub assigns no code owner for that line.
Teams are better than individuals because:
People leave and change teams. A team handle survives reorgs. A username in CODEOWNERS becomes a broken rule when that person leaves.
Review load can be shared automatically. Team maintainers can turn on code review auto assignment, which replaces the team review request with specific members. GitHub offers a round robin algorithm and a load balance algorithm, which "tries to ensure that each team member reviews an equal number of pull requests in any 30 day period." Members whose status is set to Busy are skipped.
Approval from any owner counts. When code owner review is required, an approval from any one listed owner meets the requirement. With a team, that's any member.
One interaction to know: if branch protection requires code owner review, the team request stays on the PR alongside the auto-assigned individual until that person completes the review.
A realistic example file
Here's a CODEOWNERS file for a mid-sized product repo with a web app, an API, infrastructure code and docs:
# .github/CODEOWNERS
# Order matters: the LAST matching pattern wins.
# Broad rules at the top, specific rules at the bottom.
# Default: anything not matched below
* @acme/eng-leads
# Frontend
/web/ @acme/frontend
/web/src/design-system/ @acme/design-system @acme/frontend
# Backend API
/api/ @acme/backend
/api/payments/ @acme/payments @acme/security
/api/auth/ @acme/identity @acme/security
# Database migrations need a backend reviewer plus the data team
/db/migrations/ @acme/backend @acme/data
# Infrastructure and CI
/infra/ @acme/platform
/.github/workflows/ @acme/platform
Dockerfile @acme/platform
# Dependency manifests: platform keeps an eye on new packages
package.json @acme/platform @acme/frontend
go.mod @acme/platform @acme/backend
# Docs: anyone with write access can approve
/docs/
# Protect this file itself
/.github/CODEOWNERS @acme/eng-leads
A few choices to notice. Every owner is a team. Sensitive areas (payments, auth) pair the owning team with security on the same line. /docs/ has no owner on purpose, so docs fixes don't wait on anyone in particular. And the last line protects the file itself, which GitHub's docs recommend: to fully protect a repository, define an owner for the CODEOWNERS file too.
Making code owner review required
On its own, CODEOWNERS only requests reviews. To require them:
In the repository, open Settings > Branches. Edit the rule for your default branch, or click Add classic branch protection rule and enter the branch name.
Under "Protect matching branches," select Require a pull request before merging.
Select Require review from Code Owners, then save the rule.
Admins bypass by default. Branch protection rules don't apply to people with admin permissions unless you enable "Do not allow bypassing the above settings."
Rulesets are the newer option. GitHub's docs note that rulesets offer the same code owner review requirement, and several rulesets can apply at once, while only one branch protection rule applies to a branch at a time.
Common mistakes
Most CODEOWNERS problems come from a short list:
The catch-all is at the bottom.* @acme/eng-leads as the last line makes eng-leads the only owner of everything. Move it to the top.
Owners split across lines. Writing /api/ @alice and then /api/ @bob on the next line leaves only @bob. Put both on one line.
The team lacks explicit write access. The team exists and its members have access, but the team itself was never added to the repo. GitHub assigns no owner.
Using ! to exclude a path. It doesn't work. To remove ownership from a subpath, add a later line with the path and no owner.
Wrong case./Docs/ doesn't match docs/.
Editing the file on a feature branch and expecting it to apply. Owners come from the base branch, so the change takes effect only after it merges.
Several CODEOWNERS files. Only the first one found (.github/, then root, then docs/) is used. The others mislead readers.
One person owns everything. Required review from a single owner turns a vacation into a merge freeze.
Catch errors automatically
GitHub's REST API has a List CODEOWNERS errors endpoint that returns syntax errors with line numbers and, sometimes, a suggestion. It takes an optional ref to check a specific branch:
gh api repos/acme/web-platform/codeowners/errors --jq '.errors[] | "\(.line): \(.message)"'
You can run it in CI whenever the file changes:
name: Validate CODEOWNERS
on:
pull_request:
paths:
- ".github/CODEOWNERS"
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Check for CODEOWNERS errors
env:
GH_TOKEN: ${{ github.token }}
run: |
count=$(gh api "repos/${{ github.repository }}/codeowners/errors?ref=${{ github.event.pull_request.head.sha }}" --jq '.errors | length')
if [ "$count" -gt 0 ]; then
gh api "repos/${{ github.repository }}/codeowners/errors?ref=${{ github.event.pull_request.head.sha }}" --jq '.errors[] | "\(.line): \(.message)"'
exit 1
fi
This checks syntax only. It won't tell you whether a team still makes sense as the owner. For that you need a habit.
Keeping it current
A stale CODEOWNERS file is worse than none. It sends reviews to people who left or to teams that no longer own the code. Keep it current with a few small habits:
Review it quarterly. For each line, ask whether this team still owns the path and whether it actually reviews those PRs. Use git log on the path to see who has been changing it.
Update it during reorgs. When a team splits or merges, changing CODEOWNERS belongs on the same checklist as updating Slack channels and on-call schedules.
Add it to offboarding. If any individual usernames remain, search the file for them when someone leaves.
Add owners when you add directories. A PR that creates a new top-level service should add its CODEOWNERS line in the same PR.
Using CODEOWNERS to spread ownership
CODEOWNERS can concentrate knowledge or spread it, depending on how you write it. One username on /billing/ with required review means one person sees every billing change and becomes the only person who understands billing. That's a classic low bus factor.
To spread ownership instead:
Own by team, never by person, and turn on auto assignment with the load balance algorithm so reviews rotate.
Add a second team to critical paths./api/payments/ @acme/payments @acme/backend means a backend engineer outside payments can approve, and learns the code by doing it.
Put newer engineers on owning teams early. Reviewing is one of the fastest ways to learn an area. A code review checklist helps them review with confidence.
Ask for context in PRs. Owners can only review well if the PR explains why. A pull request description template makes that the default.
Where KnowWhyy fits
CODEOWNERS tells you who should review a change. It doesn't tell you why the code works the way it does. That's in the PRs, issues and threads where it was decided. KnowWhyy indexes the GitHub repos, Slack channels and Notion pages you choose. It indexes commits, PRs, issues and their discussions, but not source code itself. In the KnowWhyy web app, a new owner can ask "why is the card retry delay seven seconds?" and get an answer that links to the PRs and threads it came from, or a clear "not found" if the sources don't cover it. See GitHub history search, or start free.
The short version
Put one CODEOWNERS file in .github/. PRs use the version on their base branch.
The last matching pattern wins and owners aren't merged, so put broad rules first and specific rules last.
Use teams with explicit write access, list all owners for a pattern on one line, and give the file an owner too.
Turn on Require review from Code Owners in branch protection or a ruleset. One approval from any listed owner is enough.
Check syntax with the codeowners/errors API in CI, and review ownership every quarter.
Use team ownership and review auto assignment to spread knowledge, not concentrate it.