Enforcing CODEOWNERS review on sensitive paths Jump to heading
A CODEOWNERS file without an enforcing rule requests reviews that anyone can dismiss, which is the correct setting for most of a repository and the wrong one for the handful of paths where a change grants capability rather than changing behaviour. Workflow definitions, infrastructure, migrations and the ownership file itself all fall into the second category, and enforcing review on them specifically keeps the approval meaningful rather than routine. This recipe draws that line, within repository access control and policy.
When to use this approach Jump to heading
- Workflow files can be changed without a security review.
- Infrastructure or migration changes have merged without the owning team seeing them.
- Ownership exists as a file but nothing depends on it.
- An audit asked what prevents a workflow being modified to exfiltrate secrets.
- If enforcement is already on for everything, the useful change is narrowing it rather than adding more.
Step 1 β Identify the paths that grant capability Jump to heading
The test is what a change lets someone do, not how important the code is.
# Paths whose contents execute with repository credentials
ls .github/workflows/ .github/actions/ 2>/dev/null # Paths that change infrastructure or data irreversibly
git ls-files | grep -E '^(infra|terraform|deploy)/|migrations/' | head # And the ownership file itself, which controls the rest
git log --oneline -5 -- .github/CODEOWNERS # Verification: how often do these change, and who reviews them today?
git log --since='1 year ago' --format='%an' -- .github/workflows/ | sort | uniq -c | sort -rn Step 2 β Write patterns that resolve the way you expect Jump to heading
CODEOWNERS takes the last matching rule, which surprises people who expect the most specific one.
cat > .github/CODEOWNERS <<'OWN'
# Default: routing, not enforcement
* @acme/engineering
# Capability-granting paths: the last match wins, so these come after the default
/.github/workflows/ @acme/platform-security
/.github/actions/ @acme/platform-security
/.github/CODEOWNERS @acme/platform-security
/infra/ @acme/platform
/terraform/ @acme/platform
**/migrations/ @acme/data
OWN # Verification: check what each path resolves to, with the last-match rule
for p in src/app.ts .github/workflows/ci.yml infra/main.tf db/migrations/001.sql; do
printf '%-34s ' "$p"
awk -v f="$p" '/^[[:space:]]*(#|$)/{next}
{ pat=$1; sub(/^\//,"",pat); if (index(f,pat)==1 || pat=="*") last=$2 }
END { print last }' .github/CODEOWNERS
done Putting the specific rules after the wildcard is not a style choice β reverse the order and the wildcard wins for every path, the file looks correct, and nothing is enforced.
Step 3 β Require it, on those paths only Jump to heading
{
"name": "sensitive-paths-review",
"target": "branch",
"enforcement": "active",
"conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
"rules": [
{ "type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"require_code_owner_review": true,
"dismiss_stale_reviews_on_push": true
}
}
]
} gh api -X POST repos/:owner/:repo/rulesets --input sensitive.json --jq '.id' # Verification: a workflow change requires the security team
gh pr view --json reviewRequests --jq '.reviewRequests[].slug' dismiss_stale_reviews_on_push matters here more than elsewhere: without it, an approved change to a workflow can have its content replaced after approval, and the approval persists.
Step 4 β Keep the file accurate Jump to heading
An ownership file naming a team that no longer exists silently stops enforcing.
# Do all named owners still exist?
grep -oE '@[A-Za-z0-9_/-]+' .github/CODEOWNERS | sort -u | while read -r o; do
case "$o" in
*/*) gh api "orgs/acme/teams/${o#@acme/}" --silent 2>/dev/null || echo "MISSING TEAM: $o" ;;
*) gh api "users/${o#@}" --silent 2>/dev/null || echo "MISSING USER: $o" ;;
esac
done # Do all named paths still exist?
awk '/^[[:space:]]*(#|$)/{next} {print $1}' .github/CODEOWNERS | while read -r p; do
probe="${p#/}"; probe="${probe%/}"
[ "$probe" = '*' ] && continue
git ls-files "$probe*" | head -1 | grep -q . || echo "DEAD PATH: $p"
done # Verification: both checks are clean, and they run in CI SAFETY WARNING β a rule requiring code owner review on a path whose owner no longer exists produces a pull request that cannot be approved, and the usual response under pressure is to disable the rule rather than fix the file. Run both checks above in the pipeline so a stale owner is a failing check on an ordinary day rather than a blocked merge during an incident.
Step 5 β Watch for the pattern that defeats it Jump to heading
Enforcement on a path can be avoided by not touching that path, which is worth noticing.
# Changes that add a workflow-equivalent capability elsewhere
git log --since='3 months ago' --name-only --format='%h %s' \
| grep -B3 -E '\.(sh|py)$' | grep -iE 'deploy|release|publish' | head # Composite actions and reusable workflows referenced from elsewhere
grep -rn 'uses: \./' .github/workflows/ | head # Verification: every path that can run with credentials is covered
grep -c '^/.github' .github/CODEOWNERS Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should a team own its own ownership entries? Jump to heading
Not for the capability-granting paths. A team that owns the rule governing its own review can relax it, which makes the gate self-administered. Put .github/CODEOWNERS under a security or platform team so changing who reviews what is itself reviewed by someone else.
What if the owning team is unavailable? Jump to heading
That is a staffing question rather than a Git one, and the answer is that the owning team should have enough members that one personβs absence does not block a merge. A rule that cannot be satisfied at three in the morning produces a bypass at three in the morning, and the bypass outlives the incident.
Does this work with forks? Jump to heading
The rule applies to the pull request regardless of where the branch lives, so a fork contribution touching a sensitive path requires the same review. What differs is that the forkβs own copy of the ownership file is irrelevant β the base repositoryβs file governs, which is the correct behaviour and worth confirming if you rely on it.
Related Jump to heading
- Repository Access Control & Policy β the parent topic and where enforcement fits.
- Designing Branch Protection Rulesets for an Org β expressing this across many repositories.
- Path-Based CODEOWNERS in a Monorepo β the routing side of the same file.