Skip to content
Français
CI/CD & Automatisation medium

CODEOWNERS: enforcing a review on your workflows

25 min de lecture

Read this page in French

A CODEOWNERS file declares who has to review what. Applied to .github/workflows/, it is the governance control returning the most for the least effort: it closes the most direct attack path, that of the workflow added or modified without a competent review.

What you will learn

  • Write a correct CODEOWNERS and understand its precedence rule
  • Protect the workflows directory specifically
  • Enable the review requirement, without which the file is merely advisory
  • Anticipate the self-approval limit on a single-maintainer repository

Why this directory before any other

A change to .github/workflows/ does not read like a change to application code. It can grant permissions, reach secrets, publish an artifact, or run on your infrastructure. A reviewer validating an application pull request without looking at the workflow that comes with it lets through the change with the heaviest consequences.

The GhostAction incident of September 2025 illustrates exactly that path: no workflow flaw exploited, only a malicious workflow committed into repositories the attackers had taken control of. A mandatory review on that directory, by people who know how to read a permissions:, turns that silent operation into a visible request.

Writing the file

CODEOWNERS goes at the root, in .github/, or in docs/. The most readable convention is .github/CODEOWNERS, next to what it protects.

.github/CODEOWNERS
# Default owners of the whole repository
* @my-org/maintainers
# The CI and its configuration: reviewed by the security team
/.github/workflows/ @my-org/security @my-org/maintainers
/.github/actions/ @my-org/security
/.github/dependabot.yml @my-org/security
/.github/CODEOWNERS @my-org/security
# The build chain: the same requirement
/Dockerfile @my-org/security
/requirements*.txt @my-org/security

Three syntax points are enough to avoid the common mistakes:

  • The last matching rule wins. Unlike .gitignore, order is decisive: put the general rules at the top, the specific ones at the bottom. A * line placed last would cancel everything else.
  • A path starting with / is anchored at the root. Without the leading slash, the pattern matches at any level of the tree.
  • Owners are accounts (@user), teams (@org/team) or email addresses. A team must have write access to the repository to be a valid owner, otherwise the line is silently ignored.

The file alone blocks nothing

This is the most frequent misunderstanding. CODEOWNERS designates reviewers and automatically triggers a review request. It enforces nothing.

For it to bite, you have to enable the matching requirement in the ruleset or the branch protection:

Fenêtre de terminal
REPO=my-org/my-project
# Check that code owner review is indeed required
gh api "repos/$REPO/rulesets/RULESET_ID" \
--jq '.rules[] | select(.type=="pull_request") | .parameters.require_code_owner_review'

The expected answer is true. On false, your CODEOWNERS is a suggestion: the reviewers are notified, the merge goes through without them.

Also check that the file is syntactically valid. GitHub shows the errors in the interface, under the Code owners tab of the repository settings, and an invalid line is ignored without warning in the pull requests. A misspelled team, or one without write access, produces exactly the effect of an absent protection.

The single-maintainer limit

One GitHub fact shapes everything that follows: you cannot approve your own pull request. No configuration lifts that rule.

On a repository where a single person contributes, requiring code owner review therefore makes merging impossible by construction: the author is the only owner, and cannot self-approve. Three ways out exist, to choose knowingly.

Way outWhat it costs
Designate a real second reviewerA dependency on their availability
List the administrator role in the ruleset bypass listThe control becomes declarative for you
Restrict the requirement to sensitive pathsThe workflows stay protected, the rest flows

The third is the most often relevant, and it is little known: nothing forces you to cover the whole repository. A CODEOWNERS limited to .github/workflows/, the Dockerfile and the dependency files protects what matters, while letting the application code follow a normal route.

.github/CODEOWNERS, the targeted version
# Nothing on '*': the application code needs no code owner review
/.github/workflows/ @my-org/security
/.github/CODEOWNERS @my-org/security
/Dockerfile @my-org/security

Checking that the mechanism works

A governance control you have never seen fire is not a control, it is an intention. The test takes four gestures:

Fenêtre de terminal
# On a test branch, modify a workflow
git switch -c test-codeowners
printf '\n# test\n' >> .github/workflows/ci.yml
git commit -am "test: verify the code owner review"
git push -u origin test-codeowners
gh pr create --fill
gh pr view --json reviewRequests --jq '.reviewRequests'

The output must name the declared owners, and the pull request must show the review as required, not merely requested. Delete the branch afterwards.

Key points

  • .github/workflows/ is the directory where a change has the heaviest consequences: permissions, secrets, publishing, infrastructure.
  • GhostAction exploited no workflow flaw, only the absence of review on the addition of a workflow file.
  • In CODEOWNERS, the last matching rule wins: general patterns go at the top, specific ones at the bottom.
  • An owning team must have write access, otherwise the line is silently ignored.
  • Cover the CODEOWNERS file itself, otherwise anyone can declare themselves owner of what they were supposed to have reviewed.
  • The file alone blocks nothing: the require_code_owner_review requirement must be active in the ruleset.
  • You cannot approve your own pull request: on a single-maintainer repository, plan a second reviewer, a bypass list, or a coverage limited to sensitive paths.

Next steps

  • Runner groups and the network boundary: the control bounding what a compromised run reaches, when the review let something through.
  • GitHub CLI (gh): the tool that checks in one command whether the review is genuinely required, not merely requested.
  • actionlint: the automatic check that lightens the human review by catching syntax errors up front.

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn