Skip to content
Pipelines and Pizza 🍕
Go back

GitHub CODEOWNERS: Who Owns What in Your Repo

10 min read

A few months ago, a teammate opened a pull request that touched our Terraform modules, a handful of Ansible roles, and a CI pipeline config. Three different areas of the codebase, three different people who should have reviewed it. What actually happened? The PR sat for two days because nobody was sure whose responsibility it was. When someone finally picked it up, they approved the pipeline changes they understood but missed a breaking change in the Terraform module — something the infra team would have caught immediately.

That PR made it to main. The breaking change made it to staging. We caught it before production, but just barely. The postmortem wasn’t about bad code — it was about the wrong eyes on the right files.

That’s when I set up a CODEOWNERS file. Twenty minutes of work, and we never had that problem again.


What Is CODEOWNERS?

A CODEOWNERS file tells GitHub who is responsible for reviewing specific parts of your repository. When someone opens a pull request, GitHub reads the file, matches the changed files against ownership patterns, and automatically requests reviews from the right people or teams.

Think of it as a routing table for code reviews. Instead of manually tagging reviewers and hoping you remembered who owns what, the file does it for you.

GitHub looks for the CODEOWNERS file in three locations, in this order:

  1. .github/CODEOWNERS
  2. CODEOWNERS (repository root)
  3. docs/CODEOWNERS

It uses the first one it finds and ignores the rest. Most teams put it in .github/ to keep the repo root clean — that’s what I recommend.


Syntax Basics

The format is dead simple. Each line is a file pattern followed by one or more owners:

# This is a comment
PATTERN    @owner1 @owner2

Owners can be:

FormatExampleWhat it matches
GitHub username@userIndividual user
GitHub team@my-org/infra-teamOrganization team
Email address[email protected]User by email

Here’s a real-world example from an infrastructure repo:

# Default: infra team reviews everything unless overridden below
*                       @my-org/infra-team

# Terraform modules — platform team owns these
/terraform/             @my-org/platform-team

# Ansible roles — automation team
/ansible/               @my-org/automation-team

# CI/CD pipelines — DevOps leads
/.github/workflows/     @user @my-org/devops-leads

# Documentation — anyone on the team can approve
/docs/                  @my-org/engineering

That’s it. Five lines, and every PR gets routed to the right reviewer automatically.


Pattern Matching: The Rules

CODEOWNERS uses the same pattern syntax as .gitignore, with a few quirks worth knowing.

Common Patterns

# Match everything in the repo
*

# Match a specific file
/Makefile                   @my-org/devops-leads

# Match all files in a directory (and subdirectories)
/terraform/                 @my-org/platform-team

# Match files by extension anywhere in the repo
*.tf                        @my-org/platform-team
*.yml                       @my-org/automation-team

# Match a specific subdirectory
/src/api/                   @my-org/backend-team

# Match with single-level wildcard
/terraform/modules/*/       @my-org/module-maintainers

# Match with recursive wildcard
/terraform/**/outputs.tf    @my-org/platform-team

The Golden Rule: Last Match Wins

This is the single most important thing to understand about CODEOWNERS. If multiple patterns match the same file, the last matching pattern in the file takes precedence.

# This broad rule matches everything
*                       @my-org/infra-team

# This specific rule overrides the above for Terraform files
/terraform/             @my-org/platform-team

# This even more specific rule overrides both for the networking module
/terraform/modules/networking/   @jsmith

If someone edits /terraform/modules/networking/main.tf, only @jsmith gets the review request — not @my-org/platform-team, not @my-org/infra-team. The last matching line wins, period.

This means order matters. Put your broadest rules at the top and your most specific overrides at the bottom. Think of it like CSS specificity, but simpler — it’s just line order.


Resetting Ownership (The Empty Owner Trick)

Here’s a pattern that trips people up. You can specify a path with no owner to explicitly remove ownership:

# Default: infra team reviews everything
*                       @my-org/infra-team

# But documentation changes don't need infra review
/docs/

That empty owner line means changes to /docs/ won’t trigger any automatic review request. Anyone with write access can approve. This is useful for low-risk areas where you don’t want to bottleneck the team.


Integrating with Branch Protection

A CODEOWNERS file by itself just requests reviews. If you want to require code owner approval before merging, you need branch protection rules.

Setting It Up

  1. Go to your repo’s Settings > Branches (or Settings > Rules > Rulesets for newer repos)
  2. Edit or create a branch protection rule for main
  3. Enable Require a pull request before merging
  4. Check Require review from Code Owners

Now a PR that touches files owned by @my-org/platform-team literally cannot merge until someone on that team approves it. This is where CODEOWNERS goes from “nice to have” to “critical infrastructure.”

Branch-Specific CODEOWNERS

Each branch can have its own CODEOWNERS file. GitHub reads the file from the base branch of the pull request (usually main). This means:

  • The CODEOWNERS file on main governs who reviews PRs targeting main
  • You can have stricter ownership on production branches and looser rules on develop
  • Changes to the CODEOWNERS file itself in a PR won’t take effect until they’re merged to the base branch

That last point catches people. If you add yourself as an owner in your own PR, it doesn’t count until that change lands on the base branch.


Patterns for DevOps and Infrastructure Repos

Here’s a more complete CODEOWNERS file I’ve used in production infrastructure repositories:

# ============================================
# CODEOWNERS — infrastructure monorepo
# ============================================
# Last-match-wins: broad rules first, specific overrides last.

# Default owner for everything
*                                   @my-org/infra-team

# --- Terraform ---
/terraform/                         @my-org/platform-team
/terraform/modules/                 @my-org/platform-team @my-org/module-reviewers
/terraform/environments/prod/       @my-org/platform-leads

# --- Ansible ---
/ansible/                           @my-org/automation-team
/ansible/roles/security/            @my-org/security-team

# --- CI/CD Pipelines ---
/.github/workflows/                 @my-org/devops-leads
/.github/actions/                   @my-org/devops-leads

# --- Kubernetes manifests ---
/k8s/                               @my-org/k8s-team
/k8s/production/                    @my-org/k8s-leads @my-org/sre-team

# --- Monitoring and alerting ---
/monitoring/                        @my-org/sre-team

# --- Documentation (no required reviewer) ---
/docs/

# --- Sensitive files: require senior review ---
/terraform/environments/prod/*.tfvars   @my-org/platform-leads @cto
/.github/CODEOWNERS                     @my-org/platform-leads

A few things to notice:

  • Production paths get stricter reviewers. The /terraform/environments/prod/ path requires platform leads, not just anyone on the platform team.
  • The CODEOWNERS file itself is owned. This prevents someone from quietly removing themselves from ownership.
  • Security-sensitive roles have dedicated reviewers. The security team owns ansible/roles/security/ regardless of who generally owns Ansible.
  • Documentation has no owner. Low friction for docs means docs actually get written.

Hands-On Lab: Set Up CODEOWNERS

Let’s walk through setting up CODEOWNERS from scratch.

Step 1: Create a test repo

mkdir codeowners-lab && cd codeowners-lab
git init
echo "# CODEOWNERS Lab" > README.md
git add README.md && git commit -m "chore: init"

Step 2: Create the CODEOWNERS file

mkdir -p .github
cat > .github/CODEOWNERS <<'EOF'
# Default reviewers
*                       @your-username

# Terraform files
/terraform/             @your-username

# Documentation — open to anyone
/docs/
EOF

git add .github/CODEOWNERS
git commit -m "chore: add CODEOWNERS file"

Step 3: Add some files to test patterns

mkdir -p terraform/modules docs

echo 'resource "aws_s3_bucket" "example" {}' > terraform/modules/main.tf
echo "# Architecture Decisions" > docs/architecture.md
echo "name: CI" > .github/workflows/ci.yml

git add -A && git commit -m "feat: add sample project files"

Step 4: Push and verify on GitHub

# Create a repo on GitHub (requires gh CLI)
gh repo create codeowners-lab --private --source=. --push

# Open the CODEOWNERS file in your browser — GitHub will highlight syntax errors
gh browse -- .github/CODEOWNERS

Step 5: Test with a pull request

git checkout -b test/codeowners-verification

echo 'resource "aws_instance" "test" {}' >> terraform/modules/main.tf
git add terraform/modules/main.tf
git commit -m "feat: add test instance"
git push -u origin test/codeowners-verification

# Create a PR and watch the automatic reviewer assignment
gh pr create --title "Test CODEOWNERS routing" \
  --body "Verifying that CODEOWNERS assigns the correct reviewer."

Check the PR on GitHub. You should see your username automatically requested as a reviewer based on the /terraform/ pattern.

Step 6: Enable branch protection (optional)

# Require code owner review on main
gh api repos/{owner}/{repo}/branches/main/protection \
  --method PUT \
  --input - <<'JSON'
{
  "required_pull_request_reviews": {
    "require_code_owner_reviews": true,
    "required_approving_review_count": 1
  },
  "enforce_admins": false,
  "required_status_checks": null,
  "restrictions": null
}
JSON

Now PRs touching owned files must be approved by the designated code owner before merging.


Best Practices

After running CODEOWNERS across multiple repos for a couple of years, here’s what I’ve learned:

Use teams, not individuals. People leave, change roles, go on vacation. @my-org/platform-team keeps working when someone is out. @jsmith creates a bottleneck when John is on PTO.

Keep at least two people per ownership group. A team of one is just an individual with extra steps. Make sure every owned path has at least two people who can approve.

Own the CODEOWNERS file itself. Add an explicit rule for /.github/CODEOWNERS so changes to ownership require approval from a lead or admin. Without this, anyone can quietly reassign ownership.

Review your CODEOWNERS quarterly. Teams reorganize. Repos evolve. That path someone owned six months ago might not even exist anymore. Stale rules create confusion.

Don’t over-specify. If your CODEOWNERS file is longer than your actual codebase, you’ve gone too far. Start broad and add specific overrides only where you’ve had real problems with wrong reviewers.

Keep the file under 3 MB. GitHub silently ignores CODEOWNERS files that exceed this limit. Not a concern for most repos, but monorepos with auto-generated ownership rules can hit it.


Troubleshooting Guide

ProblemCauseFix
Reviewers not being assignedCODEOWNERS not on the base branchMerge your CODEOWNERS changes to main first
Syntax errors highlighted in GitHubInvalid pattern or usernameCheck for typos; verify users/teams have write access
Wrong reviewer assignedLast-match-wins ordering issueMove specific rules below general rules
Team not getting requestsTeam lacks write access to the repoGrant the team write (or maintain) permissions
File ignored entirelyCODEOWNERS exceeds 3 MBConsolidate patterns with wildcards
Code owner review not requiredBranch protection not configuredEnable “Require review from Code Owners” in branch settings
Invalid user/team on a lineUser left org or team was renamedUpdate the file; use teams to avoid single points of failure
CODEOWNERS in wrong locationMultiple files in different locationsGitHub uses the first found: .github/ > root > docs/ — pick one
Changes to CODEOWNERS in PR not taking effectGitHub reads from the base branchThe new rules apply after the PR merges to the base branch

Quick Reference

# Validate your CODEOWNERS syntax (GitHub highlights errors in the UI)
# Navigate to: github.com/<owner>/<repo>/blob/main/.github/CODEOWNERS

# Check for CODEOWNERS errors via API
gh api repos/{owner}/{repo}/codeowners/errors

# Common patterns
*                           # Everything
*.tf                        # All Terraform files
/src/                       # Everything under /src/
/src/api/**/*.go            # All Go files under /src/api/ recursively
/terraform/modules/*/       # Direct subdirectories of modules

Happy automating!