GitCODEOWNERS File

CODEOWNERS File

A CODEOWNERS file maps file paths to the people or teams responsible for them. When a PR touches a path, GitHub automatically requests review from the matching owners. Combine it with branch protection requiring code-owner review, and ownership becomes enforceable instead of polite. It’s the single highest-leverage governance file in a multi-team repo.

Where the file lives

GitHub looks for CODEOWNERS in three locations (any one works; pick one):

  • .github/CODEOWNERS ← most common

  • CODEOWNERS at the repository root

  • docs/CODEOWNERS

If multiple exist, the first one found wins (in the order above).

Basic syntax

.github/CODEOWNERS

Text
# Each line: path-pattern  one-or-more-owners
# Owners can be users (@alice), teams (@acme-co/frontend), or emails.
# Lines starting with # are comments.

# Global fallback — every file has these owners unless overridden below
*                                @alice @bob

# Frontend
/src/components/                 @acme-co/frontend
/src/pages/                      @acme-co/frontend
*.css                            @acme-co/design

# Backend
/server/                         @acme-co/backend
/migrations/                     @acme-co/backend @acme-co/dbas

# Infra
/.github/workflows/              @acme-co/devops
/terraform/                      @acme-co/devops

# Docs
/docs/                           @charlie
*.md                             @charlie

# A specific file
/SECURITY.md                     @acme-co/security
Three kinds of owners
  • Users@alice, @bob. Must have write access to the repo.

  • Teams@acme-co/frontend. Team must have write access; team members are notified.

  • Email addressesalice@example.com. Useful for non-GitHub recipients; doesn’t request review on GitHub.

Path patterns

Patterns look like .gitignore but with a few important differences:

  • * matches one path segment; ** matches any number.

  • /foo/ matches the foo directory and everything inside.

  • A leading / anchors to the repo root; without it, the pattern matches anywhere.

  • *.js matches .js files at any depth.

  • Character classes ([abc]) work, but ? does not match a single character (unlike gitignore).

  • You can’t negate (!pattern) — unlike gitignore.

Pattern examples

Text
*                       # everything (fallback)
*.js                    # any .js file, any depth
docs/                   # the top-level docs dir
/docs/                  # same — anchored anyway
docs/*                  # files directly in docs/, not subdirs
docs/**                 # everything under docs/, recursive
src/api/*.ts            # .ts files directly in src/api/
src/**/__tests__/       # any __tests__/ dir under src/
Order matters — last match wins
Warning
The *last* matching pattern in the file wins, not the most specific. Put your fallback at the top and override with more-specific rules below. Get this backwards and nobody reviews anything.

Correct ordering: general → specific

Text
# Right
*                       @alice
/src/payments/          @acme-co/payments

# Wrong — the generic * comes last and clobbers the specific rule
/src/payments/          @acme-co/payments
*                       @alice
Required reviewers via branch protection

CODEOWNERS by itself only requests review — it doesn’t enforce. To require it:

  • Repo → Settings → Branches → Branch protection rules → Add rule.

  • Apply to main (and any other protected branches).

  • Tick Require a pull request before merging.

  • Tick Require review from Code Owners.

  • Now any PR touching an owned path needs an approval from at least one owner before the merge button activates.

Example: a realistic monorepo

A working CODEOWNERS for a multi-team monorepo

Text
# ───────── global fallback ─────────
*                                       @acme-co/maintainers

# ───────── apps ─────────
/apps/web/                              @acme-co/frontend
/apps/web/src/billing/                  @acme-co/frontend @acme-co/billing
/apps/admin/                            @acme-co/internal-tools
/apps/mobile/                           @acme-co/mobile

# ───────── shared packages ─────────
/packages/design-system/                @acme-co/design @acme-co/frontend
/packages/auth/                         @acme-co/identity
/packages/db/                           @acme-co/data-platform

# ───────── infra ─────────
/.github/workflows/                     @acme-co/devops
/terraform/                             @acme-co/devops
/k8s/                                   @acme-co/devops @acme-co/sre

# ───────── high-risk paths ─────────
/security/                              @acme-co/security
/SECURITY.md                            @acme-co/security
/.github/CODEOWNERS                     @acme-co/maintainers

# ───────── docs ─────────
*.md                                    @acme-co/docs
/docs/api/                              @acme-co/docs @acme-co/backend
Validating the file

GitHub renders a syntax check at the top of the file view if you’ve made errors. You can also validate with the CLI:

View parsed CODEOWNERS rules and validation errors

Bash
gh api repos/acme-co/widget/codeowners/errors --jq '.errors'

# Or open in the browser:
# https://github.com/acme-co/widget/blob/main/.github/CODEOWNERS
# GitHub shows a banner with any parse errors.
Common pitfalls
  • Owner without write access — the rule is silently ignored. CODEOWNERS rules require write permission.

  • Misspelled team name — also silently ignored. Test with a real PR.

  • Wrong rule order — a generic catch-all at the bottom overrides specific rules.

  • Trailing-slash gotchadocs matches files named docs; docs/ matches the directory. Always include the slash for directory patterns.

  • Pattern doesn’t match what you think — there’s no ! negation; build whitelist patterns instead.

  • Stale ownership — people leave teams; review CODEOWNERS quarterly.

Real-world workflow

Roll out CODEOWNERS on an existing repo

Bash
# 1. Draft the file
mkdir -p .github
$EDITOR .github/CODEOWNERS

# 2. Commit and open a PR — let the affected teams review
git add .github/CODEOWNERS
git commit -m "Add CODEOWNERS for path-based review"
git push -u origin add-codeowners
gh pr create --fill

# 3. After merge, enable branch protection to require code-owner review
#    Settings → Branches → main → Require review from Code Owners
CODEOWNERS enforces ownership without nagging humans
Before CODEOWNERS, asking the right reviewer was a Slack ritual: “hey who owns the billing code?” After CODEOWNERS, the right reviewer is added automatically the moment a PR is opened. No DMs, no guesswork, no PRs waiting on the wrong person. Set it once and reap forever.
Tip
Pair CODEOWNERS with a `CODEOWNERS-glossary.md` that explains *what each team is responsible for*. The file tells GitHub who to ping; the glossary tells the contributor who they’re talking to.