| name | auditing-dependency-bumps |
| description | Use when reviewing a Dependabot (or similar) dependency-bump PR — npm/pnpm minor/patch group bumps or GitHub Actions SHA bumps — and you need to do a supply-chain audit before approving and merging. Covers downloading and inspecting package contents, checking for known compromises, the git-tap trick for Dependabot CI, and the approve/merge flow. |
Auditing dependency-bump PRs
Overview
Dependabot opens PRs that bump dependencies. Before merging, audit the actual package contents — a version number and changelog tell you nothing about whether the published tarball was tampered with. The npm ecosystem has had multiple supply-chain worms (e.g. the May 2026 "Mini Shai-Hulud" TanStack compromise) where a legitimate-looking version shipped credential-stealing, self-propagating malware with valid SLSA provenance. The audit is cheap (a few minutes, scriptable) and is the whole point of reviewing these PRs.
The end-to-end flow is:
- Identify what actually changed (filter out lockfile noise).
- Audit each changed package (download + inspect + known-compromise check).
- Approve with a
Claude Code:-prefixed summary of the audit.
git tap — push an empty commit so CI re-runs with full secrets (Dependabot PRs don't get repo secrets).
- Wait for CI green, then squash-merge.
Do steps 3–5 only after the user has asked you to "approve and tap" / "get it merged" — they involve outward-facing actions. The audit itself (steps 1–2) you can always do.
Step 1 — Identify what actually changed
package.json shows the direct bumps. The lockfile (pnpm-lock.yaml / package-lock.json) is noisy: most of its diff is peer-dependency resolution strings being rewritten, not real new code. For example, when @types/react bumps, every pkg(@types/react@X) bracket in the lockfile changes even though those packages got no new code.
gh pr diff <PR> --repo <owner>/<repo> > /tmp/pr.diff
grep -nE '^\s*[-+].*"[@a-z].*":\s*"[\^~]?[0-9]' /tmp/pr.diff
grep -nE '^[-+]\s+(\S+@[0-9]|"\S+@[0-9])' /tmp/pr.diff
Build the real audit list: direct bumps plus transitive packages whose own version actually moved (e.g. react-router-dom bumps drag react-router). Ignore packages whose only change is a bracketed peer-dep string.
Step 2 — Audit each changed package
For npm/pnpm packages, download old + new tarballs straight from the registry and compare. Run downloads in parallel.
cd /tmp && rm -rf scaudit && mkdir scaudit && cd scaudit
for p in "pkg@OLD" "pkg@NEW"; do
echo "### $p"
npm view "$p" dist.tarball dist.integrity scripts 2>/dev/null
done
dl() {
curl -fsSL "$1" -o "$2.tgz" || { echo "download failed: $1" >&2; return 1; }
rm -rf "$2" && mkdir -p "$2"
tar xzf "$2.tgz" -C "$2" || { echo "extract failed (corrupt or unsafe archive?): $2.tgz" >&2; return 1; }
}
dl "$(npm view pkg@NEW dist.tarball)" new & p_new=$!
dl "$(npm view pkg@OLD dist.tarball)" old & p_old=$!
wait "$p_new" && wait "$p_old" || { echo "a download/extract failed -- re-check before trusting the diff" >&2; }
Then check, in order of signal:
- Install hooks (highest signal).
scripts should have no preinstall / install / postinstall. These run automatically on npm/pnpm install and are the #1 malware vector. build / test / dev / lint scripts are fine — they don't run from a published tarball. A prepare: husky is also fine: prepare only runs when installing from a git source, not from a registry tarball.
- Binary blobs. No unexpected executables.
.map (source maps) and package.json classify as "JSON data" — those are fine.
find new -type f -exec sh -c 'file "$1" | grep -qiE "ELF|Mach-O|PE32|executable" && echo "$1: $(file -b "$1")"' _ {} \;
- File-list diff. New files should be explainable (a new
LICENSE-*, shipped docs/*.md, content-hash-renamed bundle chunks). A new .js in a package that's supposed to be types-only is a red flag.
diff <(cd old/package && find . -type f | sort) <(cd new/package && find . -type f | sort)
- Suspicious patterns — and check they're NOT pre-existing. Grep the new build for
child_process, eval(, atob(, fromCharCode, XMLHttpRequest, require('http, env-var exfil. Crucially, also grep the OLD build: many libraries legitimately use atob/fromCharCode (e.g. react-router's base64/turbo-stream encoding). If the pattern is already in the old version, the bump didn't introduce it.
grep -rIlE "child_process|eval\(|atob\(|fromCharCode|XMLHttpRequest" new/package | grep -v '\.map$'
grep -rIlE "child_process|eval\(|atob\(|fromCharCode|XMLHttpRequest" old/package | grep -v '\.map$'
- Source diff for small/patch bumps. For patch releases, actually read the diff — it should match the stated fix. (e.g. dompurify 3.4.8 was a Trusted Types re-entrancy guard; that's exactly what the diff showed.)
diff -u old/package/dist/<entry>.mjs new/package/dist/<entry>.mjs | grep -E '^[+-]' | grep -vE '^[+-]{3}|version'
npm audit on the new set:
mkdir audit && cd audit
npm install --package-lock-only --silent <pkg@NEW> ...
npm audit
- Known-compromise web search. Search for recent supply-chain incidents naming these packages/versions. Note that incidents are often family-scoped: in the May 2026 TanStack attack,
@tanstack/react-router was compromised but @tanstack/query* (which includes @tanstack/react-query) was explicitly confirmed clean — read the advisory carefully to see which exact packages and version windows are affected.
For GitHub Actions SHA bumps, the audit is different: verify each new SHA resolves in the canonical upstream repo (gh api repos/<owner>/<repo>/commits/<sha>), is reachable from a signed/annotated release tag, was merged by a known maintainer, and that action.yml didn't gain new pre/post scripts or credential-handling inputs. Large dist/ churn is often a benign bundler migration (e.g. ncc→esbuild) — confirm against the public release commits.
Step 3 — Approve
gh pr review <PR> --approve --body "Claude Code: Approved after supply-chain audit of all N bumps (...).
Method: downloaded old+new tarballs, diffed file lists, scanned for install hooks / binary blobs / exec+network patterns, ran npm audit, checked known compromises.
Findings — all clean: <one bullet per package>."
Step 4 — git tap (the key trick for Dependabot CI)
Dependabot-triggered workflow runs don't have access to repository secrets, so any job needing a secret fails — classically Pulumi Preview (prd) with The 'private-key' input must be set to a non-empty string. This is not a real failure of the change. Pushing an empty commit as yourself re-runs CI with full credentials and the secret-gated jobs pass.
git tap is a local alias for commit --allow-empty -m 'empty commit':
git fetch origin && gh pr checkout <PR>
git commit --allow-empty -m 'empty commit'
git push
Step 5 — Wait for CI, then merge
for i in $(seq 1 60); do
s=$(gh pr checks <PR> --json name,bucket,state 2>/dev/null)
if [ -n "$s" ] && ! jq -e 'any(.[]; .bucket=="pending")' <<<"$s" >/dev/null; then
jq -r '.[] | " \(.bucket|ascii_upcase)\t\(.name)"' <<<"$s" | sort
jq -e 'all(.[]; .bucket=="pass" or .bucket=="skipping")' <<<"$s" >/dev/null \
&& echo "ALL GREEN" || echo "NOT ALL GREEN"
break
fi
sleep 30
done
gh pr merge <PR> --squash --delete-branch
Gotchas
- Don't trust the lockfile diff size. Hundreds of changed lines are usually peer-dep-string churn from one type-package bump, not new code. Filter to packages whose own version moved.
prepare/build/test scripts are not install hooks. Only preinstall/install/postinstall auto-run from a registry tarball.
- Check the OLD version before flagging a "suspicious" pattern — most are pre-existing and benign.
- Advisories are family- and version-scoped. A compromised sibling package doesn't mean the one you're bumping is affected, and vice versa. Verify the exact name and version window.
- A failing secret-gated CI job on a Dependabot PR is expected — fix it with
git tap, don't treat it as a blocker or try to unlock/skip it.