Update dependencies
The goal is a project whose dependencies are current and have no known vulnerabilities, delivered as a change the user can trust: every step verified by the project's own checks, and every risky step isolated so it can be reviewed or reverted on its own.
Dependency work fails in two typical ways. One is the "big bang" upgrade, where everything is bumped at once, something breaks, and nobody can tell which bump caused it. The other is a "fix" that hides the problem, such as --force, --legacy-peer-deps, loosening a version range, or suppressing an advisory. The workflow below is built to avoid both.
0. Decide what was asked
- Check only ("any vulnerabilities?", "what's outdated?"): do steps 1–3, report, and stop. Don't modify files.
- Update (the default for "update my deps", "fix the audit warnings", "keep packages up to date"): do the whole workflow.
- One package ("upgrade vite to v8"): do steps 1–2, then step 6 for that package (along with any packages that must move with it), then 7–8.
1. Preflight
-
Read the repo's contributor docs (
AGENTS.md,CLAUDE.md,CONTRIBUTING.md) and CI config (.github/workflows/*) to learn the verify commands (lint, typecheck, test, build) and the commit-message convention. Use what the repo actually runs in CI. If nothing is documented, use the scripts defined in the manifest. -
Make sure the working tree is clean. If it isn't, stop and ask. Mixing the user's uncommitted work into dependency commits causes real harm.
-
Detect the ecosystems by their lockfiles and manifests (a repo can have several):
Files Reference package-lock.json,pnpm-lock.yaml,yarn.lock,bun.lockreferences/node.mdrequirements*.txt,pyproject.toml,uv.lock,poetry.lock,Pipfile.lockreferences/python.mdCargo.toml/Cargo.lockreferences/rust.mdgo.mod/go.sumreferences/go.mdanything else (Gemfile.lock, composer.lock, …) use the ecosystem's own tool, plus scripts/osv_check.pyfor the auditRead the matching reference file. It lists the exact commands and that ecosystem's pitfalls.
-
Find the runtime version the project targets:
.nvmrc,engines,.python-version,requires-python,rust-version, thegodirective, or the version in CI. Some packages must track the runtime, not the latest release. For example,@types/nodeshould match the Node major the project runs on, so bumping it to a newer Node major lets code use APIs that crash in production. Treat "latest" as a candidate, not a target. -
Run the verify commands before changing anything and note the result. If something already fails, you need to know that now. Otherwise you'll blame an upgrade for a failure that was already there, or ship a red branch. If the baseline is red, tell the user and ask whether to continue.
2. Inventory
Collect two lists with the ecosystem's tools (see the reference):
- Vulnerabilities: the native audit tool. If it isn't installed and can't be run ephemerally, use the bundled scanner:
python3 <skill-dir>/scripts/osv_check.py <lockfile>...(queries osv.dev; stdlib only). Running the OSV scan as well as the native tool is cheap and often worth it, because native tools sometimes miss advisories in nested or duplicated copies of a package. - Outdated packages: current → wanted (within the declared range) → latest.
For each vulnerability, work out which direct dependency pulls in the vulnerable package and which version of that direct dependency fixes it. Users act on direct dependencies. A transitive package name alone isn't actionable.
3. Plan and report
Sort everything into three buckets and show the plan as a short table before you make changes:
- Security fixes: anything that removes an advisory. Most urgent. If the fix needs a major bump, it is a major (bucket 3), but list it first there and say it's security-driven.
- Safe bumps: patch/minor within semver (for 0.x packages, a minor counts as breaking).
- Majors: everything else, one row each. Group packages that must move together (a framework and its plugins, a tool and its type definitions, packages that share a peer dependency) and treat each group as one unit.
Severity matters for ordering, and reachability matters for the explanation. A critical advisory in a dev-only test runner is still worth fixing, but say plainly that it's dev-only rather than alarming the user. For check-only requests, this report is the deliverable. Include severity, the advisory ID, the dependency path, and the fix version.
4. Branch
Create chore/deps-YYYY-MM-DD (today's date) from the up-to-date default branch, unless the user is already on a branch made for this work.
5. Security fixes and safe bumps
- Apply in-range security fixes, re-run the verify commands, and commit.
- Apply the safe patch/minor bumps together, re-run the verify commands, and commit. If verification fails, bisect: revert half the batch and retry until you find the culprit, then pull it out and handle it like a major.
Let the package manager write the lockfile. Never edit lockfiles by hand.
6. Majors, one unit at a time
For each major unit, in order (security-driven first, then foundational tools before things that depend on them, e.g. the compiler before the linter plugin that peers on it):
- Read what changed. Find the migration guide or release notes for every major you're crossing (v2→v5 means reading v3, v4 and v5). Good sources are the project's
CHANGELOG.md, GitHub releases, and the docs site's "migration" page. Pull out the breaking changes that apply to this codebase: grep for the removed or renamed APIs and config keys. - Check peers. Make sure the new version satisfies the peer and engine requirements of everything around it. Sometimes the latest major can't be adopted yet because a plugin hasn't caught up. Then take the newest version that works, and record what's blocking the rest.
- Upgrade and fix. Bump the unit, apply the migration (config renames, API changes, codemods if the project ships them), and re-run the verify commands until they pass. Fix the code properly. Don't weaken tests, add
skips, or silence type errors withanyor@ts-ignore-style escapes to get to green. - Commit the unit on its own, with the migration notes in the body.
- Know when to stop. If a unit needs a large refactor or a product decision (dropped feature, changed runtime behavior you can't verify), or you can't get it green with reasonable effort, revert it cleanly (
git checkout -- .back to the last commit) and record it as deferred, with the specific blocker. A deferred major with a clear reason is a good outcome. A half-done one is not.
7. Re-verify and re-audit
Run the full verify commands and the audit again on the final state. Every advisory should now be either fixed or explained. For an advisory you couldn't fix:
- If it's transitive and the parent hasn't released a fix, an override/resolution that pins the patched transitive version is acceptable only if that version is semver-compatible with what the parent expects. Verify it, and note it in the PR so someone removes it later.
- If no patched version exists anywhere, say so, and assess whether the vulnerable code path is reachable in this project.
- Never use ignore/allowlist mechanisms to make the audit pass silently.
8. Deliver
Commits follow the repo's convention. If the repo uses Conventional Commits and says nothing more specific, use:
fix(deps): ...for a security fix to a runtime dependencybuild(deps): ...for upgrades, and for security fixes to dev-only tooling- the summary names the package and target, e.g.
build(deps): bump vitest to v5. The body lists the advisories fixed and any migration changes made.
Push and open a PR (gh pr create) unless the user asked otherwise or there's no remote. Use this body:
## Summary
<one or two sentences: what moved, how many advisories are resolved>
## Security
| Advisory | Severity | Package (path) | Fixed by |
|---|---|---|---|
## Updates
| Package | From | To | Type | Notes |
|---|---|---|---|---|
<Type = security / patch / minor / major; Notes = migration changes made>
## Deferred
| Package | Latest | Why not now |
|---|---|---|
<blocked peer, needs refactor, runtime pin, ...; "none" if empty>
## Remaining advisories
<none, or each with reason and reachability assessment>
## Verification
<the exact commands run on the final commit and their result>
Finish with a short chat summary: the PR link, the number of advisories fixed vs. remaining, and the deferred majors with one-line reasons.