Setting Up CODEOWNERS for Monorepo Packages
In separate repositories, access control does the review routing: only the team that owns a repository reviews its pull requests. A monorepo removes that boundary, so without explicit ownership every pull request is either reviewed by whoever happens to look or blocked waiting for the wrong people. A CODEOWNERS file maps paths to the teams responsible for them, and with required reviews on protected branches, a change to packages/payments/ cannot merge without a payments owner approving it. This guide writes a CODEOWNERS file for a monorepo, explains how matching works (including the last-match-wins rule that trips everyone up), and keeps the file accurate as packages move.
What goes wrong without ownership
Common symptoms in monorepos without a maintained CODEOWNERS file:
- Pull requests touching three packages sit unreviewed because each team assumes another will review.
- A change to shared infrastructure — the root
turbo.json, CI workflows, the sharedtsconfig— merges with approval from a product team that did not understand the impact. - Teams discover breaking changes to their packages after the fact, because nobody asked them.
And, once CODEOWNERS exists but is stale:
# GitHub pull request sidebar
Code owners: @acme/platform-team (for 214 files)
# ...because the only matching rule left is the catch-all at the top
How matching works
GitHub, GitLab and Bitbucket all support a CODEOWNERS file with gitignore-style patterns. GitHub reads it from .github/CODEOWNERS, the repository root, or docs/. For each changed file, the platform finds matching patterns and requests review from the owners of the last matching pattern in the file. Earlier matches are ignored. The overall monorepo organisation is covered in Monorepo Migration and Adoption.
That rule dictates the file's structure: general rules at the top, specific rules below them.
Writing the file
# .github/CODEOWNERS
# Rules are evaluated top to bottom; the LAST matching rule wins.
# 1. Catch-all: anything not claimed below goes to the platform team
* @acme/platform-team
# 2. Shared infrastructure
/.github/ @acme/platform-team
/turbo.json @acme/platform-team
/pnpm-workspace.yaml @acme/platform-team
/pnpm-lock.yaml @acme/platform-team @acme/security
/tooling/ @acme/platform-team
# 3. Applications
/apps/web/ @acme/web-team
/apps/admin/ @acme/admin-team
/apps/api/ @acme/api-team
# 4. Packages
/packages/ui/ @acme/design-system
/packages/payments/ @acme/payments
/packages/api-client/ @acme/api-team @acme/web-team
# 5. Exceptions inside packages (most specific, therefore last)
/packages/payments/src/legacy/ @acme/payments @acme/security
/packages/*/package.json @acme/platform-team
Key choices:
- Leading slashes anchor patterns to the repository root;
/apps/web/matches only that folder, whileapps/web/would match any folder namedapps/webanywhere. - Trailing slashes match directories and everything below them.
- Teams, not individuals.
@acme/paymentssurvives people changing roles; individual handles go stale quickly. - Multiple owners on one line means any one of them can approve (GitHub requests all, requires one unless configured otherwise).
Watch the last line: /packages/*/package.json comes after the package rules, so it replaces package owners for manifests — package owners would no longer be requested on their own package.json. If you want both, list both on that line: @acme/platform-team plus the relevant team, or drop the rule and rely on reviewers noticing manifest changes. Last-match-wins makes these interactions subtle, so test them.
Enforcing reviews
CODEOWNERS only requests reviews. To require them, enable branch protection (or a repository ruleset) on main with "Require review from Code Owners". Combine it with:
- Required status checks, so owner approval is not a substitute for green CI.
- Dismiss stale approvals when new commits are pushed, so owners re-review changed code.
- Restrict who can bypass, so the catch-all owner cannot be used to rubber-stamp everything.
Owner teams must have at least write access to the repository for their approval to count.
Keeping ownership accurate
CODEOWNERS rots as packages are added, renamed and moved. A few automated checks keep it honest:
# Every workspace package folder must have a specific (non catch-all) rule
for dir in apps/* packages/*; do
grep -qE "^/${dir}/" .github/CODEOWNERS || echo "No owner rule for ${dir}"
done
Run it in CI and fail when a new package has no rule. GitHub also reports syntax errors and unknown users or teams on the CODEOWNERS file page, and the gh api repos/{owner}/{repo}/codeowners/errors endpoint returns them for automation.
For large repositories, generate CODEOWNERS from metadata instead of editing it by hand: add an owners field to each package's package.json (or an Nx project tag), and have a script write the file. Ownership then lives next to the code it describes, and moving a package moves its ownership automatically.
Testing rules before merging them
Because the last match wins, a single new line can silently change ownership for thousands of files. Test changes to CODEOWNERS the way you would test code. The simplest check lists, for a sample of paths, which rule matches and which owners result. A short script can implement the matching locally with a gitignore-pattern library and print the owners for every file changed in a pull request, or for a fixed list of representative paths. Run it before and after the change and diff the output; any path whose owners changed unexpectedly shows up immediately.
Hosting platforms help too. On GitHub, the CODEOWNERS file view flags invalid lines, and opening a draft pull request that touches representative files shows exactly which reviewers are requested. Treat a CODEOWNERS change as infrastructure: owned by the platform team (as in the example), reviewed with the before-and-after output attached.
Ownership for shared and generated files
A few kinds of files do not belong to any single package and need deliberate rules.
The lockfile changes with every dependency update in every package. Assigning it to the platform team alone makes that team a bottleneck; assigning it to everyone is noise. A common compromise is to give it to platform and security for visibility but not require their approval on dependency-bot pull requests that are already constrained by grouping and automated checks.
Generated files — API clients generated from schemas, lockfile-like artefacts, snapshots — should be owned by whoever owns the generator's input, because that is who can judge the change.
Documentation in a central docs/ folder can be split by sub-folder to match product areas, or owned by a docs team with product teams as optional reviewers.
Release configuration — .changeset/config.json, release workflows — belongs with the team that runs releases, since mistakes there affect every published package.
Worked example: ownership after a migration
After merging five repositories, a team's CODEOWNERS contains only * @acme/engineering. Every pull request requests review from 60 people, and in practice nobody feels responsible. The team adds rules for each application and package based on the original repository ownership, gives shared infrastructure to the platform team, and puts the lockfile under both platform and security. With "Require review from Code Owners" enabled, review times drop because each pull request now requests the two or three people who actually know the code. A CI check that fails when a new package lacks an owner rule has caught every new package since.
Prevention and guardrails
- Order rules from general to specific, because the last match wins.
- Use teams, not individuals, and give them write access.
- Require code owner review on protected branches, alongside required checks.
- Fail CI for packages without an owner rule, or generate the file from package metadata.
Frequently Asked Questions
Does CODEOWNERS restrict who can change files? No. Anyone with write access can open a pull request that changes any file. CODEOWNERS routes and, with branch protection, requires reviews; it does not prevent edits.
What happens if a pull request touches several packages? Owners of every matching rule are requested, and with required reviews, each set of owners must approve. Large cross-cutting changes therefore need several approvals, which is usually what you want.
Does GitLab use the same syntax?
Mostly. GitLab supports the same pattern syntax and adds sections ([Payments]) with their own approval requirements. The last-match-wins rule applies within a section.
How do I handle a package with no clear owner? Assign it explicitly to the platform or architecture team rather than leaving it to the catch-all, and record it as an ownership gap. Unowned packages accumulate problems; making the gap visible is the first step to finding a real owner.
Can owners be optional reviewers instead of required?
Not per rule in GitHub's CODEOWNERS — requirement is a branch protection setting for all owners. For optional review, leave a path out of CODEOWNERS and use a pull request template or a bot to suggest reviewers.
Related
- Monorepo Migration and Adoption covers ownership as part of the migration plan.
- Enforcing Module Boundaries with Nx Tags aligns import rules with ownership scopes.
- Using Package-Level turbo.json Overrides keeps package configuration under package owners.
- Requiring 2FA for Package Maintainers covers ownership on the registry side.