Skip to content
Back to skills

Update Deps

ASecurity

Update dependencies safely — apply in-range minor/patch updates, then analyze each pending major in parallel and apply only the ones proven safe for this codebase, reporting the rest with justifications.

  • 7 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
ai-agentsjavascripttypescriptrustgojavabashreactvuenodeexpress

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 22, 2026

npx -y skills add jeffrigby/somepulp-agents --skill update-deps --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Update Deps?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Update Deps
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeffrigby-update-deps/badge)](https://www.skillsdirectory.com/skills/jeffrigby-update-deps)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: update-deps
description: Update dependencies safely — apply in-range minor/patch updates, then analyze each pending major in parallel and apply only the ones proven safe for this codebase, reporting the rest with justifications.
when_to_use: When the user asks to "update dependencies", "update deps", "upgrade packages", "bump dependencies", or "check what's safe to update".
argument-hint: "[minors-only|majors-only] [dry-run] [sequential]"
allowed-tools:
  - Bash("${CLAUDE_PLUGIN_ROOT}/scripts/dep-outdated.sh" *)
  - Bash(npm update*)
  - Bash(npm install*)
  - Bash(npm view*)
  - Bash(npm run*)
  - Bash(npx tsc*)
  - Bash(pnpm update*)
  - Bash(pnpm run*)
  - Bash(yarn up*)
  - Bash(yarn upgrade*)
  - Bash(yarn run*)
  - Bash(git status*)
  - Bash(git diff*)
  - Glob
  - Grep
  - Read
  - Agent
  - AskUserQuestion
  - TodoWrite
disable-model-invocation: true
---

# Update Dependencies (Orchestrator)

Bring a JavaScript project's dependencies current in two passes: apply everything the declared ranges already permit, then decide **per package** whether each pending major is safe for *this* codebase — and report the ones that aren't, with a reason the user can act on.

You are the conductor. `major-upgrade-analyzer` subagents do the per-major research; you sequence, gate, apply, and verify.

**Modes requested (optional):** "$ARGUMENTS"

## Scope

**JavaScript/TypeScript only** — npm, pnpm, and yarn (classic and berry), detected from the lockfile or the `packageManager` field. If the project has no `package.json`, stop and say so; don't improvise with pip or cargo.

Monorepos are supported: findings are attributed to the workspace that declared them, and upgrades are applied to that workspace rather than the root.

## Modes

Split `$ARGUMENTS` on whitespace:

| Token | Effect |
| --- | --- |
| `minors-only` | Apply in-range updates only. No major analysis. |
| `majors-only` | Skip the in-range pass. Analyze and apply majors against the versions already installed. |
| `dry-run` | Do all the analysis, change nothing. The approval gate is skipped and the plan is the output. |
| `sequential` | Analyze majors one at a time instead of in parallel. Slower; useful for debugging a stuck analyzer. |

Anything else is a scope hint passed to every analyzer (e.g. a path, or "src/ only").

Default: both passes, majors analyzed in parallel, one approval gate covering both.

## Workflow

The order matters: **nothing is written until the single approval gate in step 5.** That means the outdated snapshot is taken *before* any update runs, so the plan and the final report can both name exact `from → to` versions.

### 1. Preconditions

1. Confirm `package.json` exists. If not, stop.
2. Run `git status --porcelain`. **A dirty tree is not a blocker** — this skill never commits and never reverts — but if `package.json` or the lockfile already has uncommitted changes, tell the user before touching them, so they know what's theirs and what's yours.
3. Detect the toolchain:

   ```bash
   "${CLAUDE_PLUGIN_ROOT}"/scripts/dep-outdated.sh --detect
   ```

   Keep `manager`, `updateCommand`, `updateCommandTemplate`, `majorInstallTemplate`, and `workspaceFlagTemplate` — later steps use them instead of hardcoding a command.
4. **Workspaces**: if `workspaces` is true, note the globs. The report covers **every workspace**, not just the root — the script resolves each finding back to the workspace that declared it. What this means for step 7 is in "Workspace targeting" below; the short version is that a bare install at a workspace root writes to the *root* manifest, which is almost never what you want.

### 2. Snapshot what's outdated

```bash
"${CLAUDE_PLUGIN_ROOT}"/scripts/dep-outdated.sh
```

Inject the JSON into your context and work from it. Take this snapshot **first** — it is the only record of the pre-update versions, and both the plan and the report depend on it.

Three fields drive everything, and the first two are **independent axes, not a partition**:

- `upgrade` — the gap from installed to `latest`: `major`, `minor`, `patch`, or `unknown`
- `inRangeUpdate` — whether the declared range already permits a move (`current` → `wanted`)
- `type` / `dev` — which dependency section the package lives in

A package can be both (`chalk` 4.1.0, range `^4.1.0`, latest 6.0.0 → in-range to 4.1.2 *and* a major behind). Because they overlap, `counts.inRangeUpdate + counts.major` does not equal `counts.total` — never present it as a breakdown that sums.

Partition the packages:

| Group | Selector | Handled by |
| --- | --- | --- |
| **In-range** | `inRangeUpdate: true` | Step 6, no analysis — the declared range already permits it |
| **Majors** | `upgrade: "major"` | Step 4, one analyzer each; step 5 groups coupled families |
| **Unanalyzable** | `upgrade: "unknown"` | Nothing — but they **must** appear in the report's Notes |

Each package also carries `dependents[]` — every workspace that declares it, with that workspace's own range and section. Read it rather than assuming:

- `type` is the **strictest** section across all consumers, so a package that is a prod dependency in any workspace is reported as one. `dev` is true only when every consumer declares it as a devDependency.
- `range` is null when workspaces disagree (e.g. `api` pins `^4.0.14` while `client` pins `^4.1.0`). The per-workspace ranges are in `dependents[]`.
- More than one entry means an upgrade **must touch each of them**. Say so in the plan; a partial upgrade leaves the monorepo inconsistent.

`unknown` means the version couldn't be compared, usually because `current` is null: `node_modules` is missing, or yarn berry is running in PnP mode where there is no `node_modules` tree for npm's resolver to read. If *every* package is `unknown`, say so loudly rather than reporting a clean tree — suggest installing dependencies first, or for yarn PnP, running `yarn install --mode=update-lockfile` and re-running.

Carry `notes[]` through to the final report — that's where the script records fallbacks (yarn using npm's resolver), a missing `node_modules`, and a suspected failed lookup.

If there are no in-range updates and no majors, report that and stop.

### 3. Build the project brief

Every analyzer needs the same context, so assemble it once:

- **Runtime**: `engines.node` in `package.json`, `.nvmrc`, `.node-version`, `node --version`, and the Node versions in any CI workflow
- **TypeScript**: version from `package.json`, plus `module`/`moduleResolution`/`target` from `tsconfig.json`
- **Module format**: `"type"` in `package.json`; bundler if any
- **Framework**: React/Vue/Next/Express/etc. and version
- **Verification scripts**: which of `typecheck`, `test`, `build`, `lint` exist in `package.json` `scripts`
- **Source scope**: the globs analyzers should grep (from `$ARGUMENTS` if given, else the project's source dirs)

### 4. Analyze each major in parallel

Skip this step entirely under `minors-only`.

Launch one `major-upgrade-analyzer` per package with `upgrade: "major"`.

**Parallel (default):** issue all `Agent` calls in a single assistant message. Above ~10 majors, batch them ~8 at a time so results stay manageable.

**Sequential (`sequential` token):** one at a time.

Give each analyzer: the package (name, current, latest, type, range), the project brief from step 3, the scope, and an instruction to **return only its verdict block** — not to edit or install anything.

**Verify each result** before using it. A valid block starts with `### <package>:` and contains a `- **Verdict**:` line reading `safe`, `safe-with-edits`, or `wait`. If a result is empty, malformed, or missing the verdict, treat that analyzer as **failed** and put the package in the wait list with `analyzer failed — <cause>` as its reason. Never drop a launched package silently, and never infer a verdict the analyzer didn't state.

### 5. Resolve coupled upgrades

Do this **before** building the plan. Analyzers judge one package each, so a family locked together by exact peer pins produces N separate `wait` verdicts that all say the same thing: *this one can't move alone, orchestrator — sequence us.* Reporting them as N independent hold-backs is wrong twice over: it triples the apparent cost, and it buries the actual decision.

Collect every analyzer's `Coupled with` line. When two or more packages in this run name each other, they form one **group**.

For each group, re-read the member blocks and derive a group verdict by **discounting the mutual-pin gate** — that gate only says "not alone," and inside the group they aren't alone. What's left is the real question:

- Every member's *other* hard gates pass, and the combined breaking-change surface is empty or tiny → the group can be `safe` or `safe-with-edits`, applied as one atomic change.
- Any member has a non-peer gate failure, or the combined surface is large, or the release is too new to trust → the group is `wait`, and the justification is that reason, **never** "it's coupled."

Present and apply a group as a single unit — one plan entry, one line in the report, one install command per workspace listing every member. A partial application of a mutually-pinned family produces an unsatisfiable install.

### 6. One plan, one approval gate

This is the only gate. Present the whole plan — both passes — before anything is written:

```
Plan (nothing applied yet)

In-range updates — N packages, no decision needed (the declared range already allows these)
  chalk       4.1.0 → 4.1.2
  zod         3.20.0 → 3.25.76

Majors — safe (N)
  rimraf      3.0.2 → 6.1.3 (devDependency in: app)  — no breaking change is used here
Majors — safe with edits (N)
  chalk       4.1.2 → 6.0.0  — ESM-only; 2 files need import changes
Majors — hold back (N)
  react       18.3.1 → 19.2.0 — <one-line blocker>
```

Then `AskUserQuestion` once:

- **Apply in-range + safe majors** (recommended)
- **Apply in-range + safe + safe-with-edits** — *offer only when there are safe-with-edits verdicts*
- **Apply in-range only** — skip all majors
- **Cancel** — change nothing

(Under `majors-only` the in-range half of each label drops away; see below.)

Under `minors-only`, the only meaningful options are apply in-range or cancel. Under `majors-only`, drop the in-range half: offer apply safe / apply safe + safe-with-edits / cancel. Present only the options that apply.

Under `dry-run`, **skip this gate entirely** — the plan above is the deliverable. Print it, then the held-back justifications from step 8, and stop.

Apply only what was approved. Never widen the selection on your own; a `wait` package stays untouched even if you disagree with the analyzer.

### 7. Apply

**In-range first** (skip under `majors-only`, or if the user chose "apply safe majors only"):

Run the detected `updateCommand` (e.g. `npm update`, `pnpm update`, `yarn upgrade`).

On yarn berry `updateCommand` is `null` — there is no safe bulk form, since `yarn up '*'` resolves to latest and crosses majors. Apply `updateCommandTemplate` per package for each entry with `inRangeUpdate: true`, substituting `{name}` and `{range}`.

**Then the approved majors:** use `majorInstallTemplate` with `{name}` and `{latest}` substituted (e.g. `npm install chalk@6.0.0`). Prefer one command listing all approved packages when the manager supports it, so the resolver sees them together.

#### Workspace targeting

In a monorepo, `majorInstallTemplate` alone is **wrong and destructive**. A bare `npm install vitest@5.0.0` run at a workspace root adds `vitest` to the *root* manifest as a new `dependencies` entry and leaves the workspace that actually declares it untouched — a silent downgrade of correctness in two directions at once.

When `workspaces` is true, append `workspaceFlagTemplate` for every workspace in the package's `dependents[]`, substituting `{workspace}`:

| Manager | Shape | Example |
| --- | --- | --- |
| npm | flag, repeatable | `npm install vitest@5.0.0 -w client -w api` |
| pnpm | flag | `pnpm update vitest@5.0.0 --filter client` |
| yarn berry | command **prefix**, one workspace per command | `yarn workspace client up vitest@5.0.0` |
| yarn classic | no workspace targeting — `cd` into the workspace directory | — |

A `workspaceFlagTemplate` beginning with `prefix:` is a command prefix, not a trailing flag: strip the `prefix:` and put the rest in front of the command.

Skip the flag for a dependent whose `workspace` matches the root package's own name — that one really does belong to the root manifest.

Verify afterward: `git diff -- package.json '*/package.json'` should show the change landing in the workspaces you targeted and nowhere else. If a package appeared in the root manifest that wasn't there before, you hit exactly the bug above — say so plainly in the report rather than leaving it for the user to find.

npm rewrites `package.json` formatting (indentation, key spacing) whenever it touches a manifest, so a diff can look far larger than the change. Read the dependency lines, not the line count, and don't report a reformat as a change you made.

For `safe-with-edits`, make the code edits the analyzer specified **before** running verification.

### 8. Verify — report, don't revert

Run whichever of these exist, in this order, stopping at the first failure:

1. `<manager> run typecheck` (or `tsc --noEmit` if the script is absent but TypeScript is present)
2. `<manager> run build`
3. `<manager> run test`

Judge each step by its **exit code**, not by scanning output for the word "passed". In a monorepo, `npm test --workspaces` keeps going after a workspace fails and still prints later successes, so a passing line proves nothing about the run as a whole. Report *which* workspace failed, not just that testing failed.

**Report the outcome; do not revert.** If something fails, show the actual error output, name the packages that were applied in this run, and say plainly that `package.json` and the lockfile are modified and the failure is unresolved. Suggest `git diff package.json` and, if they want out, `git checkout package.json <lockfile>` followed by an install — but let the user run it.

This skill never commits.

### 9. Console report

Print, in this order:

```
## Dependency Update — <manager> <version>

### Applied
- In-range (minor/patch): N packages
  - <name> <from> → <to>
- Majors: N packages (coupled families count as one)
  - <name> <from> → <to> [workspaces: client, api] — <one-line reason it was safe>
  - <name> <from> → <to> [workspaces: client] — safe with edits: <files touched>

### Verification
- typecheck: pass / fail / not configured
- build: pass / fail / not configured
- test: pass / fail / not configured  <in a monorepo, name the failing workspace>
<on failure: the actual error output, which packages were applied in this run,
 and an explicit line that nothing was reverted>

### Held back — N majors
#### <name, or "<family> family (<member>, <member>, <member>)"> <current> → <latest>  (confidence NN)
<the analyzer's "Why wait" text: the specific blocker, what would have to change
first, and the rough cost>

### Notes
- Could not analyze — N packages: <names> (<cause, e.g. "not installed, so the
  current version is unknown">)   [omit the line when N is 0]
- <script notes[]: yarn npm-fallback, missing node_modules, suspected failed lookup>
- <analyzers that failed, as "<name> — <cause>">
- <in a monorepo: which workspaces each applied change landed in, and confirmation
  that the root manifest gained nothing it shouldn't have>
- Working tree is modified and uncommitted. Review with `git diff package.json`.
```

The `from → to` versions come from the step-2 snapshot; that's why it is taken before anything is applied.

Every held-back package gets a real justification. "May contain breaking changes" is not a justification, and neither is "it's coupled to another package" — say what the group as a whole is blocked on — if that's all an analyzer returned, say the analysis was inconclusive and why.

## Usage Examples

```
/update-deps
# Snapshot, parallel major analysis, one approval gate, apply both passes, verify

/update-deps dry-run
# Full analysis, nothing changed, plan printed

/update-deps minors-only
# In-range updates only, still behind the approval gate

/update-deps majors-only
# Skip the in-range pass; analyze and apply majors only

/update-deps sequential
# Analyze majors one at a time

/update-deps dry-run src/api
# Analysis only, with analyzers scoped to src/api call sites
```

## Notes

- Each analyzer returns a verdict block, never a report. You own grouping, approval, application, and the final markdown.
- Analyzers are peers and must not invoke each other.
- The confidence floor (≥ 80 for any `safe` verdict) lives inside the analyzer. Don't re-litigate its filtering.
- `major-upgrade-analyzer`'s description tells Claude not to invoke it directly; `/update-deps` is the entry point.
- Overlaps with `library-modernizer` (used by `/deep-audit`) are intentional and different in kind: that agent *reports* version drift as an audit finding; this skill *applies* upgrades.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…