The Maintainer Access Grant Standard: Write, Triage-Only, or Not Yet
Grade any contributor on five written criteria and reach Write, Triage-Only, or Not Yet, with the role, branch rules, and review window a second maintainer would also pick.
Key takeaways
- Account age cannot gate maintainer access: the account that backdoored xz was created on 26 January 2021, nine months before its first patch, and would have passed any age check.
- GitHub's Write role has the same rights over Actions secrets as Maintain and Admin, so move publishing tokens to trusted publishing before anyone outside the core team gets Write.
- Any Fail on identity continuity, review history, conduct record, or pressure signals produces Not Yet, and Write requires four Passes plus a real need to merge.
- Release rights are a separate grant: in xz, organization access and the first solo release came months apart, and a release environment with required reviewers turns that gap into a second decision point.
- A GitHub environment named in a PyPI trusted publisher record is auto-created with no protection rules, and adding reviewers changes nothing on PyPI's side, so verify the environment directly.
- Neither Apache nor the OpenSSF publishes a minimum tenure or merged-contribution count for committers, so a project that wants a floor has to write its own and apply it to everyone.
An outside contributor has been helping for months, and now someone wants them to have commit or release rights. This standard is for the maintainer or engineering manager who has to make that call. It turns the contributor's public record into five graded criteria, maps the grades to one of three verdicts (Write, Triage-Only, or Not Yet), and sets the repository role, branch rules, and review window for each, so a second maintainer grading the same person reaches the same verdict.
What this standard decides
This standard settles one thing: which GitHub repository role a named contributor gets now, and with which branch and release controls. Every case ends in one of three verdicts, and release rights are never part of the same decision.
- Write. GitHub's Write role on the named repository only. Branches stay protected, and release publishing stays behind a separately reviewed environment.
- Triage-Only. GitHub's Triage role, which GitHub describes as for contributors who "need to proactively manage issues, discussions, and pull requests without write access".
- Not Yet. No change to their role, a written reason, and a stated condition for grading them again.
Why write a standard instead of making a judgement call each time? Because the OSPS Baseline expects one. The OSPS Baseline is the OpenSSF's Open Source Project Security Baseline, a tiered list of security controls for projects. OSPS-AC-02.01 says a new collaborator must get permissions assigned manually or start with "the lowest available privileges by default". OSPS-GV-04.01 asks for a policy for reviewing collaborators before they get escalated permissions. Its recommendation includes "establishing a justifiable lineage of identity such as confirming the contributor's association with a known trusted organization." OSPS does not ask for a vote. It asks that the policy exists and that you follow it.
Out of scope:
- Handing a whole project over when a maintainer leaves. That is succession, a different runbook.
- Triaging individual pull requests.
- Org owner, Admin, or all-repository roles. No outside contributor should reach those through this process.
What GitHub's Triage, Write, and Maintain roles actually grant
Triage lets someone manage issues and pull requests without pushing code. Write lets them push, and it also gives them the same rights over Actions secrets that Admin has. Most maintainers miss that second point, and it is why release credentials have to leave the secrets store before anyone outside the core team gets Write.
GitHub recommends Write for "contributors who actively push to your project" and Maintain for "project managers who need to manage the repository without access to sensitive or destructive actions". The table below is the part of GitHub's permission table that matters for supply-chain risk.
| Action | Triage | Write | Maintain |
|---|---|---|---|
| Push code | No | Yes | Yes |
| Create, update, delete Actions secrets | No | Yes | Yes |
| Configure Pages publishing source | No | No | Yes |
| Add repository to a team | No | No | No (Admin only) |
Source: the repository roles table in the GitHub Enterprise Server 3.21 docs. Read has none of these rights. Admin has all four.
The table leaves three things open. Check them in your own settings instead of assuming:
- Ruleset bypass is granted separately from the role, through the ruleset's bypass permissions. Write does not include it by default, but someone may have added it.
- Which roles can manage rulesets or cut GitHub releases is not shown in the documentation I reviewed. Treat it as unverified and test it in a scratch repository.
- Organization roles sit above all of this. GitHub's predefined org roles include "All-repository write" and "All-repository admin", and organization owners "have admin access to every repository owned by the organization". Granting either org role is not a repository grant. It covers every repository at once.
What each step up the role ladder unlocks
- Release publishingThe workflow and environment that PyPI trusted publishing names; granted only after probation
- Actions secretsCreate, update, delete; held by Write, Maintain, and Admin alike
- Push and mergeWrite and above; the right to land code on branches
- Issue and PR managementTriage; labels, routing, and closing without write access
- ReadEveryone else; clone, comment, open pull requests
The five grading criteria
Every contributor is graded on five criteria: identity continuity, review history, conduct record, pressure signals, and scope of need. The first four each get Pass, Partial, or Fail. Scope of need is recorded as Issues, Merge, or Release.
For each criterion I state what it proves and what it looks like when it lies. A criterion that cannot lie is not measuring anything.
| Criterion | What a Pass proves | How it lies |
|---|---|---|
| Identity continuity | A real, lasting identity outside this project | Aged accounts, borrowed associations |
| Review history | They understand what they would merge | Clean PRs hiding build or fixture changes |
| Conduct record | They handle disagreement in public | Clean record over a short window |
| Pressure signals | The request is organic | Real users mistaken for a campaign |
| Scope of need | The role fits the work | Asking for merge to do issue work |
Identity continuity
- Pass: Activity outside this project, from before the request, that links back to the same person: other repositories, other platforms, talks, posts. Any claimed employer is confirmed with that employer directly.
- Partial: Real outside activity exists, but a claimed affiliation is unverified or the links only run one way.
- Fail: No meaningful activity outside this project, however old the account is.
Account creation date plays no part in the grade. The JiaT75 account that later backdoored xz was created on 26 January 2021, and its first xz contribution came on 29 October 2021. An age check would have passed it.
How it lies: the Company field on a GitHub profile is self-declared and can say anything. The OpenSSF Scorecard Contributors check reads exactly that field and scores highest with "contributors from at least 3 different companies in the last 30 commits". An NDSS 2025 paper describes "reputation hijacking", where attackers add well-known contributors to their own repositories so an account looks connected to trusted people. Sharing a repository with someone is not the same as being vouched for by them. Years of real outside activity are expensive to fake. A creation date and a list of co-contributors cost almost nothing.
Review history
- Pass: Merged pull requests and reviews given in the area they would get Write over. Every change they made to build files, CI configuration, or test fixtures has been read and understood by an existing maintainer.
- Partial: Merged work exists, but they have given few reviews or have no history in the area they would merge into.
- Fail: Any build, CI, or test-fixture change that nobody can explain, or any artefact they produced that does not rebuild from the tagged source.
Neither Apache nor the OpenSSF has published a minimum tenure or a minimum count of merged contributions. Apache says PMCs "vary significantly in the level of commitment and work they expect". The OpenSSF contributor ladder asks for "a proven track record of being successful approvers and submitting high-quality PRs" but gives no number. If your project wants a floor, set it, write it into the policy, and apply it to everyone. Do not borrow a number from somewhere and call it industry practice.
Conduct record
- Pass: Disagreements happen in public threads and stay within your code of conduct. No unresolved reports.
- Partial: The record is clean but too short to say much.
- Fail: A substantiated conduct report, or a habit of moving disputes into private channels to win them.
How it lies: a clean record over a short window looks the same as a clean record over years. Always read the conduct grade next to the length of the review history.
Pressure signals
- Pass: The request came from the contributor or an existing maintainer, and nobody is campaigning for it.
- Partial: Real users are asking for faster releases or more maintainers, and those users have history outside the thread. Write it down, but treat it as neither support nor a warning sign.
- Fail: Accounts with little or no history outside the thread push for the promotion, or the urgency is framed as the current maintainer failing.
In xz, accounts calling themselves "Jigar Kumar" and "Dennis Ens" complained between April and June 2022. Jia Tan was added to the Tukaani organization on GitHub in October 2022. The pressure came before the grant, from voices that existed nowhere else.
Scope of need
Write down the resource the person actually needs:
- Issues: labelling, routing, closing. Triage covers it.
- Merge: landing reviewed changes. This needs Write.
- Release: publishing artefacts. Never granted in this decision. It goes through the release gate after probation.
How grades map to Write, Triage-Only, or Not Yet
Any Fail means Not Yet. Write needs a Pass on all four graded criteria plus a scope of Merge. Everything in between is Triage-Only.
Apply these rules in order. The first one that matches decides:
- Any Fail on identity, review history, conduct, or pressure: Not Yet.
- Scope is Issues and there is no Fail: Triage-Only.
- Scope is Merge or Release and any criterion is Partial: Triage-Only, with the Partial named as the condition for grading again.
- Scope is Merge or Release and all four criteria Pass: Write. Release stays gated.
Evidence strength against scope of need
| Verdict | Repository role | Branch and release rules | Review window |
|---|---|---|---|
| Write | Write, named repo only | Reviewed PRs, no bypass, gated release | Probation, dated re-grade |
| Triage-Only | Triage | Unchanged | When the Partial clears |
| Not Yet | None | Unchanged | On new evidence for the Fail |
What the Write row means in practice:
- Repository Write only. Not Maintain, and not an all-repository org role. Maintain adds control over things like the Pages publishing source, which a contributor does not need.
- MFA before the role takes effect. OSPS-AC-01.01 says that when a user reads or modifies a sensitive resource, "the system MUST require the user to complete a multi-factor authentication process."
- Protected default branch. Changes land only through pull requests reviewed by another maintainer, and the contributor gets no ruleset bypass.
- Release behind an environment. GitHub environments let you "add required reviewers, restrict which branches can deploy, and see deployment history." With trusted publishing, "you tell PyPI which GitHub repository and workflow are allowed to publish," so no long-lived token has to sit in secrets.
The review window is my own rule, not a published one. No public source describes a time-boxed probationary grant or how to revoke one. I end probation with an event: the first release the new Write holder prepares that a second maintainer approves in the environment gate and that rebuilds from the tag. I also write a calendar re-grade date into the record. My default is 90 days, borrowed from the half-life reputer uses to discount contributor activity. It is a starting point, not a validated probation length.
Why release is a separate grant: in xz, Jia Tan joined the organization in October 2022, and the first release Jia Tan tagged and built came months later. Sources disagree on that date. Timelines derived from Russ Cox's work give 18 March 2023, and another source gives 13 December 2022. On either date, a separate release gate would have created a second decision point, and xz had none.
Hi NAME, Thank you for the work on AREA over the past months. Two maintainers reviewed your access request against the project's written access policy: LINK. Decision: Triage-Only / Not Yet What this means: ROLE, which lets you DO-WHAT. What would change it: CRITERION, specifically EVIDENCE. The maintainers will grade again on DATE, or sooner if that evidence exists. Nothing about this decision is a judgement on the quality of your contributions so far.
Use for Triage-Only or Not Yet. Name the criterion and the evidence, never the reviewer.
Step-by-step: grading a contributor
The procedure has eight steps and two reviewers, put together from Apache, OSPS, and PyPI practice. The timed steps add up to about 90 minutes of reviewer time. Those times are practitioner estimates, not measurements.
Sources disagree on the order. Apache discusses a candidate before checking them. OSPS puts identity vetting before the grant. I put the evidence first, so any discussion or vote happens over a graded record, not over a name and a feeling.
From access request to applied grant
- Open a private intake recordRecord who asked, where, and what access was requested, in a private tracker issue. Lead maintainer, about 10 minutes.
- Grade identity continuityReviewer A checks outside activity, cross-platform links, and any claimed employer, ignoring account age. About 30 minutes; done when a written note carries a grade.
- Grade review historyReviewer A lists merged PRs and reviews given, and flags every change to build, CI, or test fixtures. About 30 minutes; done when every flag is explained or graded Fail.
- Audit conduct and pressure independentlyReviewer B, without seeing A's notes, grades conduct and checks the age and outside history of every account pushing for the grant. About 20 minutes.
- Agree the scope of needBoth reviewers record whether the work needs Issues, Merge, or Release. Release is never granted here.
- Compare grades and decideReconcile with lower-grade-wins, apply the verdict rules in order, and write down the verdict and the re-grade condition. Projects with three or more maintainers can add a one-week vote.
- Apply the grantEnforce MFA, assign Triage or repository Write, and confirm the release environment has required reviewers. Verify by command, then update the sensitive-access member list.
- Run probation and re-gradeHold the first gated release to a second maintainer's approval and a rebuild from the tag, and re-grade on the written date. Revoke if any criterion falls to Fail.
Step 2 takes the most time, because the outside activity sits on different platforms, often under slightly different names. Refolk finds a named person across GitHub, LinkedIn, and the open web from one plain-English search. That turns "does this person exist outside our repo" into a list of links you can read and cite in the note. Then confirm any employer claim with the employer anyway. A profile tells you what someone claims. Only the employer can confirm it.
Step 6 is where governance differs by project. Apache runs a discussion thread and a one-week vote that passes "After at least 3 +1 binding vote and no veto". It then records the result "on the PMC list with a [RESULT][VOTE] subject" and only after that invites the candidate. If your project has the people for this, run the vote on the reconciled grading records. If it does not, the two independent grading records are the decision, which OSPS accepts as long as the written policy is followed.
Contributor: Requested by, and where: Access requested: Triage / Write / Release Reviewer: A / B Date graded: 1. Identity continuity: Pass / Partial / Fail Outside activity (links and dates): Claimed employer, and how it was confirmed: 2. Review history: Pass / Partial / Fail Merged PRs (area): Reviews given: Build, CI, or test-fixture changes, and who explained each: 3. Conduct record: Pass / Partial / Fail Evidence: 4. Pressure signals: Pass / Partial / Fail Accounts pushing for the grant, with their outside history: 5. Scope of need: Issues / Merge / Release Verdict (first matching rule wins): [ ] Any Fail -> Not Yet [ ] Scope Issues, no Fail -> Triage-Only [ ] Scope Merge or Release, any Partial -> Triage-Only [ ] Scope Merge or Release, four Passes -> Write (release stays gated) Re-grade date: Condition for re-grade:
One copy per reviewer. Fill in independently, then reconcile both copies in the private intake issue.
If your project has only one active maintainer, step 4 has nobody to do it. Borrow an outside grader instead of skipping the step.
Finding an independent second reviewer
A project with one active maintainer should bring in an outside grader for the conduct and pressure audit rather than grade alone. One tired person under pressure to share the load is the exact situation the xz campaign targeted.
Apache requires that "PMCs must have at least three (3) active members", and its votes need three binding +1s. A one-person project cannot meet that bar and should not pretend to. A second grader from outside the project, with no stake in the outcome, is the realistic substitute. Their name goes in the intake record.
| Query in Refolk's index | Profiles |
|---|---|
| "software supply chain security", US | 238 |
| "open source maintainer", US | 124 |
| OSPO-type titles, US | 12 |
| OSPO-type titles, Germany | 1 |
Two cautions about this pool:
- Employer concentration. In a sample of 25 US supply-chain-security profiles from Refolk's index, Chainguard accounted for 7 (28%), and Manifest Cyber and Red Hat had 2 each. If the contributor claims one of those employers, pick a grader who works somewhere else. A grader who shares the contributor's employer is not independent.
- Thin OSPO counts. The OSPO title counts (12 in the US, 1 in Germany) are too small to say anything about the market. They are still useful in one way: an Open Source Program Office lead at the contributor's claimed employer is often the fastest way to confirm the affiliation directly. Refolk can find that person by company and title when the employer is large enough to have one.
How this goes wrong
Most failures of this standard are checks that pass for the wrong reason. Each failure mode below has a symptom and a specific check that catches it. Read this section before your first grading, not after.
The xz timeline shows where each criterion would have applied, and where the obvious check would have failed.
| xz event | Date | Criterion it tests |
|---|---|---|
| Account created | 26 Jan 2021 | Identity continuity (age check passes it) |
| First patch | 29 Oct 2021 | Start of review history |
| Pressure accounts active | Apr-Jun 2022 | Pressure signals |
| Added to org | Oct 2022 | Scope of need |
| First solo release | 18 Mar 2023 (disputed) | Release split |
Dates follow secondary sources citing Russ Cox's xz timeline. Another source dates the first release to 13 December 2022.
1. Account age used as a gate. The symptom is a record that says "account is three years old, passes". JiaT75 was nine months old at its first patch. Check: grade only activity outside the project and ignore the creation date.
2. Write treated as code-only access. The symptom is a grant justified as "they just need to merge". Write also lets them create, update, and delete Actions secrets. Check: compare the grant against the role table above, and make sure no long-lived publishing token lives in secrets.
3. Release environment auto-created with no protection. The symptom is a release that published without anyone approving it. A pypi environment that does not exist yet is auto-created "with no protection rules". PyPI checks only the environment name. Adding required reviewers "changes nothing on PyPI's side; removing an environment the record names does." So an environment with no reviewers still matches the trusted publisher record and publishes without a second approval. Check: run gh api repos/{owner}/{repo}/environments and confirm the release environment lists required reviewers.
4. Spoofed organization affiliation. The symptom is "works at a well-known company" based on a profile field. Scorecard reads that same self-declared Company field, so anyone can claim any employer. Reputation hijacking makes it worse, because attackers add well-known contributors to their own repositories. Check: confirm employment with the employer.
5. Real users counted as a pressure campaign. This is the false positive, and it is real. Actual users asking for faster releases look a lot like sock puppets on a busy thread. Check: for each account pushing for the grant, look at its age and whether it has any history outside the thread. Accounts with real outside history make the grade Partial, never Fail.
6. Reputation scores used as the verdict. The symptom is "the score was high, so we granted". Tools such as reputer show "account age, 2FA status, commit verification, follower count, and the overall score". A review of the ARMS proposal found its xz example "is post hoc", and nobody has published false-positive rates for these signals. Check: a score may go into the identity note as one input. It never decides the verdict.
7. Release artefacts that differ from the repository. The symptom is a reviewed repository and a published release that do not match. The xz tarballs contained "a modified build-to-host.m4 that the Git repository didn't". Check: rebuild the release from the tag and compare it with the published artefact. That is also the event that ends probation.
Account age is the cheapest signal to fake, and it is the first one most maintainers check.
Probation, revocation, and keeping the policy current
Treat a grant as provisional until it passes the probation event. Re-grade on the written date and revoke without debate if any criterion falls to Fail. No public source documents a probation or revocation procedure, so the rules below are mine. Adopt them or replace them, but write down whichever you use.
Revocation rules:
- Trigger: any criterion re-grades to Fail, or a release under the new holder fails the rebuild-from-tag check.
- Action: move the role back to what it was before the grant, remove any ruleset bypass, and rotate every Actions secret the person could have edited, because Write could create, update, and delete them.
- Record: note the revocation and the reason in the private intake issue, and update the list of members with access to sensitive resources. A later OSPS checklist at Level 2 requires "a list of project members with access to sensitive resources."
Keeping the policy current:
- Re-check the OSPS level you claim. The text of OSPS-GV-04.01 has not changed, but its level has. The 2025-02-25-rc listed it under Level 2, and the devel version lists it under Maturity Level 3. A project claiming a level under one version may not meet it under another. Check the version pages at baseline.openssf.org. The OpenBao OSPS self-assessment is a public example to compare against, and the OpenSSF Best Practices Baseline badge at bestpractices.dev is where you can record your own.
- Re-check the GitHub role table whenever GitHub updates its repository roles documentation, particularly the unverified points about who can manage rulesets and cut releases.
- Re-read your floor. If you set a minimum tenure or contribution count, look again at whether it still fits the project. It is your number, not an industry one.
Before calling the grant done
- Two reviewers graded independently, and both grading records are attached to the private intake issue.
- No criterion was graded on account creation date.
- Any claimed employer was confirmed with the employer, not read from the profile Company field.
- Every build, CI, and test-fixture change in the contributor's history has a written explanation.
- Every account pushing for the grant was checked for history outside the thread.
- The role assigned is repository Triage or Write, not Maintain, Admin, or an all-repository org role.
- The contributor holds no ruleset bypass permission.
- MFA is enforced on the account before the role took effect.
- No long-lived publishing token is stored as an Actions secret.
- The environments API shows required reviewers on the release environment.
- The list of members with access to sensitive resources is updated.
- A re-grade date and the condition for re-grading are written in the record.
Questions practitioners ask
When should I give a contributor commit access?
Give commit access, meaning GitHub's Write role, only when two maintainers independently grade the contributor Pass on identity continuity, review history, conduct record, and pressure signals, and the work really requires merging. Tenure alone is not a criterion. Neither Apache nor the OpenSSF publishes a minimum tenure or contribution count, so if your project wants one, write it into the policy and apply it to every candidate. Release rights stay behind a separate gate.
What is the difference between GitHub Triage, Write, and Maintain roles?
Triage manages issues, discussions, and pull requests without write access. Write adds pushing code and, often overlooked, the right to create, update, and delete Actions secrets. Maintain adds repository management such as configuring the GitHub Pages publishing source, without the most sensitive or destructive actions. For an outside contributor, Triage or repository Write covers almost every real need. Maintain rarely adds anything they need.
Does the OSPS Baseline require a vote to add a maintainer?
No. The OSPS Baseline requires a written policy for reviewing collaborators before escalating their permissions, and it requires new collaborators to start with manual assignment or the lowest privileges. It does not require a vote. Apache-style votes, with at least three binding +1s, no veto, and one week of discussion, are a governance choice. A project with fewer than three active maintainers cannot meet that bar and should not pretend to.
Can a contributor reputation score replace this review?
No. Tools such as reputer combine account age, 2FA status, commit verification, and follower count into a score, but a review of the ARMS proposal found its xz example was post hoc, and nobody has published false-positive rates for these signals. Account age in particular would have passed the xz attacker. Record a score as one input to the identity note if you like, but never let it set the verdict.
How long should a new maintainer's probation be?
No published source sets a probation length or a revocation procedure, so this is a house rule. I end probation with an event rather than a date: the first release the new Write holder prepares that a second maintainer approves in the environment gate and that rebuilds from the tag. I also set a calendar re-grade, defaulting to 90 days, borrowed from reputer's activity half-life. That number is an anchor, not a validated length.
What if my project only has one maintainer?
Borrow a second grader from outside the project rather than grading alone. A single tired maintainer under pressure is exactly the situation the xz attack exploited. Pick an independent reviewer who does not share an employer with the contributor, give them the grading sheet, and have them grade conduct and pressure without seeing your notes. Write the outside reviewer's name into the intake record.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.