skip to content

In a GitHub CODEOWNERS file, why can't a ! negation line exempt a subdirectory from its owner?

level: middleimportance: nice to knowfreq 25%

answer

  1. Similar to gitignore, but not identical
  2. A few pattern features are missing
  3. Exemption is expressed a different way
  4. Order, not subtraction
  5. Last match replaces earlier owners

basics

~20 s

GitHub's CODEOWNERS syntax is gitignore-like but does not support negation with !, character ranges in square brackets, or escaping a leading # with a backslash. Carve out exceptions by adding a more specific rule below, since the last matching rule wins.

solid answer

~40 s

CODEOWNERS borrows gitignore's path patterns but not all of them. Three gitignore features are unsupported: `!` to negate an earlier pattern, `[` `]` character ranges, and escaping a leading `#` with a backslash. A line using them is not silently tolerated — it shows up as a problem in the CODEOWNERS errors view, and the intended exemption never happens. The idiomatic replacement leans on precedence: because the **last** matching rule wins and ownership is not additive, you express an exception by writing a narrower rule *below* the broad one and giving it the owners you want. So instead of trying to subtract `docs/api/` from the writers' rule, add a later `/docs/api/ @my-org/api-team` line. There is no supported way to remove ownership by negation; you reassign it.

code

text · 7 lines
text
# unsupported — ! negation is not honored here
/docs/        @my-org/tech-writers
!/docs/api/

# supported — a later, narrower rule replaces the owners
/docs/        @my-org/tech-writers
/docs/api/    @my-org/api-team

go deeper

for a junior

Remember that CODEOWNERS patterns look like gitignore but are a reduced subset — in particular there is no ! negation, so exceptions are written as extra rules.

for a middle

Name the unsupported constructs precisely and explain the replacement idiom: a narrower rule placed below wins, because the last match replaces the earlier owners rather than adding to them.

for a senior

Connect it to maintenance — an unsupported line fails silently from the reviewer's point of view, so the errors view and broad-to-narrow ordering are the habits that keep a large file trustworthy.

for a principal

Consider file design rather than patching: a deliberately narrow top rule leaves paths unowned by default and avoids chasing exemptions that the syntax cannot express.

## Gitignore-like, not gitignore GitHub's `CODEOWNERS` uses gitignore-style path patterns, and the resemblance is close enough that people assume full parity. It is not. The documented unsupported constructs are: - **`!` negation** — you cannot write `!docs/api/` to subtract paths from an earlier rule. - **`[` `]` character ranges** — a class such as `[a-z]` is not honoured. - **Escaping a leading `#` with `\`** — you cannot form a pattern that literally begins with `#`, because `#` always starts a comment. What *is* supported is the everyday subset: `*` as a wildcard, `**` for spanning directories, a leading slash to anchor at the repository root, and a trailing slash to mean a directory and everything under it. `#` at the start of a line is a comment. A line using an unsupported construct does not merely fail to match; it is reported in the CODEOWNERS errors view when you browse the file on GitHub, and a pull request that edits the file is annotated with the same problem. The dangerous outcome is not a loud error, it is the mental model: the author believes a directory is exempt, and reviewers believe it is owned, and neither is checked again. ## Express exceptions with precedence instead CODEOWNERS resolves each changed file by keeping the **last** matching rule in the file, and the winning rule's owners are the only owners — earlier matches are discarded entirely. That replacement semantics is what makes negation unnecessary for the common case. Instead of subtracting, you overwrite: a catch-all line, then `/docs/` owned by the writers, then `/docs/api/` owned by the API team. `docs/guide.md` belongs to the writers; `docs/api/openapi.yaml` belongs to the API team, because its rule matches last. No negation is involved — the narrower rule simply supersedes the broader one for the paths it covers. The ordering discipline follows directly: write the file broad to narrow, catch-all first, exceptions last. Appending a broad rule at the bottom is the mirror-image bug, silently reclaiming ownership from every specific rule above it. ## When you want *no* owner The case negation does not cover is "this directory should have no code owner at all" under a repository-wide catch-all. Since you cannot negate, the honest options are structural: narrow the catch-all so it does not cover that path in the first place, or accept the owner and make the owning team broad enough that it is not a bottleneck. Designing the file with a deliberately restricted top rule — rather than a bare `*` plus attempted exemptions — avoids the problem entirely, at the cost of leaving some paths unowned by default. ## Pattern near-misses worth knowing Several "the rule didn't fire" reports are not about negation at all but about matching semantics carried over from gitignore: - `docs/*` matches files **directly inside** `docs` only — nothing in `docs/api/`. - `/build/` anchors to the repository root, so it does not match `packages/app/build/`. - `build/` without the leading slash matches a `build` directory at any depth. When an exemption "doesn't work", check whether the broad rule even matched the path before reaching for exotic syntax. ## The interview point This question is a small one, but it separates candidates who have actually maintained a CODEOWNERS file from those who have only read about it. The right answer has two halves: name the unsupported constructs precisely, and then show that the platform's answer to exemptions is rule ordering rather than negation, because ownership is replaced by the last match instead of accumulated across matches.

  • What other gitignore features are missing from GitHub's CODEOWNERS syntax?
    Besides `!` negation, character ranges written with square brackets are unsupported, and you cannot escape a leading `#` with a backslash to match a path starting with that character. The everyday subset — `*`, `**`, a leading slash to anchor at the root, a trailing slash for a directory tree, and `#` comments — does work.
  • How do you give one subdirectory a different owner than its parent?
    Add a more specific rule below the parent's rule. Because the last matching line wins and owners are replaced rather than combined, `/docs/ @my-org/tech-writers` followed by `/docs/api/ @my-org/api-team` gives the API team sole ownership of that subtree while the writers keep the rest of docs.
  • Why does a rule using ! not simply get ignored quietly?
    GitHub validates the file and reports unsupported syntax in the CODEOWNERS errors view, and annotates pull requests that edit the file. The line still owns nothing, so the practical effect resembles being ignored — but the error surface is there, which is why checking it after an edit is the cheap way to catch the mistake.

saying these in an interview costs you the question

  • Assuming CODEOWNERS supports gitignore's ! negation
  • Putting an exception rule above the broad rule
  • Expecting character ranges in brackets to match
  • Thinking an unsupported line is silently harmless
  • Believing exceptions subtract owners rather than replace them

context