← Back
April 4, 2026Claude6 min read
I use Claude to audit npm dependencies before every major upgrade
Published April 4, 20266 min read
Major version upgrades of npm packages used to be something I dreaded. I would run npm outdated, see 40 packages with major updates, and put it off for another quarter. Then I built a workflow that uses Claude to read changelogs and breaking change notes, and generate a prioritized upgrade plan. The audit that used to take a day now takes an hour.
The audit workflow
python
#!/usr/bin/env python3
# dep-audit.py
import subprocess
import json
import anthropic
import requests
from packaging.version import Version
client = anthropic.Anthropic()
def get_outdated_packages() -> list[dict]:
"""Get list of outdated packages with current/latest versions."""
result = subprocess.run(
["npm", "outdated", "--json"],
capture_output=True, text=True
)
if not result.stdout.strip():
return []
outdated = json.loads(result.stdout)
packages = []
for name, info in outdated.items():
current = info.get("current", "0.0.0")
latest = info.get("latest", current)
try:
current_v = Version(current)
latest_v = Version(latest)
is_major = latest_v.major > current_v.major
except Exception:
is_major = False
packages.append({
"name": name,
"current": current,
"latest": latest,
"is_major_update": is_major,
})
# Focus on major updates first
return sorted(packages, key=lambda x: x["is_major_update"], reverse=True)
def get_changelog(package_name: str, from_version: str, to_version: str) -> str:
"""Fetch changelog from npm registry or GitHub."""
try:
# Try npm registry for repo URL
registry_url = f"https://registry.npmjs.org/{package_name}"
resp = requests.get(registry_url, timeout=5)
data = resp.json()
repo = data.get("repository", {})
if isinstance(repo, dict):
repo_url = repo.get("url", "")
else:
repo_url = str(repo)
# Clean up GitHub URL
if "github.com" in repo_url:
repo_path = repo_url.split("github.com/")[-1].rstrip(".git")
# Fetch releases from GitHub API
releases_url = f"https://api.github.com/repos/{repo_path}/releases"
releases_resp = requests.get(releases_url, timeout=5)
releases = releases_resp.json()
if isinstance(releases, list):
relevant = [
r["body"] for r in releases[:10]
if isinstance(r, dict) and r.get("body")
]
return "
---
".join(relevant[:5])
except Exception:
pass
return f"Could not fetch changelog for {package_name}"
def analyze_upgrade_risk(packages: list[dict]) -> str:
"""Use Claude to analyze breaking changes and generate upgrade plan."""
# Build context with changelog info for major updates
context_parts = []
for pkg in packages[:15]: # Limit to 15 most important
if pkg["is_major_update"]:
changelog = get_changelog(pkg["name"], pkg["current"], pkg["latest"])
context_parts.append(
f"## {pkg['name']}: {pkg['current']} -> {pkg['latest']}
{changelog[:2000]}"
)
else:
context_parts.append(
f"## {pkg['name']}: {pkg['current']} -> {pkg['latest']} (minor/patch)"
)
context = "
".join(context_parts)
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=2000,
system="""You are a senior engineer reviewing npm dependency upgrades.
For each package, assess:
1. Risk level (HIGH/MEDIUM/LOW) based on breaking changes
2. What specifically breaks
3. Migration steps needed
4. Recommended upgrade order (start with low risk)
Format as a prioritized upgrade plan with clear sections.""",
messages=[{
"role": "user",
"content": f"Analyze these package updates and create an upgrade plan:
{context}"
}]
)
return response.content[0].text
if __name__ == "__main__":
print("Checking outdated packages...")
packages = get_outdated_packages()
if not packages:
print("All packages up to date!")
exit(0)
major_count = sum(1 for p in packages if p["is_major_update"])
print(f"Found {len(packages)} outdated packages ({major_count} major updates)
")
plan = analyze_upgrade_risk(packages)
print(plan)
Example output
text
## Upgrade Plan
### HIGH RISK — Do Last, Requires Migration
**next: 14.x -> 15.x**
Breaking changes:
- `cookies()`, `headers()` are now async — every Server Component using these needs `await`
- `params` in page.tsx is now a Promise — must be awaited
Migration: Run `npx @next/codemod@canary upgrade latest` first, then manually fix remaining issues
Estimated effort: 2-4 hours
### MEDIUM RISK — Test Thoroughly
**zod: 3.22 -> 3.23**
Breaking: `.email()` validation is now stricter (rejects some previously valid emails)
Migration: Review any email validation and update test fixtures
Estimated effort: 30 minutes
### LOW RISK — Safe to Upgrade
These packages have no breaking changes:
- axios: 1.6 -> 1.7 (security fix)
- date-fns: 3.3 -> 3.6 (new functions added)
- lodash: 4.17.20 -> 4.17.21 (security patch)
Recommended order: lodash → axios → date-fns → zod → next
Running in CI on a schedule
yaml
# .github/workflows/dep-audit.yml
name: Weekly Dependency Audit
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am
workflow_dispatch:
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- name: Run audit
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: python3 scripts/dep-audit.py > dep-audit.md
- name: Create issue
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('dep-audit.md', 'utf8');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Weekly Dependency Audit',
body,
labels: ['dependencies']
});
The weekly GitHub issue gives the team a clear, actionable upgrade plan every Monday. Dependencies stop accumulating for quarters and the upgrades become manageable incremental work.
Share this
← All Posts6 min read