The Departing-Maintainer Runbook: Personal Publish Rights to Org Control
Turn a departing engineer's handles into a one-row-per-artifact inventory, then move every repo, package, key and CI publisher to org control before their last day.
Key takeaways
- Standard IT offboarding removes a person from the GitHub org, but registry accounts, user-scoped npm names, trusted-publisher bindings and signing keys sit outside SSO and need their own handover.
- Transferring a GitHub repo automatically adds the original owner as a collaborator, so a transfer with no cleanup step gives the departing engineer an extra write path.
- Removing a user from a PyPI project does not remove the Trusted Publishers they registered, so the old CI identity keeps working until you delete it from the project's publishing settings.
- npm maintainer and trusted-publishing changes now need an interactive 2FA session, so the departing engineer has to be present for them before their accounts are locked.
- PyPI's PEP 541 recovery backlog once ran a year and four months behind, and even at its best first response averages about a week, so preventing a handover gap costs far less than recovering from one.
- In Refolk's index there are about 10.3 self-described US open source maintainers for every OSPO-titled person, which helps explain why publish-rights handovers usually land on individual engineering managers.
An engineer who publishes your open-source packages from their personal accounts has given notice. Every repo, registry package, signing key and CI publishing identity tied to them now has to move under company control before their last day. This runbook is for the engineering manager, technical founder, DevRel lead or sourcer who owns that handover. It covers how to build the inventory, the order and timing of transfers on each registry, and the checks that prove each item actually moved.
Why do IT offboarding checklists miss publish rights?
Standard checklists work on the systems behind SSO, and most publish rights sit outside it. Removing someone from the GitHub org and reassigning their repos only covers the outer layer. Registry accounts, scoped package names, trusted-publisher bindings and signing keys live in personal accounts that no identity provider controls.
A trusted publisher is a registry setting that lets a specific CI workflow publish without a stored token. The registry checks the workflow's OIDC identity: who owns the repo, which repo it is, and which workflow file is running. Because that binding is attached to the package and not to a person, removing a human owner leaves it in place.
Where a maintainer's publish rights actually live
- GitHub org membershipBehind SSO; standard offboarding already removes it
- Repos owned by the personal accountOnly move if the owner explicitly transfers them
- Registry accounts and scoped namesnpm, PyPI, crates.io, Maven logins with their own 2FA
- Trusted-publisher bindingsCI identities bound to the project, not the person
- Signing keys and secret valuesPGP keys and tokens on a laptop or in a personal vault
Missing these layers is expensive. When nobody at the company has rights to a package, the only way back is the registry's dispute process. On PyPI that process is PEP 541, the policy for reassigning abandoned or disputed project names.
Recovery after the fact can also depend on things that have already left the building. Maven Central's support pages say that losing PGP private keys may limit your ability to recover publishing access. Treat recovery as a process measured in weeks, with no guaranteed outcome.
How do you build the inventory from a person's handles?
Start from every handle the engineer uses and work outward one ecosystem at a time until each artifact has its own row with an owner column per registry. Budget about a day for the engineering manager and a sourcer, and finish the inventory before anything moves.
You can only search for handles you know about, and engineers often publish under a handle that differs from their work identity. If you only have a name and work email, Refolk can return the person's public GitHub and open-web profiles from a plain-English request, which gives you the handle list to start from. Then ask the engineer to confirm the list. They will remember the side project you never heard of.
What to sweep, and what each source proves
- Personal GitHub repos. These show where code lives, but not where it publishes. A repo that publishes a package from a personal account counts as two rows.
- GHCR images. GitHub Container Registry packages do not move with the repo. An inventory built only from the repo list will miss them.
- npm packages and user-scoped names.
npm owner ls <pkg>shows the current owners. Search every package.json for@<personal-handle>/, because those names can never move. - PyPI projects and crates.io crates. Use the owner list on each project.
cargo owner --list <crate>works for crates. - Maven Central namespaces. The Central Portal's namespaces page lists every namespace an account owns. Pay attention to
io.github.<user>namespaces, which are tied to a personal GitHub handle. - Trusted publishers. Open each project's publishing settings and record the owner, repo and workflow tuple.
- Signing keys and secrets. PGP keys used for Maven signing, plus every repo and environment secret the release workflows read.
Each of these can mislead you. An owner list only tells you who has rights right now. It says nothing about which CI identity can publish. A GitHub repo list leaves out GHCR images. A clean package.json search tells you nothing about packages published from repos you have not cloned.
COLUMNS artifact | ecosystem | current owners | org owner added (date) | publisher tuple (owner/repo/workflow) | secrets or keys involved | action: transfer / re-bind / republish / exception | verified release (version) COMMANDS npm owner ls <package> cargo owner --list <crate> grep -rn '"name": "@<personal-handle>/' --include=package.json . gh repo list <personal-handle> --limit 500 UNVERIFIED (confirm locally before relying on it) npm access list packages <personal-handle>
One row per artifact; replace the handle and package names, and run listing commands from a session that can see each package.
I have not verified a per-user listing command for npm, PyPI, RubyGems, Docker Hub or Go modules. npm access list packages is the likely one for npm, but I have not checked it. Close the gap by having the engineer share their screen while logged into each registry, then reconcile their account pages against your sheet row by row.
How do owner changes work on each registry?
Each registry has its own rules for adding owners, using team or org identities, and moving names. npm and crates.io let you do it yourself. Maven Central goes through a support desk. PyPI's mechanics were not verified for this guide. The table summarises the mechanics I could source.
| Registry | Add owner | Org or team owner | Self-service? |
|---|---|---|---|
| npm | npm owner add + invite accept | Org scope; scoped names can't move | Yes, interactive 2FA |
| crates.io | cargo owner --add | github:org:team, restricted rights | Yes |
| PyPI | Not verified | Organizations (feature revived) | Not verified |
| Maven Central | Via Central Support | Namespace-level | No |
npm
npm has only one level of access: either you can modify a package or you can't. The order matters. Add the new owner, wait until they accept the invitation, and only then have the old owner run npm owner rm. A scoped package cannot move to another user or organization, because the scope is the account name. You have to publish a new package in the new scope and edit the name field in package.json. Setting an org scope in your client only affects new packages.
Token policy has tightened. Classic tokens have been permanently revoked, granular write tokens have a maximum lifetime of 90 days, and npm login issues a two-hour session token. Granular tokens that bypass 2FA can no longer change package access, maintainers or trusted publishing configuration, and their direct publishing ability is also being phased out. GitHub's changelog and one secondary source give different effective dates for the governance block. Check the changelog for the current state.
crates.io
cargo owner --add username foo sends an invitation. Teams use the syntax github:org:team, and you must be a member of a team to invite it. A named owner must have logged into crates.io at least once. Team owners can publish or yank but cannot add or remove owners. That is why a team should never be the only owner. The Rust Forge trusted publishing docs describe crates.io's publisher setup.
PyPI
PyPI trusted publishers are many-to-many: one project can trust several publishers, and one publisher can serve several projects. PyPI always checks the repository_owner_id claim to block account-resurrection attacks, so a publisher is pinned to the numeric owner and not only to the name. Reusable workflows cannot currently be used as a Trusted Publisher workflow. Whether PyPI requires 2FA for all accounts was not verified here.
Maven Central
Adding or removing users who can publish goes through Central Support. An io.github.<user> namespace is tied to a personal handle, so moving it will probably also need Support. That is my inference, not documented policy. Self-service namespace migration was limited to namespaces with no more than 6 authorized publishing accounts and no parent or child namespaces. Check whether your namespace qualifies before you assume you can do it yourself.
Who should receive the publish rights?
Give each package at least two named company humans as owners, plus an org or team identity where the registry supports one. Never hand everything to a single person. One person holding all the rights is exactly the situation you are unwinding now.
Few companies have someone whose job is to own publish rights. In Refolk's index of professional profiles, OSPO titles are rare, and they are rarer still outside the US. OSPO here means an open source program office: Open Source Program Manager, OSPO Lead, Head of Open Source and similar titles.
| Market | OSPO-titled profiles | Top employer concentration |
|---|---|---|
| US | 12 | No employer above 1 |
| DE + UK + FR + NL | 5 | Decathlon, 2 of 5 (40%) |
| US to EU-4 ratio | 2.4x | - |
The US profiles are spread across employers such as NVIDIA, Goldman Sachs, State Farm and SAS. Across Germany, the UK, France and the Netherlands combined, two of the five work at Decathlon. The match looks like an exact-title match, so treat these counts as minimums, not as the size of the market.
| US population | Profiles in Refolk's index |
|---|---|
| OSPO-titled | 12 |
| "open source maintainer" in headline | 124 |
| Maintainers per OSPO title | ~10.3 (indicative) |
The 124 self-described maintainers include people at Microsoft, AWS, Docker, Okta and CNCF. The two counts used different match methods (exact title versus headline keyword), so the ratio is only a rough indicator.
In practice, choose two receiving owners from inside the team: usually the engineering manager and the release engineer who owns CI. If the departure leaves nobody who understands the release pipeline, hire for that before the notice period ends. Refolk handles that search in plain English, the same way it handled the handle search above.
In what order should the handover run, and how long does it take?
Do the work that needs the engineer first: secret values, then second owners on every registry. Repo transfers and publisher re-binding come next. Removals and a verifying release come last. Apart from the one-day inventory, most stages take minutes to hours, so the whole runbook fits in a normal notice period if you start in week one.
Two variables decide how hard each artifact will be: whether the engineer can still authenticate, and whether the registry lets you change owners yourself. Sort each row in the inventory into one of four quadrants.
Where each artifact lands in the handover
Stage timings
- Inventory: about 1 day, EM plus sourcer.
- Collect secret values: hours, done by the departing engineer.
- Add second owners: as long as the invitations take to be accepted, engineer plus EM.
- Transfer repos: minutes per repo, done by the engineer. Google's open source transfer guide warns that the transfer email token is only good for 24 hours.
- Re-bind publishers: under 1 hour per package, release engineer.
- Remove, verify, document: one release cycle, release engineer and EM.
Rename or transfer first?
Sources disagree. One maintainer advises renaming before setting up the publisher, because a rename breaks uploads until the publisher is edited. Others transfer first and fix publishers afterwards. Since PyPI publishers are many-to-many, you can in principle register the org tuple before the transfer so publishing never has a gap. That follows from PyPI's internals documentation, but it is not a documented procedure, so try it on one low-traffic package before relying on it.
The handover procedure, step by step
Run these eight steps in order. Each one has an owner and a clear definition of done. Do not start step 4 until steps 2 and 3 are finished, because those are the steps that need the engineer's credentials.
Personal publish rights to org control
- Inventory every artifact from the handlesSweep personal GitHub repos, GHCR images, packages on each registry, Maven namespaces and signing keys into one sheet. Done when there is one row per artifact and an owner column per registry; about one day.
- Collect secret values before anything movesThe engineer copies every repo and environment secret, token and signing key into the company vault. GitHub never shows a secret value again.
- Add org or second owners on every registryAdd the org or two named company humans on npm, crates.io, PyPI and Maven Central while the engineer can still authenticate. Done when every invitation is accepted and listing commands show the new owners.
- Transfer the GitHub reposThe engineer transfers each repo to the company org and accepts the transfer email within 24 hours. Handle GHCR images separately, because they do not move with the repo.
- Re-bind trusted publishers to the new tupleRegister the new org owner, repo and workflow as publisher on PyPI, npm and crates.io. Under an hour per package; test one first.
- Remove old publishers, personal owners and collaboratorDelete the old publisher tuples, remove personal accounts as owners, and remove the engineer from the collaborator role GitHub added. Done when every list shows only org identities.
- Verify with a real release from the org identityCut an actual release through the org workflow for each package. Done when the new version shows the org publisher.
- Document members and exceptionsRecord everyone with publish rights in MAINTAINERS.md or GOVERNANCE.md, per OSPS-GV-01, and list every item that could not move along with its next action.
Notes on the steps that people get wrong
Step 3 on crates.io. Before you add a team, confirm you are a member of it. Before you add a named human, confirm they have logged into crates.io once.
Step 4 and the old name. GitHub permanently retires the old OWNER/NAME pair if the repo had more than 100 clones or more than 100 uses of GitHub Actions in the week before the transfer. Creating a new repo at the old location permanently deletes the redirects. Tell the engineer not to recreate a fork or placeholder under their personal account with the same name.
Step 4 and secrets. GitHub says webhooks, secrets and deploy keys stay associated, and that issues, PRs, wiki, stars and watchers transfer. Sources disagree on environment secrets. One community answer says environment secrets and organization-level secrets do not move. A practitioner says the docs are less explicit about environments, variables and repository-level self-hosted runners. Assume they did not move until you have checked.
Step 5 and the OIDC subject. One practitioner reports that GitHub now uses an immutable OIDC subject format with numeric owner and repo IDs for transferred repos. That comes from a single source, so check GitHub's changelog before you write any publisher rule that depends on the subject format.
A repo transfer without a cleanup step gives the departing engineer write access they did not have before.
How does a handover go wrong?
Most failed handovers looked finished. A step reports success while an old access path stays open, or an artifact never made it into the inventory. Every failure mode below has a check that takes minutes, so run all of them before you sign off.
- "Repo transferred" read as "done". GitHub automatically adds the original owner as a collaborator on the transferred repo. Check: the repo's collaborators list after the move.
- Stale trusted publisher. The old owner and repo tuple stays registered on PyPI after the human is removed, because removing a user does not remove the Trusted Publishers they registered. Check: the project's publishing settings list only org tuples.
- Inventory from GitHub only. This misses GHCR images, which stay in the personal namespace, and user-scoped npm names. Check: search every package.json for
@<personal-handle>/and list the personal GHCR namespace. - Owner changes run from CI tokens. Bypass-2FA npm tokens can no longer change maintainers, so a script that "succeeded" may have quietly skipped the change. Check:
npm owner ls <pkg>for every npm row. - A team as the only owner on crates.io. A team cannot add or remove owners. If the named human leaves, nobody can manage ownership. Check: at least two named company humans in
cargo owner --list. - The PGP key leaves with the engineer. Maven Central says losing the private key may limit recovery. Check: the signing key is in a company vault, not on a laptop.
- Environment secrets assumed to have migrated. Sources conflict on whether they move. Check: every environment's secrets exist after the move, and a workflow that reads them has run.
- Out-of-date OSPS wording. OSPS-AC-01.01 says "access" in the dated baseline release and "read or modify... authoritative repository" in a later checklist. Check: which baseline version your MAINTAINERS.md cites.
Several of these can be false positives. A clean npm owner ls proves who the owners are, but it says nothing about trusted publishing configuration. A successful release from the org workflow proves the new path works, but not that the old one is closed. A green CI run after the transfer proves repo secrets exist, but not environment secrets if that workflow never used an environment. Each check covers one path, so you need all of them.
How do you prove the handover worked and keep it current?
A handover is proven when each package has shipped a real release through the org identity, every owner and publisher list shows only company identities, and the remaining exceptions are written down with an owner and a date. Write that state into the repo so the next departure starts from a list and not from a search.
The OpenSSF Open Source Project Security Baseline (OSPS Baseline) gives you the structure. OSPS-GV-01.01, a Level 2 control alongside GV-01.02 on roles, requires documentation listing project members with access to sensitive resources. OSPS-AC-01.01 requires MFA when accessing a sensitive resource in the version control system. At Level 3, OSPS-GV-04.01 requires a policy to review collaborators before granting escalated permissions, which is the control that would have stopped one person from holding everything. The ossf/security-baseline repo has the current text.
## Publish rights (OSPS-GV-01.01, baseline version: <cite version>) | Package | Registry | Named owners (2+) | Org/team owner | Trusted publisher (owner/repo/workflow) | Signing key location | |---|---|---|---|---|---| | <name> | npm | <person A>, <person B> | <org> | <org>/<repo>/<workflow.yml> | n/a | | <name> | Maven Central | <person A>, <person B> | namespace <id> | n/a | company vault: <entry> | ## Exceptions | Item | Why it could not move | Next action | Owner | Due | |---|---|---|---|---| | @<personal-handle>/<pkg> | User-scoped npm name cannot move | Republish as @<org>/<pkg>, deprecate old | <person> | <date> | | io.github.<user> | Personal Maven namespace | Central Support case <number> | <person> | <date> | Last reviewed: <date> by <person>
Commit this at the repo root; update it in the same PR that changes any owner or publisher.
Before the engineer's last day
- Every row in the inventory sheet has an action and a done date or an exception entry.
- Every repo, environment secret value and signing key is in the company vault, and the PGP key is not only on a laptop.
- npm owner ls and cargo owner --list show at least two named company humans for every package.
- No crates.io crate has a team as its only owner.
- Every PyPI, npm and crates.io project lists only org trusted-publisher tuples.
- The departing engineer is not in the collaborator list of any transferred repo.
- GHCR images under the personal namespace have been moved or listed as exceptions.
- Every environment's secrets exist in the transferred repo and a workflow using them has run.
- Each package has a new release whose registry page shows the org publisher.
- MAINTAINERS.md lists publish rights and every exception with an owner and due date.
- Any Maven Central or PEP 541 support case has a number recorded in the exceptions table.
Keeping it current
Registry rules on tokens and 2FA keep tightening, so re-check the mechanisms instead of copying the dates in this guide. For npm token policy and the OIDC subject format, read the GitHub changelog. For PyPI publisher behaviour, read the Trusted Publishers security model and troubleshooting docs. For crates.io, read the Rust Forge trusted publishing page. Re-run the listing commands and the publisher review on a fixed schedule, and whenever anyone with publish rights changes role. Then the next notice period starts with a list you already have.
Questions practitioners ask
How do I transfer npm package ownership from a departing employee to the company?
Add the company owner first with `npm owner add <user> <pkg>`, wait for the new maintainer to accept the invitation, and only then have the departing engineer run `npm owner rm`. These maintainer changes now need an interactive 2FA session, and bypass-2FA tokens can no longer make them, so do the work with the engineer present before their last day. Check the result with `npm owner ls <pkg>`.
Does transferring a GitHub repo break PyPI trusted publishing?
Yes. After a transfer PyPI returns an invalid-publisher error, because the OIDC owner and repository values no longer match the registered publisher. You need to register the new org, repo and workflow tuple on each PyPI project and then remove the old one. The same applies on crates.io: the datafusion-contrib/tpcgen-rs project had to update its crates.io publishers after moving.
What if the maintainer has already left and cannot be reached?
If any other company owner is still on the package, use that account. If not, you are in the registry's recovery process. On PyPI that means a PEP 541 request through the pypi/support tracker, where first response averages about a week at best and the backlog has run more than a year in the past. Public timelines for npm, crates.io, RubyGems and Docker Hub disputes are not established. Plan in weeks, and keep a rename ready.
Can I move a user-scoped npm package into our org scope?
No. A scoped package cannot move to another user or organization, because the scope is the account name. You have to publish a new package under the org scope, edit the name field in package.json, and point users at the new name. Setting an org scope in your client only affects new packages. Record each user-scoped package as a documented exception with a republish date.
Is removing the engineer as a PyPI project owner enough?
No. Removing a user does not remove the Trusted Publishers they registered, because publishers are bound to the project and not to the person. If the old owner, repo and workflow tuple stays in the project's publishing settings, that CI path can still publish. Open the publishing settings and confirm that only org tuples are listed.
Should a GitHub team be the only owner of our crates.io crates?
No. A crates.io team owner can publish and yank, but it cannot add or remove owners. If the only named human leaves, nobody is left who can manage ownership. Keep at least two named company humans in `cargo owner --list` alongside the team, and remember that each named owner must have logged into crates.io before they can be added.
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.