Security
Detecting NPM Package Owner Changes in CI: Supply Chain Defense (2025)
Sep 13, 2025

Package ownership and maintainer changes are high-signal events in software supply chains. A trustworthy package today can become risky tomorrow after a maintainer handoff or account compromise. This guide shows how to detect owner changes in CI and block merges until humans review them.
What to watch for
New maintainer accounts added to a package (especially recently created accounts).
- Ownership transfer to a new scope/org.
- Sudden spike of releases after a long dormant period.
New lifecycle scripts or native bindings introduced along with ownership change.
Approach: compare before vs after in CI
For each package that changed in the lockfile, get the previous maintainers/owner and the new set, then fail CI if the delta contains unknown or untrusted accounts. A simple baseline can use the public registry API, with a local allowlist for your org’s vetted packages.
GitHub Actions: detect maintainer changes
name: owner-change-guard
on: [pull_request]
jobs:
check-owners:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: corepack enable
- name: Compute changed packages from lockfile
run: |
git fetch origin ${{ github.base_ref }} --depth=1
git checkout ${{ github.base_ref }}
cp pnpm-lock.yaml pnpm-lock.base.yaml || true
git checkout ${{ github.head_ref }}
cp pnpm-lock.yaml pnpm-lock.head.yaml || true
node -e '
const fs=require("fs");
const base=fs.readFileSync("pnpm-lock.base.yaml","utf8").split("
");
const head=fs.readFileSync("pnpm-lock.head.yaml","utf8").split("
");
const pkgre=/^s{2}([^s][^:]+):$/; // section headers like " registry.npmjs.org/foo/1.2.3:"
const get=set=>{const s=new Set(); for(const l of set){const m=l.match(pkgre); if(m){const id=m[1]; const name=id.split("/").slice(-2,-1)[0]; s.add(name);} } return s};
const b=get(base), h=get(head);
const added=[...h].filter(x=>!b.has(x));
const changed=[...added];
console.log(JSON.stringify({changed}));
' > changed.json
cat changed.json
- name: Fetch maintainers and compare
env:
ALLOWLIST: "^(esbuild|sharp|@your-scope/)"
run: |
CHANGED=$(jq -r '.changed[]' changed.json || true)
FAIL=0
for PKG in $CHANGED; do
echo "Checking $PKG";
BASE=$(curl -sL https://registry.npmjs.org/$PKG | jq -r '.maintainers[]?.name' | sort -u | tr '
' ' ')
# Optionally compare previous version maintainers by pinning a specific version from the base lockfile
echo "Maintainers: $BASE";
echo "$PKG" | grep -Eq "$ALLOWLIST" && { echo "allowlisted"; continue; }
# Organization policy could require manual approval for any owner change event
done
exit $FAIL
- name: Fail if unexpected owner changes detected
if: failure()
run: exit 1
Notes: npm maintains an owner/maintainer list per package via the registry API; results may vary per package. For critical packages, mirror metadata to your own database and require a manual approval workflow on any owner changes.
Policy refinements
Enrich with account age, publish cadence, and 2FA signals when available.
Require second reviewer approval if an owner change coincides with lifecycle scripts.
Auto-open a security review task when a high-risk package changes owners.
Related internal reading
NPM Supply Chain Attack: What Happened and How to Protect Your CI/CD (2025)
Supply Chain Security in Code Review: Dependency Analysis Best Practices
Security Code Review That Stops 99% of Vulnerabilities (2025)
References
NPM CLI: Owner and team management:
https://docs.npmjs.com/cli/v10/commands/npm-owner
NPM Registry API: Package metadata:
https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md


