GitHub’s nested teams let you build a hierarchical structure for permissions, which is fantastic for managing access at scale. But the real magic happens when you combine that with CODEOWNERS, allowing you to automate granular file-level ownership and review assignments.

Let’s see this in action. Imagine a repository with a few directories: backend/, frontend/, and docs/.

.
├── backend/
│   ├── api/
│   ├── database/
│   └── services/
├── frontend/
│   ├── components/
│   └── pages/
└── docs/
    └── user-guide.md

We want to set up permissions and ownership like this:

  • @my-org/engineering: Full access to backend/ and frontend/.
  • @my-org/frontend-team: Read-only access to frontend/, but can also be assigned as reviewers for specific files within frontend/ via CODEOWNERS.
  • @my-org/backend-team: Read-only access to backend/, but can also be assigned as reviewers for specific files within backend/ via CODEOWNERS.
  • @my-org/docs-writers: Read-only access to docs/ and can be assigned as reviewers for docs/ files.
  • @my-org/security-team: Can be assigned as reviewers for any file in the repository.

First, we create the teams in GitHub:

  1. @my-org/engineering: This is our top-level team.
  2. @my-org/frontend-team: This team will be a child of engineering.
  3. @my-org/backend-team: This team will also be a child of engineering.
  4. @my-org/docs-writers: This team is independent.
  5. @my-org/security-team: This team is independent.

Now, we nest frontend-team and backend-team under engineering. In the GitHub UI, when creating or editing a team, there’s an option to "Add parent team." We select engineering for both frontend-team and backend-team. This means anyone in frontend-team or backend-team automatically inherits the permissions of engineering.

Next, we assign repository permissions:

  • Go to your repository’s Settings > Teams.
  • Add @my-org/engineering and grant it Admin access (or Write if you prefer).
  • Add @my-org/frontend-team and grant it Read access to the repository.
  • Add @my-org/backend-team and grant it Read access to the repository.
  • Add @my-org/docs-writers and grant it Read access to the repository.
  • Add @my-org/security-team and grant it Read access to the repository.

Because frontend-team and backend-team are children of engineering, their members will have both the Admin (inherited) and Read (explicitly granted) permissions. GitHub consolidates these, so they effectively have Admin access. This is a common point of confusion; inherited permissions are additive and take precedence for access levels. If you only wanted them to have Read access, you would not nest them under engineering or assign engineering Admin access. For this example, we’ll assume the intent is for the nested teams to have broader access.

Now for the CODEOWNERS file. Create a file named CODEOWNERS in the root of your repository. This file uses a simple glob pattern syntax to map files or directories to GitHub teams or users.

# This is a comment
# Any changes to files in the docs/ directory require a review from docs-writers
docs/* @my-org/docs-writers

# Specific file in docs
docs/user-guide.md @my-org/docs-writers @my-org/security-team

# Any changes to files in the frontend/ directory require a review from frontend-team
frontend/* @my-org/frontend-team

# Specific files in backend require review from backend-team
backend/api/* @my-org/backend-team
backend/database/* @my-org/backend-team

# But any changes to the core services need a review from engineering directly
backend/services/* @my-org/engineering

# Anyone in the security team can review any file in the repo
* @my-org/security-team

Let’s break down how this works:

  • When a pull request is opened, GitHub scans the changed files.
  • For each changed file, it looks for a matching pattern in the CODEOWNERS file, starting from the top.
  • The last matching pattern determines the owners.
  • If a file matches docs/*, it’s owned by @my-org/docs-writers.
  • If docs/user-guide.md is changed, the pattern docs/user-guide.md is more specific and takes precedence over docs/*. So, it’s owned by @my-org/docs-writers and @my-org/security-team.
  • If a file in frontend/ is changed, say frontend/pages/HomePage.js, it matches frontend/* and is owned by @my-org/frontend-team.
  • If a file in backend/services/ is changed, it matches backend/services/* and is owned by @my-org/engineering.
  • The catch-all * pattern at the end means any file not otherwise matched will require a review from @my-org/security-team.

The key advantage here is that you don’t need to manually assign reviewers for every PR. GitHub automatically requests reviews from the CODEOWNERS based on the files touched in the PR. Team members in frontend-team can now be assigned as reviewers for frontend/ files, even though their repository-level permission is only 'Read'. This is the power of CODEOWNERS: it’s about review assignment, not broad repository access.

The most counterintuitive aspect of CODEOWNERS is that the order of rules matters, but only in the sense that the last matching rule wins. You might expect the most specific rule to always win, but GitHub processes rules from top to bottom and the final match dictates ownership, making it crucial to put more specific rules earlier in the file if you want them to apply. For example, if you had frontend/* @my-org/frontend-team and then frontend/components/* @my-org/components-team, a change in frontend/components/Button.js would be owned by @my-org/components-team because that rule appears later and matches.

By combining nested teams for broad access control and CODEOWNERS for granular, automated review assignments, you create a robust and scalable permission management system for your GitHub repositories.

Next, you’ll likely want to explore how to enforce branch protection rules that require reviews from CODEOWNERS.

Want structured learning?

Take the full Github course →