Polyatic CODEOWNERS Resolver
All tools

CODEOWNERS Resolver

Paste a GitHub CODEOWNERS file and a list of paths — every path resolves to its reviewer(s) live, with the exact deciding line, plus warnings for the syntax GitHub silently ignores. Entirely in your browser.

CODEOWNERS
Paths to resolve

One repo-relative file path per line, e.g. packages/api/src/index.ts.

Resolved owners

Paste a CODEOWNERS above and list paths under it — every path resolves here instantly, no Generate button. Copy gives a PR-ready one-liner like line 4 assigns @org/api-team to packages/api/src/index.ts.

Nothing leaves your browser. Your CODEOWNERS and paths are resolved on your own device with a small script you can read — no CODEOWNERS is ever uploaded (the page's only network call is one anonymous page-view count). It resolves GitHub's documented semantics — the last matching line wins.

Honest limits

  • Pattern matching, not membership. It resolves which line matches each path; it does not check whether @org/team exists or has write access — that needs the GitHub API. Owner tokens are validated for form only.
  • Last match wins. Per GitHub's docs, order matters: the last matching pattern takes precedence. That deciding line is what the table reports.
  • Unsupported syntax is flagged, not faked. ! negation, [ ] ranges and an escaped leading # never match on GitHub; each such line is called out instead of silently pretending it works.
  • Only the one pasted file is considered — GitHub reads a single CODEOWNERS per branch, so paste the one your branch uses.

What a CODEOWNERS resolver does

GitHub's CODEOWNERS file decides who is automatically requested for review when a pull request touches a given path. But the file's rules interact in ways that are easy to get wrong: the last matching pattern wins (not the most specific), several gitignore-style syntaxes are silently unsupported, and a pattern with no owner listed means no reviewer rather than the previous one. This tool answers the practical question — if I change this file, who gets requested? — for every path you list, showing the exact line and pattern that decided it. Everything runs live in your browser as you type; there is no Generate button and nothing is uploaded.

Worked example: the prefilled file

The boxes start with a small CODEOWNERS and five paths that exercise every outcome. Reading the rules:

# Order matters: the LAST matching pattern wins.
/docs/          @octocat docs@example.com
*.js            @js-owner
/packages/api/  @org/api-team

# Pattern with NO owners: matched files get no reviewer.
/generated/

# Works in .gitignore but NOT in CODEOWNERS:
!*.md
/apps/[abc]/

packages/api/src/index.ts is matched only by /packages/api/, so it resolves to @org/api-team on line 4. src/app.js matches *.js and resolves to @js-owner. docs/getting-started.md matches /docs/ and resolves to @octocat plus the email owner — the !*.md line does not re-include it, because negation is unsupported, and the tool flags that line as dead. generated/schema.json matches the bare /generated/ pattern, which lists no owners, so it is labelled no owner by match (matched, but no reviewer requested). assets/logo.png matches nothing at all and is labelled the same way — the table distinguishes the two cases. The /apps/[abc]/ line is flagged too: character ranges never match on GitHub.

Why "last match wins" trips people up

Every gitignore-style file resolves precedence by order, and CODEOWNERS is no different: GitHub's documentation states plainly that "the last matching pattern takes the most precedence." The trap is that people read the file top-to-bottom and assume the most specific rule wins, like CSS selectors. It does not. If you have *.js @frontend near the top and /legacy/ @platform lower down, then legacy/main.js resolves to @platform, not @frontend — the later directory rule overrides the earlier extension rule. This tool scans from the bottom of the file up and reports the first pattern that matches, so the deciding line it shows is exactly the one GitHub would use.

The three gitignore rules CODEOWNERS drops

GitHub says CODEOWNERS patterns "follow most of the same rules used in gitignore files, with some exceptions." Those exceptions are the source of most silent breakage, because such lines are simply skipped — they match nothing, and any file you expected them to cover is left unowned:

SyntaxIn .gitignoreIn CODEOWNERS
!*.md (negation)Re-includes matching filesNever matches — line ignored
/apps/[abc]/ (range)Matches a, b or cNever matches — line ignored
\#notacomment (escaped #)Treated as a literal # patternNever matches — line ignored

The tool surfaces each of these with its line number and a one-line reason, so you catch a dead rule while editing rather than after a review request quietly fails to fire.

How docs/* differs from docs/

A wildcard in the final segment changes the depth a pattern reaches, and this catches teams out. A trailing-slash directory pattern like docs/ owns everything inside docs/ at any depth. But docs/* — with the wildcard — matches files directly in docs/ and does not descend: docs/getting-started.md matches, while docs/build-app/troubleshooting.md does not. This is GitHub's own documented behaviour, and the resolver implements it exactly, so a docs/* rule you thought covered a whole tree will honestly show the deeper files as unmatched.

Owners: form is checked, existence is not

An owner token is accepted only if it looks like @username, @org/team-name, or an email@example.com address. A line whose owner is not even syntactically valid is skipped and reported as an invalid-owner warning — matching GitHub's own CODEOWNERS error view, which flags such lines. What the tool cannot do, being a pure offline resolver, is verify that a team or user actually exists or has write access to the repo; that requires the GitHub API and is deliberately out of scope. So a green "resolves to @org/api-team" here means the pattern and owner syntax are correct, not that the team is real — check that separately for a brand-new team slug.

Learn the syntax

Want the rules rather than only the answers? CODEOWNERS Explained covers the three locations GitHub reads the file from and why the base branch's copy is the one that applies, how a leading (or inner) / anchors a pattern to the repo root while * stops at a slash and ** does not, the last-matching-rule-wins precedence in full, the write-access requirement that lets a valid-looking owner request nobody — and two worked examples with every resolved owner and its deciding rule laid out so you can re-derive them by hand.

How it's built and verified

The resolver core is an MIT-licensed, zero-dependency CommonJS module that runs entirely in this tab — the exact code that produces the table below is the library, shipped with the page. It is golden-tested against vectors hand-built from GitHub's own documented CODEOWNERS example file, plus an adversarial suite covering 5,000-rule scale, unicode paths, backslash-escaped spaces in patterns, CRLF vs LF line endings, case sensitivity, and a named case for every GitHub-vs-gitignore deviation — 54 tests, all green. The core does no require(), no filesystem access and no network I/O; on malformed input it returns a structured error object rather than throwing, so a bad paste shows a clear message instead of a blank page.

Frequently asked questions

Is my CODEOWNERS file uploaded anywhere?

No. The whole resolution runs locally in your browser with a small script you can read — there are no network requests to send your file, and the only network call the page makes at all is a single anonymous, self-hosted page-view count. Your CODEOWNERS and your list of paths never leave the tab, so it is safe for the ownership map of a private repo. You can go offline and it keeps resolving.

How does the tool decide who owns a file?

It applies GitHub's documented rule: order matters, and the LAST matching pattern in the file wins. For each path it scans the rules from the bottom up and stops at the first pattern that matches, then reports that line's owners as the deciding assignment. This is the single most common source of surprise — a broad rule near the top (like *.js @js-team) is overridden by any later, more specific rule that also matches, even if the broad one looks 'more specific' to a human reader.

Why does it warn about lines with !, [ ] or a leading \#?

Because GitHub's CODEOWNERS syntax follows most gitignore rules but explicitly drops three of them: you cannot negate a pattern with !, you cannot use [ ] character ranges, and you cannot escape a leading # with a backslash. Lines using any of these are silently ignored by GitHub — they match nothing, so files you think are covered are actually unowned. Instead of pretending those lines work, this tool flags each one with the line number and the reason, so you catch a dead rule before it ships.

What does “no owner by match” mean?

It covers two honest, distinct cases and the table tells you which. Either no pattern in the file matched the path at all, or a pattern matched but lists no owners after it (GitHub treats a pattern with an empty owner list as 'requests no review' — its own docs pair /apps/ @octocat with a bare /apps/github/ line whose owners are left empty). In both cases GitHub requests no reviewer for that path; the tool labels it rather than inventing an owner.

Does it check whether a team or username actually exists?

No — and that is a deliberate limit. Being a pure offline tool, it validates owner FORM only: a token has to look like @username, @org/team-name, or an email address. It cannot know whether @org/api-team is a real team, whether a user still works there, or whether they have write access to the repo — that needs the GitHub API and is out of scope. A line whose owner token is not even syntactically valid is reported as an invalid-owner warning and skipped, matching how GitHub's own CODEOWNERS error view treats it.

Which CODEOWNERS file should I paste, and does docs/* differ from docs/?

Paste the one file your branch actually uses: GitHub reads a single CODEOWNERS per branch, from .github/, the repo root, or docs/ (in that order). On the docs/* vs docs/ question the tool follows GitHub exactly: a trailing-slash pattern like docs/ owns everything inside the directory at any depth, while docs/* with a wildcard matches files directly in docs/ but does NOT descend — docs/build/troubleshooting.md is left unmatched by docs/*. Getting that distinction wrong is a classic way to think a folder is covered when its subfolders are not.

Can I copy a result to paste into a pull request or ticket?

Yes. Every row has a Copy button that yields a plain-English one-liner such as 'line 4 assigns @org/api-team to packages/api/src/index.ts (pattern /packages/api/)', and Copy all grabs the whole set at once. The one-liners are honest about the no-owner cases too, so you can paste an ownership audit straight into a PR description or an issue without hand-writing it.