Why Pastoralist: Dependency Overrides Need an Audit Trail (original) (raw)
Many JavaScript codebases eventually accumulate overrides or resolutions in package.json.
A CVE lands in a transitive dependency so a dependency is pinned to a safer version. The PR gets merged. Six months later, the override is still there, but the reason could be gone. No one knows so often the override stays.
There are other reasons for overrides but there isn't a clear way to track the reason. Until now.Pastoralist[1] provides a set-it-and-forget-it way to manage this issue.
TL;DR
- If your repo uses
overrides,resolutions, orpnpm.overrides, Pastoralist[1] can help document why those pins packages exist and whether they still matter. - It turns override blocks into a reviewable audit trail, which is useful when a security patch, compatibility workaround, or temporary pin outlives the PR that introduced it.
- It can help identify stale overrides that no longer match anything in the dependency tree, so cleanup becomes a small review instead of a manual archaeology project.
- I would appreciate help testing Pastoralist on different package managers, monorepos, and security workflows. Feedback, bug reports, awkward edge cases, and PRs are all useful.
The Problem
A typical package.json override block can look like this:
package.json - undocumented overrides
{ "overrides": { "semver": "^7.5.4", "ws": ">=8.17.1", "axios": "1.12.0", "lodash": ">=4.17.23" } }
When reviewing that block later, a maintainer usually needs to answer:
- Why is each override here?
- Which packages in your dependency tree actually depend on it?
- Is the package still needed?
- Did a new CVE show up since you pinned it?
package.json alone usually cannot answer those questions. The missing piece is an audit trail.
Quick review of 10 Popular OSS Projects regarding overrides
To get a concrete sample, lets look at the root package.json of 10 well-known open source projects and count every overrides, resolutions, and pnpm.overrides.
Stars, dependency trees, and override counts move. The useful signal is the pattern: projects add overrides, but the reason rarely lives with the override.
Initial Override Snapshot
| n8n | ~51k | pnpm.overrides | 58 | ~17 |
|---|---|---|---|---|
| Storybook | ~85k | resolutions | 19 | ~3 |
| Prisma | ~41k | pnpm.overrides | 5 | 5 (form-data, hono, jws, tar-fs, lodash) |
| VS Code | ~167k | overrides | 2 | 1 (node-gyp-build) |
| React | ~233k | resolutions | 2 | 1 (jsdom) |
| React Native | ~120k | resolutions | 3 | 2 (on-headers, compression) |
| Supabase | ~77k | pnpm.overrides | 0 | 0 (cleaned up since initial audit) |
| Angular CLI | ~27k | overrides + resolutions | 3 | 1 (typescript) |
| Gatsby | ~55k | resolutions | 2 | 0 |
| Cal.com | ~34k | resolutions | 41 | 3+ |
These numbers are a February 20, 2026 snapshot captured with Pastoralist 1.11.0 using Claude. In that snapshot, there were 135 overrides across 10 projects. At least 33 looked security-related, or about 24%. None of the override entries carried their rationale inline.
These examples show why understanding overrides can be hard to review later:
n8n had 58 overrides. Some look like security patches (ws, semver, axios). Some look like compatibility pins (date-fns, chokidar, esbuild). Some are forks ([[email protected]](/cdn-cgi/l/email-protection) remapped to npm:[[email protected]](/cdn-cgi/l/email-protection)). The intent is easier to understand when the history is recorded near the override.
Prisma had 5 overrides that all appeared security-related. The >= ranges suggest "at least this version to patch a CVE," but the CVE context is not visible from the entry itself.
Supabase had 4 overrides when this was first written and has since removed all of them. That's a useful example of the lifecycle Pastoralist is meant to support: add a pin, keep the context while it is needed, and remove it when it is not.
React shows the smaller version of the same issue: the entry pins jsdom to 22.1.0, but the package file does not say whether that was for security, compatibility, or test environment behavior.
The common pattern: override intent often lives outside package.json.
What's actually still needed?
Counting overrides gives a rough view of how much review context may be missing. A dependency-aware scan can show which entries still match the current tree.
For a cleanup sample, I ran Pastoralist against five larger override sets to see which overrides were still active in the current dependency tree:
Unused Override Cleanup Sample
| n8n | 58 | 56 | 97% |
|---|---|---|---|
| TanStack Router | 51 | 41 | 80% |
| Cal.com | 41 | 35 | 85% |
| Next.js | 16 | 3 | 19% |
| Prisma | 5 | 5 | 100% |
In this sample, 140 of 171 tested overrides no longer matched anything in the current dependency tree, or 82%.
One useful cleanup case was Prisma: all 5 overrides were security patches for packages that had since left the dependency tree. Those entries were not harmful, but they were still carrying old operational context.
Follow-Up: Current Branch Findings
On May 25, 2026, I reran the study against clean fork branches synced from upstream for projects with current root override or resolution surfaces. This skipped older local work branches and opened PRs against my forks rather than the upstream projects.
Pastoralist added an audit trail for 275 override or resolution entries across 5,252 scanned package manifests:
The useful output is reviewable in the PR diffs: one Pastoralist run turns a plain override block into a package-by-package ledger that records which dependency still needs the override and why the constraint exists.
Fork PR Study Results
Two smaller TanStack projects also made good supporting studies:
TanStack Supporting Studies
The follow-up changed the practical finding from "projects have undocumented overrides" to "the missing operational record can be generated across different dependency graphs." n8n and Cal.com are the largest examples in this sample, but the same pattern appears in framework, database, design-system, docs, and developer-tooling repos.
A few recognizable names are not in this follow-up table. TanStack Router no longer had root overrides or resolutions on the current upstream branch. React Router and TanStack Hotkeys need separate validation because their workspace-only override shape triggered removal behavior instead of producing appendix entries. That is exactly the kind of edge case where more testing and feedback would help.
What Pastoralist Does
Pastoralist is meant to help with this by doing three things:
1. Track
When you run pastoralist, it scans your overrides and builds an appendix that documents each one:
package.json - Pastoralist appendix
{ "pastoralist": { "appendix": { "lodash@>=4.17.23": { "dependents": { "express": "^4.18.0", "async": "^3.2.0" }, "ledger": { "reason": "Security vulnerability CVE-2021-23337", "severity": "high", "addedDate": "2025-01-15T00:00:00.000Z" } } } } }
The appendix records why an override is there, what depends on it, and when it was added. That keeps context with the override instead of relying on memory or old PRs.
2. Scan
Pastoralist can check your overrides against vulnerability databases. It ships with support for 4 providers:
- OSV (free, no auth required)
- GitHub Advisory Database
- Snyk
- Socket
If a new CVE affects one of your overridden packages, Pastoralist can flag it and help generate a fix. Interactive mode lets you choose which fixes to apply.
3. Clean
Overrides that are no longer needed get flagged for removal. If the parent dependency updated and no longer pulls in the vulnerable version, Pastoralist can tell you the override is stale and can be removed after your normal CI checks pass.
This is the maintenance loop Pastoralist is meant to support: overrides can be added quickly during an incident, kept with enough context to review later, and removed when the dependency tree catches up.
Before and After
Before Pastoralist: Mystery overrides with no context.
package.json - before Pastoralist
{ "overrides": { "trim": "^0.0.3" } }
After Pastoralist: Audit trail kept with the override.
package.json - after Pastoralist
{ "overrides": { "trim": "^0.0.3" }, "pastoralist": { "appendix": { "trim@^0.0.3": { "dependents": { "remark-parse": "4.0.0" }, "ledger": { "addedDate": "2025-01-15T00:00:00.000Z", "reason": "Security vulnerability", "severity": "high", "cve": "CVE-2020-7753" } } } } }
CI/CD
Pastoralist ships as a GitHub Action with three modes[2]:
check-- Validates overrides are documented and current. Fails on vulnerabilities whenfail-on-securityis enabled.update-- Updates overrides and the appendix in place.pr-- Creates a pull request with the changes.
A weekly cron for reviewable override maintenance:
.github/workflows/pastoralist.yml
name: pastoralist on: schedule: - cron: "0 5 * * 1" jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: yowainwright/pastoralist@v1 with: mode: pr check-security: true fail-on-security: true
For CI pipelines, --quiet --checkSecurity exits with code 1 if vulnerabilities are found. JSON output is available for programmatic consumption.
Monorepo Support
If your package.json has a workspaces field, Pastoralist auto-detects it. Set depPaths: "workspace" and it scans all workspace packages. The appendix stays in the root so workspace packages stay clean.
Getting Started
Install Pastoralist
npm i pastoralist -D
Then run it:
Run Pastoralist
npx pastoralist
Or add it to postinstall:
package.json - postinstall
{ "scripts": { "postinstall": "pastoralist" } }
Works with npm, yarn, pnpm, and bun. The docs site has interactive demos and StackBlitz sandboxes if you want to try it without installing anything.
How fast is it?
For a timing sample, I ran Pastoralist against seven projects, including the cleanup sample above, with a full OSV security scan (--checkSecurity --securityProvider osv). No API key, no auth:
Pastoralist Scan Timing Sample
| n8n | 58 | 416ms |
|---|---|---|
| TanStack Router | 51 | 252ms |
| Cal.com | 41 | 406ms |
| Storybook | 19 | 177ms |
| Next.js | 16 | 1,050ms |
| Prisma | 5 | 196ms |
| React Router | 0 | 108ms |
The median run was 252ms. The three largest override sets all finished under 500ms, network call included. Next.js took 1,050ms because it also evaluates patch files alongside its overrides. React Router had no overrides and finished in 108ms.
The practical takeaway: this is fast enough to run in CI or on a weekly scheduled PR without adding meaningful friction.
Why This Exists
Pastoralist grew out of the same maintenance problem behind my older posts about building your own Dependabot and Snyk and Dependabot ignore configs. Those tools are useful for dependencies, but override lifecycle management still tends to sit between dependency management and security scanning.
Pastoralist makes that blind spot more reviewable. It is useful today for documenting and cleaning overrides, and I would especially appreciate feedback on package-manager edge cases, workspace layouts, security provider integrations, and CI ergonomics.
If this matches a maintenance problem in your project, try it and let me know where it works or fals short. If your project does not have overrides yet, a clean baseline can make the first emergency pin easier to review later. ~Thanks!