Skip to content
Back to skills

Maintaining Dependencies

ASecurity

Judgment for keeping a pnpm workspace's dependencies patched and current without a person in the loop — how to fix a transitive advisory, when an override is a lie, why cooldown does not apply to security patches, how to read a red Dependabot PR by its failure signature, and what a CodeQL alert needs before it may be dismissed. Use when running /app:upgrade, fixing a dependency advisory, diagnosing a red Dependabot PR, or deciding whether a bump is safe to land unattended.

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 23, 2026
ai-agentsgobashreactnodeexpressdebugginggitsecurity

Works with

  • cli

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 26, 2026

npx -y skills add dork-labs/dorkos --skill maintaining-dependencies --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Maintaining Dependencies?

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

Security grade badge for Maintaining Dependencies
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dork-labs-maintaining-dependencies/badge)](https://www.skillsdirectory.com/skills/dork-labs-maintaining-dependencies)

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: maintaining-dependencies
description: Judgment for keeping a pnpm workspace's dependencies patched and current without a person in the loop — how to fix a transitive advisory, when an override is a lie, why cooldown does not apply to security patches, how to read a red Dependabot PR by its failure signature, and what a CodeQL alert needs before it may be dismissed. Use when running /app:upgrade, fixing a dependency advisory, diagnosing a red Dependabot PR, or deciding whether a bump is safe to land unattended.
user-invocable: false
---

# Maintaining Dependencies

`/app:upgrade` is the procedure; this is the reasoning it leans on. Read it
before the first lane, and come back to the signature table the moment a
dependency PR goes red.

## Core principle: three files, one truth

A pnpm workspace states its dependencies in three places, and they disagree
silently:

| File                  | What it is                | Who may be lying                           |
| --------------------- | ------------------------- | ------------------------------------------ |
| every `package.json`  | a **claim** about a range | any spec an override rewrites              |
| root `pnpm.overrides` | a **rewrite** of claims   | any entry whose reason is gone             |
| `pnpm-lock.yaml`      | the **truth** — what runs | never, but it only tells you what, not why |

Plus the ledger, `contributing/dependency-overrides.md`, which is where the
_why_ lives because `package.json` is strict JSON and cannot carry a comment.

Every change keeps all four consistent **in one commit**. A bump that moves a
spec but not the override that masks it changed nothing and now lies. An
override that moves without its ledger entry outlives its reason. The
override-masking check — for every touched override, every workspace spec
naming that package is at or below its floor — is not optional, it is the
difference between a bump and a claim.

## Direct or transitive — the first question for any advisory

```bash
pnpm why <package>                                             # paths from workspace packages
grep -oE "^  /?<package>@[0-9][0-9.]*" pnpm-lock.yaml | sort -uV   # every resolved line
```

**Direct** (a workspace `package.json` declares it): bump the spec in every
declaring package, keeping the dep type and range style the file uses. No
override — a redundant override duplicates a spec the repo already declares
and hides the next bump (ledger rule 2).

**Transitive** (only reached through something we depend on): an override in
the root `package.json`. Reach for it only when no direct bump clears the
finding — if the parent has shipped a version that resolves past the
advisory, bump the parent instead (ledger rule 1).

**Scope the override to the major line that exists.** When the lockfile holds
two major lines of one package (`brace-expansion@1`, `@2` and `@5` today; once
`js-yaml@3` for `gray-matter` beside `js-yaml@4`), a bare override forces one
version on all of them and breaks the consumer that needed another line. Write
one entry per line. The same
holds for 0.x lines — `fflate@0.4` beside an untouched `fflate@0.8`. After
the relock, list the resolved lines again: the vulnerable one is gone and
every other line is still there.

**Raise the floor to the patched version, not to a range that admits it.**
`hono: ^4.12.34` allowed 4.13.5 and the lockfile stayed on vulnerable 4.13.4
for nine days, because nothing asked for a re-resolve. pnpm records the
overrides block in the lockfile; a changed override is what forces
`pnpm install` to re-resolve the affected package. An unchanged override
forces nothing.

**Never `pnpm audit --fix`.** It writes blanket, unscoped overrides — the
exact shape the previous paragraph forbids — and it does not touch the ledger.

**Drop a transient pin when the parent moves past it.** The check: remove the
entry, `pnpm install --lockfile-only`, and if the resolved version is still at
or above the patched one, the parent's own range now carries the fix and the
pin is redundant. Ledger rule 2 applies in reverse: a pin that does nothing
will fight the next bump.

## Cooldown: features wait, fixes do not

The 21-day cooldown is supply-chain hygiene for **feature** bumps — a
compromised release is usually pulled within days, and a feature can wait
three weeks at no cost.

A **security patch inside the current line** is the opposite trade: waiting
keeps a known hole open for the sake of a hypothetical one. It moves on day
zero. The two HIGH `js-yaml` advisories sat for a week because a run applied
cooldown to a CVE fix; do not repeat that.

A fix that needs a **major** is neither: it is a migration with a security
motive, and it goes to the majors ticket with "security" as the first row of
its decision table.

## What never lands unattended

Not "usually not" — never, under `auto`, regardless of how safe it looks:

- **A major version.** Behaviour changes; the decision needs a person and a
  migration guide, and the guide is wrong about one thing per package.
- **A runtime-SDK family** (`@anthropic-ai/claude-agent-sdk*`,
  `@anthropic-ai/sdk`, `@openai/codex*`, `@opencode-ai/*`). Each sits behind an
  adapter boundary and moves in seven places in one commit through the
  `upgrading-runtime-dependencies` skill. A bump of half a family breaks the
  packaged desktop app (DOR-1644).
- **A family edit to `.github/dependabot.yml`.** Version-scoped ignores for a
  known-bad version are fine; adding, removing or narrowing a family pattern
  is not — the lockstep test exists because that went wrong once.
- **A secret-scanning alert.** Not dismissed, not rotated. Headline and stop.
- **The merge itself.** A PR is the unit of blast radius; the queue and the
  automated review are the gate.
- **`pnpm audit --fix`, `git checkout -- <path>`, `git stash`.** The first
  writes unscoped overrides; the other two are blocked by the git guard
  because they have destroyed work here.

## Reading a red Dependabot PR

Dependabot never diagnoses. The next weekly run force-pushes a larger version
of the same failure, so a red group PR stays red until someone reads it.
Read the signature before the diff — in this repo the cause is almost always
one of these, and the fix is different for each:

| Signature in the failing job                                                                                                                                                  | What it is                                                                                                                                                                                                                                                                                | Fix                                                                                                                                                                                                                                                          |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` on **every** job                                                                                                                          | Dependabot regenerated the lockfile without the `overrides:` block (DOR-1644)                                                                                                                                                                                                             | `dependabot-lockfile-repair.yml` pushes the fix; if its commit is absent the workflow failed; check the `dorkos-merge-tail` app secrets `MERGE_TAIL_APP_CLIENT_ID` and `MERGE_TAIL_APP_PRIVATE_KEY`. On a takeover branch a plain `pnpm install` repairs it. |
| A spec bumped in the diff, lockfile still resolves the **old** version                                                                                                        | **Override masking** — a dedupe override (`lucide-react`, `@vitejs/plugin-react`, `drizzle-orm`, `eslint-plugin-react-hooks`) pins it                                                                                                                                                     | Move the override to the bumped version in the same commit, or revert the spec. #1847 shipped `lucide-react 1.44.0` in three manifests over a lockfile still on 1.39.0.                                                                                      |
| `TS7056: The inferred type of this node exceeds the maximum length…` in `apps/server/src/services/core/auth/index.ts`, or `BetterAuthError: Drizzle schema mismatch` in tests | Either a **duplicated peer instance** — two copies of `@better-auth/core` / `@better-auth/utils` / `jose` after a `better-auth` bump (DOR-1538 shape) — or a **real upstream change**: 1.7.3 dropped `account.issuer` and outgrew the inferred type, with one core in the tree (DOR-2036) | Count the cores first. Two: move the whole family together (ledger, `better-auth` family). One: read the release notes for a schema change, migrate, and keep `AuthOptions` in `auth/index.ts` in step. Holding is the last resort, never the fix.           |
| `'error' is of type 'unknown'` across `route-error-fallback.tsx`                                                                                                              | `@tanstack/react-router` narrowed its error type                                                                                                                                                                                                                                          | Fix the call sites (narrow with `instanceof Error`, matching `app-crash-fallback.tsx`); check `main` for a fix PR already in flight before writing one                                                                                                       |
| A wall of type errors in a package that imports **neither** bumped package                                                                                                    | Duplicated peer instance, generic form                                                                                                                                                                                                                                                    | `grep -oE "^  <suspect>@[0-9.]+" pnpm-lock.yaml \| sort -u` for each peer the failing package declares; two versions = the fork                                                                                                                              |
| Only the load-sensitive suites red; rerun passes                                                                                                                              | Contention on the shared runner, not a defect                                                                                                                                                                                                                                             | `gh run rerun --failed` once and believe the second result. The `debugging-test-failures` skill names the suites that flake under load.                                                                                                                      |
| `Dependabot Updates` run red, but the group PR opened anyway                                                                                                                  | One dependency skipped (`HelperSubprocessFailed`, often `minimumReleaseAge` refusing a 2-day-old version)                                                                                                                                                                                 | Cosmetic. Nothing to do.                                                                                                                                                                                                                                     |
| `Dependabot Updates` run red on a `for <package>` security job: "latest possible version … is `<current>`"                                                                    | The package is transitive; Dependabot cannot write a pnpm override                                                                                                                                                                                                                        | Structural. Lane 1 of `/app:upgrade` is the only fix; the run will stay red until the advisory closes.                                                                                                                                                       |

**Peel, don't patch.** On a takeover branch, revert the culprit's spec lines
and relock; do not fix the culprit's breakage inside a 37-package bump. The
green remainder lands today; the culprit gets a hold, a version-scoped
Dependabot ignore, and a ticket with the signature. Two rounds of peeling is
the budget — past that, the suspect list goes in the ticket and the lane
stops.

## The ledger

`contributing/dependency-overrides.md` has three sections and each override
or hold belongs to exactly one:

- **Deliberate pins** — the version is a decision (runtime SDKs, dedupes,
  the Node line). These move only through a deliberate bump.
- **Transient security pins** — an unpatched advisory reachable through a
  dependency we do not control. Listed by name; dropped when the parent moves.
- **Version holds that are not overrides** — exact specs in each declaring
  `package.json` (`better-auth`, `@a2a-js/sdk`), each with a failure
  signature and a drop condition.

A run that adds an override adds the name to the second list. A run that
holds a culprit writes a subsection in the third, in the shape the existing
ones use: what breaks, the PR that showed it, why not a middle version, the
exact recipe to re-test, and the drop condition. A ledger entry without a
drop condition is a pin forever.

## CodeQL alerts: the evidence bar

An alert is a claim about a code path. Dismissing it is a counter-claim, and
the counter-claim needs a line of code:

- **`used in tests`** — the path is under `__tests__/`, is a `*.test.ts`, or
  is in `apps/e2e/`, and the rule is about production exposure
  (`js/missing-rate-limiting` on a test's express app is the common one).
  The file path is the evidence; quote it.
- **`false positive`** — the input is bounded before it reaches the sink, and
  you can name the line: the allowlist, the `path.resolve` + prefix check,
  the length cap before the regex. Quote that line in the dismissal comment.
  No line, no dismissal — file a ticket instead.
- **`won't fix`** — never from an unattended run. That is a product decision.

A **fix** from an unattended run is allowed when it is confined to one
function, ships a regression test that fails before and passes after, and is
one of at most two fix PRs in the run. `js/path-injection` in a handler that
joins user input into a filesystem path usually qualifies (resolve, then
assert the prefix). `js/tainted-format-string` where the taint is a log
message usually qualifies (pass the value as an argument, not in the format
string). `js/polynomial-redos` usually does **not** — the fix is a different
regex, and a different regex is a behaviour change that needs a reviewer who
knows the inputs. Ticket it with the rule help attached.

## When the daily run should do nothing

- `main`'s typecheck is red. A dependency PR cannot be told apart from the
  pre-existing failure; nothing can be validated. Report and stop.
- A secret-scanning alert is open. Nothing else matters until it is closed.
- An earlier run's PR for the same lane is open. Tend it; never open a twin.
- The merge queue is non-empty and the lane's PR is `CLEAN`. Leave arming to
  `merge-tail`; an entry armed into a busy ALLGREEN queue is the one most
  likely to be ejected for a neighbour's red.

Doing nothing and saying so is a successful run. Skipping a lane and printing
a clean summary is the one failure this skill exists to prevent.

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…