# Maintainer

> An open source maintainer is a person with authority to merge changes, cut releases, and publish a project's packages that every user then installs.

Also known as: open source maintainer, package maintainer, project maintainer, package owner, co-maintainer

Source: https://socket.dev/glossary/maintainer

Last updated: 2026-11-02

An open source maintainer, also called a package maintainer or project maintainer, is a person with authority over an open source project: they review and merge changes, cut releases, and hold the registry credentials that publish new versions. Every install inherits the maintainer's judgment and security, which makes maintainers the highest-leverage target in the software supply chain.

The word sounds like one role, but it names three separate permissions kept in three separate systems. Write access to the source repository lets someone merge code. A role on the package registry lets someone publish the artifact users install. The `maintainers` field in package metadata is text the publisher writes about itself. Nothing keeps the three in sync, and only the registry role reaches your machine.

That registry role is the one that matters. The registry checks the credential on a publish, not the code, so one maintainer account can ship a new version to every project whose version range accepts it. Most of that power sits with individuals. In a 2022 count by Josh Bressers, [16 million of 28 million npm package releases had exactly one maintainer](https://socket.dev/blog/the-unpaid-backbone-of-open-source), and Tidelift's 2024 survey of 437 maintainers found 60% are unpaid.

Our position: treat the set of people who can publish a package as part of the dependency. A change in who holds publish rights deserves the scrutiny of a code change, and every registry should expose that set as public, machine-readable data. A trusted name is still not evidence of safe code. The most damaging recent attacks shipped through real maintainer accounts, so the check that holds up is analyzing what each new version contains.

## What is an open source maintainer?

An open source maintainer is the person who decides what goes into a project and ships it to users. The daily work is unglamorous: reviewing [pull requests](https://socket.dev/glossary/pull-request), triaging issues, keeping CI green, updating dependencies, writing release notes, and handling vulnerability reports. The authority is what defines the role. A maintainer can reject a change, and can publish a version that thousands of other projects pick up with no further review.

Maintainers also decide who else becomes a maintainer. That governance decision is as security-critical as any line of code, and in a solo project it is one person's call.

## What is the difference between a contributor and a maintainer?

A contributor proposes changes, and a maintainer decides whether they land and when they ship. A contributor opens an issue, or a pull request from a fork, without write access to the repository or the package. The maintainer reviews the change, merges it, and later publishes a release that contains it.

GitHub's [repository roles](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization) make the gradient explicit: Read, Triage, Write, Maintain, and Admin. Write lets someone push code. Maintain adds repository management, including pushing to protected branches, but excludes "sensitive or destructive actions." Admin adds collaborator management, deploy keys, and security advisories. No GitHub role grants permission to publish to npm or PyPI. Registry rights are granted separately, in a different system. [Trusted publishing](https://socket.dev/glossary/npm-trusted-publishing) connects the two on purpose: once a registry trusts a CI workflow, anyone who can change or trigger that workflow can publish.

Most people become maintainers the same way: sustained contribution, then commit access, then release rights. That ladder is how projects survive burnout. It is also an attack path, as the xz Utils case below shows. [The fsnotify maintainer dispute](https://socket.dev/blog/fsnotify-maintainer-dispute-sparks-supply-chain-concerns) in May 2026 showed the reverse failure. Commit rights handed out years earlier for one-off fixes no longer matched who was stewarding the project. When those rights were revoked, downstream users, including Kubernetes maintainers, started asking whether a routine update was still safe, because from the outside a governance cleanup and a takeover look alike.

## Three records of "maintainer" that disagree

Ask "who maintains this package?" and the answer depends on which record you read. For `requests`, the Python HTTP library, the core metadata of the latest release (2.34.2, uploaded May 14, 2026) and [PyPI](https://socket.dev/glossary/python-package-index-pypi)'s role table disagree:

```bash
$ curl -s https://pypi.org/pypi/requests/json \
    | jq '{author_email: .info.author_email,
           maintainer_email: .info.maintainer_email,
           roles: .ownership.roles}'
{
  "author_email": "Kenneth Reitz <me@kennethreitz.org>",
  "maintainer_email": "Ian Stapleton Cordasco <graffatcolmingov@gmail.com>, Nate Prewitt <nate.prewitt@gmail.com>",
  "roles": [
    { "role": "Owner", "user": "Lukasa" },
    { "role": "Owner", "user": "graffatcolmingov" },
    { "role": "Owner", "user": "nateprewitt" }
  ]
}
```

The metadata names one author and two maintainers. PyPI's [`ownership` key](https://docs.pypi.org/api/json/) lists three accounts with the Owner role. Two match the declared maintainers by username or email address. The third, `Lukasa`, holds full upload and administration rights and appears nowhere in the metadata. Kenneth Reitz, the declared author, holds no role on the project.

| Person or account | Named in package metadata | Role on PyPI | Can publish? |
| --- | --- | --- | --- |
| Kenneth Reitz | Author | None | No |
| Ian Stapleton Cordasco (graffatcolmingov) | Maintainer | Owner | Yes |
| Nate Prewitt (nateprewitt) | Maintainer | Owner | Yes |
| Lukasa | Not listed | Owner | Yes |

None of this is a mistake by the project. The [core metadata specification](https://packaging.python.org/en/latest/specifications/core-metadata/) defines the Maintainer field as a name and contact details, "intended for use when a project is being maintained by someone other than the original author." In metadata, "maintainer" means the person to contact. On the registry, it means the person who can ship code to you.

> Figure 1. Three records answer "who maintains this package?" Only the registry's role table decides who can put code on your machine. Metadata rides inside the tarball as unverified text.
>
> Three records of who maintains a package: the source repository grants merge rights, the package registry grants publish rights and is the only one that reaches users on install, and package metadata grants nothing.

Figure 1 shows the three records side by side. The source repository holds code-level roles (Read, Triage, Write, Maintain, Admin) that let someone merge. The registry holds publish roles (owner, maintainer, team) that let someone upload an installable version. Package metadata holds the author and maintainer fields, written by the publisher and checked by no one. A user's install pulls only from the registry, so only the registry roles decide who can put code on the user's machine. The repository reaches users only when someone with a registry role, or a trusted publishing workflow bound to the registry, publishes from it.

The [npm registry](https://socket.dev/glossary/npm-registry) has the same split, plus a history. `package.json` carries `author` and `contributors` fields that the publisher writes. The registry separately records who can publish (`npm owner ls`), and stamps every version with the publishing account (`_npmUser`) and a snapshot of the maintainer list at publish time. That snapshot turns the maintainer set into versioned data:

```bash
$ curl -s https://registry.npmjs.org/chalk | jq -r '. as $p
    | ["5.6.0","5.6.1","5.6.2"][] as $v
    | "\($v)\t\($p.time[$v])\t\($p.versions[$v]._npmUser.name // "(unpublished)")\t\([$p.versions[$v].maintainers[]?.name] | join(","))"'
5.6.0   2025-08-17T07:27:47.572Z   sindresorhus    sindresorhus,qix
5.6.1   2025-09-08T13:13:05.239Z   (unpublished)
5.6.2   2025-09-08T14:47:54.486Z   sindresorhus    sindresorhus
```

Chalk had two maintainers when 5.6.0 shipped. Version 5.6.1 was published at 13:13 UTC on September 8, 2025, during [the Qix compromise](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack), in which co-maintainer Qix's phished account pushed a crypto-wallet drainer into packages with 2 to 3 billion weekly downloads. Version 5.6.1 is gone from the registry, which kept only its timestamp. Version 5.6.2, published about 95 minutes later, lists one maintainer. As of November 2, 2026, `npm access list collaborators chalk` still returns only `sindresorhus: read-write`.

The rule of thumb: only the registry's role table answers "who can ship code to my machine?" Package metadata answers "who would like to hear from you?"

## How do package registries define maintainer permissions?

Package registries define maintainer permissions differently, and the difference sets how much one compromised maintainer can do. The question that matters is whether "publish a version" and "change who can publish" are separate rights.

| Registry | Roles | Who can publish | Who can add or remove publishers |
| --- | --- | --- | --- |
| npm, package owned by users | One level ("there is only one level of access") | Every owner | Every owner |
| npm, package owned by an organization | Owner, Admin, Member | All three roles | Owners and admins, who manage team package access |
| PyPI | Owner, Maintainer | Both roles | Owner only |
| RubyGems.org | Owner, Maintainer (added November 2024) | Both roles | Owner only |
| crates.io | Named user owner, GitHub team owner | Both kinds | Named owners only |

Sources: [npm owner docs](https://docs.npmjs.com/cli/v11/commands/npm-owner), [npm organization roles](https://docs.npmjs.com/organization-roles-and-permissions), [PyPI's role descriptions](https://github.com/pypi/warehouse/blob/main/warehouse/templates/manage/project/roles.html), [RubyGems.org's new maintainer role](https://socket.dev/blog/rubygems-adds-new-maintainer-role), and [the Cargo Book](https://doc.rust-lang.org/cargo/reference/publishing.html).

On PyPI, a Maintainer "can upload releases" but "cannot invite collaborators" and cannot delete files, releases, or the project. RubyGems added the same split for the reason Ruby Central engineer Colby Swandale gave: "As long as every user has owner permissions, gaining access any of those accounts is enough to fully take over a gem." On [crates.io](https://socket.dev/glossary/crates-io), a team owner can publish and yank but cannot change owners. The Cargo Book is blunt about the other kind: "Needless to say, you shouldn't make people you don't fully trust into a named owner."

You can read the list directly on three of these registries:

```bash
$ npm owner ls debug
qix <npm@josh.junon.me>
tootallnate <nathan@tootallnate.net>

$ curl -s https://crates.io/api/v1/crates/serde/owners | jq -r '.users[] | "\(.kind)\t\(.login)"'
user    dtolnay
team    github:serde-rs:publish

$ curl -s https://pypi.org/pypi/requests/json | jq -c '.ownership.roles'
[{"role":"Owner","user":"Lukasa"},{"role":"Owner","user":"graffatcolmingov"},{"role":"Owner","user":"nateprewitt"}]
```

Our verdict: split publish rights from ownership rights wherever the registry allows it. On PyPI and RubyGems, give co-maintainers the Maintainer role. On crates.io, publish through a team owner, as `serde` does, and keep named owners to one or two people. On npm, tiers exist only through organizations, so a package owned by individual accounts gives every co-maintainer the power to add or remove every other one. Know what the split does not do. A Maintainer role still publishes, so it does not stop the first malicious release. It stops an attacker from locking out the real owners or adding accounts for persistence, and on PyPI from deleting releases.

## Why are maintainers the main target of supply chain attacks?

Maintainers are the main target of supply chain attacks because one maintainer credential converts into code execution on every downstream machine, including through every [transitive dependency](https://socket.dev/glossary/transitive-dependency) path. That credential usually belongs to one unpaid person. Using ecosyste.ms data, Bressers estimated about 1.4 million unique people hold publish rights, and Andrew Nesbitt estimated roughly 10,000 maintain the packages behind 80% of usage, [according to our coverage of the Tidelift survey](https://socket.dev/blog/the-unpaid-backbone-of-open-source). The same survey found 61% of unpaid maintainers work alone.

Attackers borrow a maintainer's authority in three ways:

- **Take over the account.** Phishing, stolen tokens, or malware on the maintainer's laptop. [Maintainer account takeover](https://socket.dev/glossary/maintainer-account-takeover) covers these routes in depth. In March 2026, [a coordinated campaign hunting high-impact Node.js maintainers](https://socket.dev/blog/attackers-hunting-high-impact-nodejs-maintainers) used cloned companies, staged Slack workspaces, and fake meeting software to plant remote access trojans. One of those intrusions led to [the axios compromise](https://socket.dev/blog/axios-npm-package-compromised) on March 31, 2026. Malware riding a logged-in session makes 2FA irrelevant.
- **Inherit a dormant role.** Publish rights outlive interest. When [the node-ipc compromise](https://socket.dev/supply-chain-attacks/node-ipc) shipped three malicious versions on May 14, 2026, the package had twelve listed maintainers, and the publishing account had been [dormant on the package for years](https://socket.dev/blog/node-ipc-package-compromised). As of November 2, 2026, the registry lists one.
- **Be given the role.** Offer to help, do good work, or claim to be someone who already belongs, and receive publish rights. No login looks unusual. In [the `is` package hijack](https://socket.dev/blog/npm-is-package-hijacked-in-expanding-supply-chain-attack) in July 2025, an email that appeared to come from a removed former owner asked co-maintainer Jordan Harband to re-add the account. "Everything seemed normal, so I obliged," Harband said. Malware was published the next morning. The `event-stream` incident in 2018 followed the same pattern. Per [npm's incident report](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident), the attacker, "posing as a maintainer, took over maintainership" of the module. On September 9, 2018, the new maintainer added `flatmap-stream` as a dependency in `event-stream@3.3.6`, and `flatmap-stream@0.1.1` carried a payload that harvested private keys from large wallets in the Copay bitcoin app. npm's security team was notified on November 26, 2018, about 11 weeks later.

The third route is the hardest to defend, because every control along the way sees a legitimate maintainer doing legitimate maintainer things. It also produces no CVE, so vulnerability scanners have nothing to match.

## The promotion ladder is an attack path

The xz Utils case is the clearest example of an attacker earning the maintainer role instead of stealing it. [The xz Utils backdoor](https://socket.dev/blog/how-to-use-socket-to-find-out-if-you-were-affected-by-the-backdoored-xz-package) (CVE-2024-3094) came from "Jia Tan," a contributor who climbed every rung of the normal ladder over more than two years. Each step was a routine open source event. [Russ Cox's timeline](https://research.swtch.com/xz-timeline), built from the mailing list and commit history, dates each one.

> Figure 2. The xz Utils promotion ladder. Each rung was a normal open source event; indentation shows authority accumulating. Dates from Russ Cox's xz timeline.
>
> Timeline of how the xz Utils attacker climbed from contributor to releaser between October 2021 and March 2023, then shipped a backdoor in February 2024.

| Date | Event | Authority gained |
| --- | --- | --- |
| 2021-10-29 | Jia Tan sends a first, harmless patch to the xz-devel mailing list | None (contributor) |
| 2022-04-22 to 2022-06-21 | Personas "Jigar Kumar" and "Dennis Ens" press maintainer Lasse Collin to hand over the project | None |
| 2022-06-29 | Collin writes that "Jia Tan may have a bigger role in the project in the future" | None (social standing) |
| 2022-10-28 | Added to the Tukaani GitHub organization | Organization membership |
| 2022-11-30 | README lists Jia Tan as a project maintainer | Public maintainer title |
| 2022-12-30 | Merges commits directly into the repository | Commit access |
| 2023-03-18 | Tags and builds v5.4.2, the first release by Jia Tan | Release rights |
| 2024-02-24 | Releases v5.6.0 with a backdoor hidden in the release tarball | Used |
| 2024-03-29 | Andres Freund discloses the backdoor on oss-security | Discovered |

Two details matter. First, the pressure campaign targeted the maintainer's capacity, not his credentials. "Progress will not happen until there is new maintainer," one persona wrote on June 7, 2022. Second, the attacker was patient at both ends. Nearly 17 months passed between the first patch and the first release, and another 11 months between the first release and the backdoor. By the time the [backdoor](https://socket.dev/glossary/backdoor) shipped, Jia Tan had been the project's releaser for nearly a year, so any check that looks for a new or unfamiliar publisher would have stayed silent.

The pattern did not stop with xz. In April 2024, the OpenJS Foundation reported [a "credible takeover attempt"](https://socket.dev/blog/openjs-xz-utils-cyberattack-likely-not-an-isolated-incident) in which emails from several addresses asked that the sender be "designated as a new maintainer" of a popular JavaScript project. Maintainers grew more guarded: in Tidelift's 2024 survey, 66% said they now trust pull requests from non-maintainers less, and 37% vet their own co-maintainers more carefully. That caution is rational, and it makes recruiting the next maintainer harder for projects that already have only one.

## Watching maintainer changes from the consumer side

Consumers can observe maintainer changes in registry data, and the signal is useful within clear limits. On npm, three fields carry it: the current owner list, each version's `_npmUser`, and each version's `maintainers` snapshot. An account that has never published this package before, or a maintainer list that grows right before a release, is worth a look.

Socket turns those fields into alerts. [New author](https://socket.dev/alerts/newAuthor) fires when an npm collaborator publishes a version of a package for the first time. [Unstable ownership](https://socket.dev/alerts/unstableOwnership) fires when a new collaborator has begun publishing versions. New author is npm-only and rated low severity, because most new collaborators are legitimate. Unstable ownership is rated high severity.

Ownership signals have three blind spots:

- **Same-identity takeovers.** An attacker who publishes as the existing maintainer, as in the Qix and axios compromises, changes nothing in the ownership record.
- **Patient insiders.** An attacker who earns the role ages past any recency window, as Jia Tan did.
- **Noisy governance.** Legitimate handovers, cleanups, and disputes trip the same signals. In the fsnotify thread, a user noted that automated scanners call a project unmaintained after 12 months without a release, which pressured a mature library into shipping. A quiet project is not the same as one with [no one left to fix it](https://socket.dev/glossary/end-of-life-dependencies).

So ownership changes are a reason to look harder, not a verdict. The check that works whether the publisher is new, old, or hijacked is the content check: did this version add an install script, a network call, obfuscated code, or a new dependency it does not need?

## What maintainers can control, and what each control misses

Maintainers can shrink their own blast radius, and every measure leaves a gap:

- **Split roles.** Give co-maintainers publish-only roles where the registry supports them. A publish-only account can still ship one malicious version.
- **Use phishing-resistant 2FA.** A security key defeats relay phishing. It does nothing against malware already running in the maintainer's browser session.
- **Publish through CI with trusted publishing.** Removing long-lived tokens closes the stolen-token route, and npm [revoked all classic tokens on December 9, 2025](https://socket.dev/blog/npm-revokes-classic-tokens). Provenance proves which workflow built a version, not that a maintainer intended it. An attacker who controls the repository or the workflow publishes with valid provenance.
- **Prune dormant publishers.** Every account that can publish is attack surface. Pruning does nothing against compromise of an active maintainer.
- **Write down how maintainers are added.** A `GOVERNANCE.md` that requires a second maintainer's sign-off and a public announcement turns a quiet handover into a visible event. It does not stop an attacker patient enough to satisfy the process, as xz showed.

## The open problem: the role is social, the permission is a credential

Registries model a maintainer as an account. Projects experience a maintainer as a person with a job, a limited supply of attention, and a strong incentive to accept help. Attacks now target both sides of that gap: the account through phishing and malware, the person through flattery, pressure, and patience.

Registries have hardened the account side: role tiers on PyPI, RubyGems, and crates.io, and the revocation of classic tokens on npm. The person side has almost no tooling. Adding a stranger as an owner of an npm package with millions of weekly installs takes one command, and downstream users see it only as an edit to the packument's `maintainers` array. Removals can be just as quiet: in the `is` hijack, Harband was irritated that npm "would remove an owner without notifying the other owners." We think a change to a popular package's publisher set should be a first-class public event that downstream tools can subscribe to, with a waiting period before a new publisher's first release is installable. Until registries build that, the defense that covers all three routes ignores who published and reads what was published.
