---
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:

1. **Identify what actually changed** (filter out lockfile noise).
2. **Audit** each changed package (download + inspect + known-compromise check).
3. **Approve** with a `Claude Code:`-prefixed summary of the audit.
4. **`git tap`** — push an empty commit so CI re-runs with full secrets (Dependabot PRs don't get repo secrets).
5. **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.

```bash
gh pr diff <PR> --repo <owner>/<repo> > /tmp/pr.diff
# direct bumps:
grep -nE '^\s*[-+].*"[@a-z].*":\s*"[\^~]?[0-9]' /tmp/pr.diff
# real version changes in the lockfile (a node whose own version moved):
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.

```bash
cd /tmp && rm -rf scaudit && mkdir scaudit && cd scaudit
# metadata + install scripts (the most important single check):
for p in "pkg@OLD" "pkg@NEW"; do
  echo "### $p"
  npm view "$p" dist.tarball dist.integrity scripts 2>/dev/null
done
# download + extract. You are handling UNTRUSTED archives, so:
#   1. curl -fsSL          -> fail fast on any HTTP error or redirect.
#   2. fresh dedicated dir + NEVER pass -P/--absolute-names. Default tar (bsdtar
#      on macOS, GNU tar on Linux) strips leading "/" and refuses ".." members
#      that would escape the dir -- bsdtar hard-errors (non-zero exit, caught
#      below). -P is the only thing that turns this protection off, so don't.
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.
  ```bash
  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.
  ```bash
  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.
  ```bash
  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$'  # baseline
  ```
- **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.)
  ```bash
  diff -u old/package/dist/<entry>.mjs new/package/dist/<entry>.mjs | grep -E '^[+-]' | grep -vE '^[+-]{3}|version'
  ```
- **`npm audit`** on the new set:
  ```bash
  mkdir audit && cd audit
  npm install --package-lock-only --silent <pkg@NEW> ...   # or write a minimal package.json
  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

```bash
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'`:

```bash
git fetch origin && gh pr checkout <PR>
git commit --allow-empty -m 'empty commit'   # = git tap
git push
```

## Step 5 — Wait for CI, then merge

```bash
# poll until no pending checks:
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
# if green:
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.
