Detecting NPM Package Owner Changes in CI: Supply Chain Defense (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
Stop Supply Chain Surprises in PRs
Propel flags risky dependency diffs, owner changes, and lifecycle scripts directly in code review.