The GitHub and npm settings open source maintainers should turn on before they need them
08 Jun 2026In the middle of a supply-chain incident, the maintainer is not just fixing packages. They are locked out of their account, answering reports, trying to contact registries, trying to warn users, and trying to prove what happened.
That is the part we do not talk about enough.
Most open source maintainers are not companies. They do not have incident response teams. They do not have a security department. They have a GitHub account, an npm account, a laptop, and a lot of people depending on them.
Security advice often assumes the maintainer is the weak link. That is backwards. The maintainer is the last line of defence, usually unpaid, usually alone, and often locked out of the systems they need during the incident.
This is the checklist I wish every maintainer had before something goes wrong. No single setting saves you, so it works in layers: the account, the branch, the release path, the workflow, the tokens, the files, the tripwires, and the recovery plan.
1. Protect your GitHub account
Everything below assumes the attacker does not control your GitHub account. Start here, because once someone has push access, a lot of "secure CI" assumptions collapse.
Use phishing-resistant login where you can. GitHub supports passkeys and hardware security keys, and a passkey signs you in and satisfies 2FA in one step. An OTP app is better than SMS; a passkey or security key is better than both, because there is no code to phish.
The ten-minute audit:
- Enable 2FA, then add a passkey or hardware security key
- Remove SSH keys you no longer recognise or use (settings/keys)
- Remove old personal access tokens; replace classic PATs with fine-grained ones scoped to specific repos
- Review authorised OAuth apps and GitHub Apps, and revoke anything you stopped using
- Check active sessions for devices you do not recognise
Calendar this twice a year. Credentials accumulate; nobody deletes them without an appointment.
2. Protect your release branch
Every package repo should protect main. In Settings → Branches (or the newer rulesets):
- Require pull requests before merging
- Require at least one approval
- Require status checks to pass
- Block force pushes
- Block branch deletion
- Require conversation resolution
- Restrict who can push directly, if your plan allows it
For solo maintainers there is an obvious objection: who reviews my PR? Be honest about what this layer does. Branch protection will not save you from yourself, and it will not save you from a fully compromised account that can change the rules. But it stops accidental pushes, basic automation abuse, and compromise of a lower-privilege collaborator. It also creates friction, and sometimes friction is the control: an attacker scripting hundreds of repos goes around protected branches rather than through them. Section 3 closes the OIDC snapshot-branch path when they go around.
The same June 2026 campaign also landed direct commits on main with stolen credentials, disguised as ordinary feature work. Branch protection with required review and signed commits makes that path much harder. The trusted publishing post covers the OIDC half.
3. Protect your release environment
This is the layer that stopped mattering being theoretical in June 2026, when a worm published malicious versions through projects' own trusted publishing pipelines by pushing throwaway snapshot-* branches containing a copy of the release workflow. The publishes carried valid provenance. See the mechanics and the fix for details; the short version:
- Create a GitHub environment called
release(Settings → Environments) - Restrict it to
main, or to the tags you actually release from - Add required reviewers if you can
- In npm's trusted publisher settings, set Environment name to
release
Jobs that reference an environment must satisfy its protection rules before they run. A workflow on a throwaway branch does not get the environment, does not get the OIDC claim, and the publish is rejected.
The principle:
A workflow should not be allowed to publish just because it exists. It should have to come from the release path.
4. Lock down GitHub Actions permissions
Default workflow permissions should be boring. Release permissions should exist only on the release job.
# workflow level: read-only
permissions:
contents: read
jobs:
publish:
environment: release
permissions:
contents: read
id-token: write # only here, never workflow-wide
The rest of the Actions checklist:
- In Settings → Actions → General, set default workflow permissions to read-only
- Pin third-party actions to full commit SHAs, not tags. GitHub's own guidance says a full-length SHA is the only way to use an action as an immutable release; tags can be moved to malicious commits.
- Avoid
pull_request_targetunless you fully understand what it grants. It runs with secrets in the context of your repo against code from a fork. - Never run untrusted PR code in a job that has secrets
5. Remove publish tokens from CI
The best npm token is the one that does not exist.
If trusted publishing is available for your setup, use it and delete the automation tokens it replaces. A long-lived publish token in CI secrets is the single most valuable thing an attacker can exfiltrate from your repo, and stealers now harvest CI secrets as standard practice.
The npm side of the audit:
- Use trusted publishing for CI releases; remove old automation publish tokens
- Where you still need tokens (e.g. installing private packages), use read-only granular tokens with short expiry
- In each package's settings, require 2FA for publishing and settings changes, and disallow token-based publishing where you can
- Audit the maintainers list on every package; remove people who no longer need access
- Check the trusted publisher settings themselves, and limit them to publish only, not every action
6. Treat release files as sensitive
Some files in your repo are not ordinary code. If a file can change what gets published, treat it as release infrastructure.
A CODEOWNERS file makes that surface visible, and combined with branch protection's "require review from code owners", changes to it demand sign-off:
# .github/CODEOWNERS
/.github/workflows/ @your-username
package.json @your-username
package-lock.json @your-username
pnpm-lock.yaml @your-username
yarn.lock @your-username
npm-shrinkwrap.json @your-username
rollup.config.* @your-username
vite.config.* @your-username
webpack.config.* @your-username
tsup.config.* @your-username
/scripts/ @your-username
Even in a small project where you are the only owner, the file documents which paths are dangerous. It says: these files are not ordinary code. Future you, future co-maintainers, and future security reviewers all benefit from that being written down.
7. Watch for snapshot branches
The June 2026 worm used throwaway branches with random names. Most maintainers do not have a SOC, and they do not need one. They need cheap tripwires:
- New branches with random or generated names
- Workflow files added or changed outside your normal branches
- Changes to
release.ymlor whatever your publish workflow is called package.jsonscripts changing, especiallypostinstallorpreinstallappearing- npm versions of your package you did not publish
- Actions runs with names you do not recognise ("Dependabot Updates" that you never configured)
Two free tools cover most of this. Watch your own repo (Watch → Custom → Releases and Discussions plus rule-based notifications), and subscribe to your own package on npm so new-version emails reach you. The cheapest alert that something is wrong is an email about a release you did not make, arriving while you still have account access.
8. Prepare for account lockout
This is the section maintainers need most, and the one nobody writes.
When an account is compromised, platforms suspend it. That is the right call for stopping the spread, and it has a brutal side effect: the maintainer loses access to the security log, the audit trail, the commit history, and the settings pages — the very evidence needed to prove which actions were not theirs, at exactly the moment everyone is asking.
So keep a recovery file outside GitHub: a password-manager secure note, an encrypted local file, even a printed sheet. For each package:
- npm package name
- GitHub repo URL
- npm owner account
- Trusted publisher settings (repo, workflow file, environment name)
- Normal release workflow path
- npm support: https://www.npmjs.com/support
- GitHub support: https://support.github.com
- Emergency contacts (co-maintainers, employer security team if relevant)
- Last known safe version
- The deprecate command:
npm deprecate <pkg>@<bad-version> "compromised - do not install"
- The supersede command (publish a clean version ABOVE the bad one,
because deprecation does not stop semver ranges resolving to it)
Add to it the moment anything changes. The file costs you twenty minutes on a quiet afternoon. Its absence costs you days in the middle of the worst week of your maintainer life.
Bottom line
Open source maintainers should not need to become incident response teams overnight.
Until the platforms build better defaults, a small set of controls protects you before the panic starts: phishing-resistant login, a protected release branch, a locked release environment, minimal Actions permissions, no long-lived publish tokens, release files treated as infrastructure, cheap tripwires, and a recovery plan that still works if your GitHub or npm access disappears.
None of this makes a maintainer invincible.
It just means one stolen credential does not immediately become dozens of compromised packages, hundreds of malicious versions, and a maintainer locked out of the audit logs mid-incident.
If you want the deeper dives: the trusted publishing setting that would have blocked the snapshot-branch attacks covers layer three in detail, and the package manager settings that would have blocked the Axios attack covers the consumer side. Publisher, consumer, and now the person in between.