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.
One repo-relative file path per line, e.g. packages/api/src/index.ts.
Lines GitHub silently ignores
| Path | Owner(s) | Deciding line | Copy |
|---|
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/teamexists 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:
| Syntax | In .gitignore | In CODEOWNERS |
|---|---|---|
!*.md (negation) | Re-includes matching files | Never matches — line ignored |
/apps/[abc]/ (range) | Matches a, b or c | Never matches — line ignored |
\#notacomment (escaped #) | Treated as a literal # pattern | Never 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.
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.