Finding 7: the dep-hygiene gap
Every finding so far has lived inside the running app — a swallowed role check, a raw-HTML render, a missing audit row, an absent header, a leaked key, an unthrottled endpoint. This one lives one layer down, in the files that decide what code is even allowed onto the machine in the first place. Your goal for this lesson is to document the supply-chain gap in pnpm-workspace.yaml — three safe-by-default flags turned off — as findings/007-dep-hygiene.md, with all four template sections filled.
There is nothing to open in the browser for this one, and that is worth saying out loud, because it is a different kind of audit step. The previous findings had a fingerprint you could see — a curl -I response, a DevTools request, a form that took ten submits without a 429. This finding has no running-app fingerprint at all. It is a deterministic read of two config files, with no install and no server needed. You open pnpm-workspace.yaml, you read three lines, and you are done finding it. The work is entirely in writing up why those three lines are the gap — and in not mistaking the corroborating signal for the defense.
Here is the shape you are filling — the same four-section template every finding in this pass uses, with the Category and Severity header on top:
# Finding 007 — Supply-chain defaults disabled in pnpm-workspace.yaml
**Category:** Dependency hygiene (security baseline).**Severity:** high — …
## Rule## Location## Consequence## FixBy the end you will have a finding that names the rule as the pnpm 11+ supply-chain defaults, records the three disabled flags off a single rg on the workspace file, corroborates with pnpm audit --prod, and proposes the fix as a config change — keep the defaults on — rather than a version-bump chore.
Your mission
Section titled “Your mission”This is the finding inexperienced teams resist, and they resist it with one specific sentence: “but we just enabled Dependabot.” That sentence is the whole reason this finding exists, because it confuses two controls that operate at completely different moments. Dependabot and Renovate raise pull requests, and pnpm audit reports known-bad versions — but both of those happen after a compromised release has already landed in the registry and, very likely, already landed in your tree. They are post-install signals. The defense this target turned off is minimumReleaseAge, and it is the only pre-install control of the three: it holds every install back behind a 24-hour window, which is roughly the time the community needs to catch and yank a poisoned release before your machine ever pulls it. Frame the finding against the real threat — typosquats and maintainer-compromise worms like Shai-Hulud, where the attacker’s entire window is the few hours between publishing the bad version and the registry pulling it — and it reads as a missing control, not a chore. A team with Dependabot green and minimumReleaseAge: 0 is still fully exposed in exactly that window.
The audit step is a read, and that is the part to internalize. There is no install, no pnpm dev, no clicking. You rg the workspace file, you confirm the three flags are off — minimumReleaseAge, blockExoticSubdeps, strictDepBuilds — and you confirm two things that turn out healthy, so you do not waste a launch reviewer’s time on them: allowBuilds is a reviewed allow-list rather than a blanket “run every build script,” and the packageManager field in package.json is pinned to an exact pnpm version. You also rg the .npmrc, and this is the common misread worth pre-empting in your own writeup: pnpm 11 reads these supply-chain settings from pnpm-workspace.yaml, never from .npmrc. A reviewer who greps .npmrc for minimumReleaseAge, finds nothing, and concludes the controls are fine has looked in the wrong file. Then you run pnpm audit --prod — not as the finding, but as the corroborating post-install signal that proves the point: real advisory-bearing versions are already sitting in the tree, pulled in transitively, which is exactly what a 24-hour window would have given the community a chance to flag.
Two things are out of scope, and naming them keeps the finding honest. You do not patch the target — the three flags stay off at the end of this lesson, because the deliverable is a documented finding, not a fixed workspace file, and the fix is a paragraph. And you do not wire the CI gate — enforcing pnpm audit and pnpm install --frozen-lockfile on every push is real work, but it belongs to the signal-checks lesson in chapter 97; name it as the fix’s forward thread, do not build it here. For the rule itself — the pnpm 11+ defaults, the threat model, the rotation and allow-list mechanics — lean on the supply-chain lesson in chapter 81 rather than re-deriving it. The finding’s job is to name the rule, not re-teach it.
findings/007-dep-hygiene.md has all four template sections filled and names the rule as the pnpm 11+ supply-chain defaults (chapter 81, lesson 8).minimumReleaseAge, blockExoticSubdeps, strictDepBuilds — names pnpm-workspace.yaml as where the settings live (not .npmrc), and names the discovery rg plus the pnpm audit --prod corroboration.allowBuilds / onlyBuiltDependencies allow-list and the packageManager pin in package.json.pnpm audit and Dependabot as post-install signals from minimumReleaseAge as the pre-install defense — no “could potentially” hedging.minimumReleaseAge: 1440, blockExoticSubdeps: true, strictDepBuilds: true), keeping allowBuilds as the reviewed allow-list, bumping the advisory-bearing deps, and gating CI as the forward thread.Coding time
Section titled “Coding time”Write findings/007-dep-hygiene.md now, against findings/template.md and the brief above — run the rg on pnpm-workspace.yaml, confirm the flags, run pnpm audit --prod to corroborate, and fill the four sections — before you open the walkthrough. Reading the worked finding first robs you of the one rep this finding teaches: telling a pre-install defense apart from a post-install signal in your own words.
Reference solution and walkthrough
The defect, in source
Section titled “The defect, in source”Here is the file the audit lands on. Three top-level keys override pnpm 11’s safe shipped values, and the comment above them is the seed flagging itself — but in a real target there would be no comment, just the three flags.
12 collapsed lines
# SEEDED AUDIT DEFECT #7 (finding 7) — dep-hygiene gap (081 L8).## The three flags below explicitly OVERRIDE the safe pnpm 11+ defaults:# - minimumReleaseAge: 0 → installs a release the moment it ships (no 24h window)# - blockExoticSubdeps: false → allows git/tarball/exotic transitive specs# - strictDepBuilds: false → does not fail an install when an un-acknowledged# dependency wants to run a build script# A grep finds these with no install step — this is the load-bearing, deterministic# evidence. The healthy shape keeps the defaults ON (minimumReleaseAge: 1440,# blockExoticSubdeps: true, strictDepBuilds: true). The pnpm-audit pin is the# corroborating secondary evidence; pnpm settings live HERE, never in .npmrc. The# target ships the bug on purpose; do not "fix" it here.minimumReleaseAge: 0blockExoticSubdeps: falsestrictDepBuilds: false# The healthy build allow-list stays correct so `next build` passes despite# strictDepBuilds: false — the seeded gap is the three flags above + the audit pin,# NEVER the build allow-list.onlyBuiltDependencies: - sharpallowBuilds: sharp: true esbuild: false # Pulled in by @trigger.dev/sdk + the trigger.dev CLI; neither needs its build # step for our use (the worker bundles via the CLI), so acknowledge-but-skip. protobufjs: false '@depot/cli': false # Pulled in transitively (posthog-js); its build step is not needed here. core-js: falseoverrides: kysely: 0.28.17Read this the way the audit reads it. The three flags at the top are the gap. Everything below them is the part that calibrates your eye to not over-report: onlyBuiltDependencies and allowBuilds form a reviewed allow-list — sharp is allowed to run its build step because the project genuinely needs it, and every other package that wanted to run a build script is named and acknowledged-but-skipped (esbuild: false, protobufjs: false, and so on). That allow-list is correct. It is, in fact, what lets the defect ship green: with the build allow-list intact, next build and pnpm verify pass even though strictDepBuilds: false is sitting right there. A relaxed strictDepBuilds does not break a build — it just removes the guardrail — which is precisely why an audit catches this and a CI run does not.
The second file to read is .npmrc, and the point of reading it is what it does not contain.
engine-strict=trueauto-install-peers=trueThat is the whole file: two registry/auth-shaped settings, neither of them a supply-chain control. This is the misread the finding pre-empts. pnpm 11 reads minimumReleaseAge, blockExoticSubdeps, and strictDepBuilds from pnpm-workspace.yaml, never from .npmrc. Grepping .npmrc for those flags returns zero hits — and zero hits in the wrong file is not evidence the controls are healthy. The finding records .npmrc explicitly as the not-where-the-settings-live file so a later reader does not repeat the mistake.
The discovery — a read, then a corroboration
Section titled “The discovery — a read, then a corroboration”The whole audit step for this finding is two reads and one corroborating command. The read is the load-bearing evidence; pnpm audit only backs it up.
# 1. The load-bearing, deterministic check — read the workspace file, no install.rg -n 'minimumReleaseAge|blockExoticSubdeps|strictDepBuilds|allowBuilds' pnpm-workspace.yaml# Confirm settings do NOT live in .npmrc (the common misread).rg -n 'minimumReleaseAge|blockExoticSubdeps|strictDepBuilds' .npmrc # -> zero hits# 2. The corroborating post-install signal — read the output, do not treat it as the defense.pnpm audit --prodThree things to read off this block. Grep 1 returns the three disabled flags plus the allowBuilds allow-list — the deterministic evidence, no install required. Grep 2 returns nothing, and that nothing is the proof that .npmrc is the wrong place to look. And pnpm audit --prod is the corroboration: it reports that real advisory-bearing versions are already in the tree — high-severity command-injection advisories in systeminformation pulled in transitively through @trigger.dev/sdk, and esbuild dev-server advisories reached through better-auth > drizzle-kit. Those are transitive, so the audit output is the only way you would see them. They are not the finding — they are the demonstration of the finding’s consequence: with minimumReleaseAge: 0, there was never a window in which any of them could have been caught before install.
The distinction to carry into the writeup is this: the read is the finding (a config that turns the defense off), and the audit is the symptom (bad versions that the absent window let through). If you write it the other way around — leading with the audit count as if outdated deps were the gap — you have written a version-bump ticket, not a supply-chain finding.
The finding
Section titled “The finding”This is the completed findings/007-dep-hygiene.md as it lands in the repo. Read it top to bottom; it is the deliverable, and the sections build on each other.
# Finding 007 — Supply-chain defaults disabled in pnpm-workspace.yaml
2 collapsed lines
**Category:** Dependency hygiene (security baseline).**Severity:** high — a malicious release installs the instant it lands in the registry, on a project that ships background workers and runs `pnpm install` in CI and on every developer's machine; it does not directly expose data today, so it sits below the live secret leak (finding 5), but the blast radius of one compromised transitive is the whole runtime.
## Rule2 collapsed lines
pnpm 11+ ships supply-chain defaults that are on unless a project turns them off, and a project keeps them on: `minimumReleaseAge` holds every install back behind a 24-hour window (the time the community needs to catch and yank a compromised release), `blockExoticSubdeps` refuses git/tarball/exotic transitive specs, `strictDepBuilds` fails an install when an un-acknowledged dependency wants to run a build script, and `allowBuilds` is a reviewed allow-list of the few packages whose build scripts are actually needed (chapter 081, lesson 8 — the pre-install defense; the threat model is typosquats and maintainer-compromise vectors like Shai-Hulud, where the attacker's window is the hours between publishing a poisoned version and the registry pulling it).
## Location
`pnpm-workspace.yaml` at the repo root — the load-bearing evidence, found by reading the file with **no install step**:
- Lines 13–15: the three defaults are explicitly disabled, overriding pnpm 11's safe shipped values: - `minimumReleaseAge: 0` — no pre-install window; an install takes a release the moment it ships. - `blockExoticSubdeps: false` — exotic transitive specs (git/tarball) are allowed. - `strictDepBuilds: false` — an un-acknowledged build script does not fail the install.- Lines 19–29: `onlyBuiltDependencies` + the `allowBuilds` map are present and correct (`sharp: true`, the rest acknowledged-but-skipped), so the build allow-list is **not** the gap — the gap is the three flags above. (This is why `next build` still passes; the relaxed flags do not break it, which is exactly what makes the defect ship green.)4 collapsed lines
`.npmrc` holds only `engine-strict=true` and `auto-install-peers=true` — registry/auth-shaped config. It is recorded here as the **not-where-supply-chain-settings-live** evidence: a reviewer who greps `.npmrc` for `minimumReleaseAge` finds nothing and might conclude the controls are fine; pnpm 11 reads these settings from `pnpm-workspace.yaml`, never `.npmrc`, so the audit reads the workspace file.
`package.json` line 5: `packageManager` is pinned (`pnpm@11.3.0`) — recorded as **present and healthy**, so this part of the checklist passes and is not a finding. CI's `--frozen-lockfile` flag is a forward thread (chapter 097, lesson 3): there is no CI gate in this repo yet, so the lockfile-enforcement and the audit gate are named as the follow-up, not scored as a gap here.
How it surfaced — the read is the discovery (no install needed), and `pnpm audit --prod` is the corroborating secondary signal:
```# 1. The load-bearing, deterministic check — read the workspace file, no install.rg -n 'minimumReleaseAge|blockExoticSubdeps|strictDepBuilds|allowBuilds' pnpm-workspace.yaml# Confirm settings do NOT live in .npmrc (the common misread).rg -n 'minimumReleaseAge|blockExoticSubdeps|strictDepBuilds' .npmrc # -> zero hits# 2. The corroborating post-install signal — read the output, do not treat it as the defense.pnpm audit --prod3 collapsed lines
```
Grep 1 returns the three disabled flags. `pnpm audit --prod` corroborates that real advisory-bearing versions are already in the tree: 10 vulnerabilities (5 high, 3 moderate, 2 low). The high-severity hits are command-injection advisories in `systeminformation` pulled transitively through `@trigger.dev/sdk > @trigger.dev/core > @opentelemetry/host-metrics`, plus the esbuild RCE/dev-server advisories reached through `better-auth > drizzle-kit`. These are the **outdated/advisory pins** the finding names — they are transitive, so the audit output is how you see them, and they prove the point: with `minimumReleaseAge: 0` there was no window to catch any of them before install.
3 collapsed lines
## Consequence
A malicious release lands the day it ships and this project installs it the same day, with no defense in the way. With `minimumReleaseAge: 0` there is no 24-hour window — the moment an attacker publishes a poisoned version of a dependency or a transitive (the Shai-Hulud pattern: compromise a maintainer, publish, the worm spreads through every project that installs before the registry yanks it), the next `pnpm install` here — on a developer's laptop or in CI — pulls it and runs whatever it carries. `strictDepBuilds: false` means an un-acknowledged dependency's build script runs without the install failing to flag it, so a poisoned `postinstall` executes silently; `blockExoticSubdeps: false` means a transitive can point at an attacker-controlled git/tarball spec and it is accepted. The audit output is not a substitute for any of this: `pnpm audit` is a *post-install* signal — it tells you a known-bad version is already in your tree — and Dependabot/Renovate raise PRs *after* a compromised release has landed in the registry. Neither is a pre-install defense; `minimumReleaseAge` is the only one of the three that stops the bad version from being installed in the first place, and it is the one turned off.
## Fix
Keep pnpm 11's supply-chain defaults **on** in `pnpm-workspace.yaml` — restore the three flags to their safe values and treat `allowBuilds` as the reviewed allow-list it is. This is a config change, not a version-bump chore: the load-bearing fix is the flags, and bumping the advisory-bearing deps is the follow-on cleanup the restored window then protects.
```yaml# pnpm-workspace.yaml — the three defaults back on.minimumReleaseAge: 1440 # 24h pre-install window — the only pre-install defenseblockExoticSubdeps: true # refuse git/tarball/exotic transitive specsstrictDepBuilds: true # fail install on an un-acknowledged build scriptallowBuilds: # reviewed allow-list — only what truly needs a build step sharp: true esbuild: false```
1. **Set `minimumReleaseAge: 1440`, `blockExoticSubdeps: true`, `strictDepBuilds: true`** so installs sit behind the 24-hour window, exotic transitive specs are refused, and an un-acknowledged build script fails the install instead of running silently.2. **Keep `allowBuilds` as the reviewed allow-list** (`sharp: true`, everything else acknowledged-but-skipped) — `strictDepBuilds: true` is only safe because the few packages that genuinely need a build step are named here; review the list rather than blanket-allowing.3. **Bump the advisory-bearing deps** the `pnpm audit --prod` output names — the transitive `systeminformation` (via `@trigger.dev/sdk`) and `esbuild` (via `better-auth > drizzle-kit`) high-severity advisories — pulling forward to patched ranges, or pinning a patched version through `overrides` where the direct dependency lags. This is the post-install cleanup; the restored `minimumReleaseAge` is what keeps the *next* bad release out.4. **Gate `pnpm audit` and `pnpm install --frozen-lockfile` in CI** — the forward thread (chapter 081, lesson 8 names the controls; chapter 097, lesson 3 wires the CI gate), so the audit signal and the lockfile enforcement run on every push rather than depending on a developer remembering to look.A few decisions worth pausing on.
The read is the finding; the audit only corroborates. This is the single distinction the whole category turns on, so it earns its own paragraph in the writeup. The rg on pnpm-workspace.yaml is deterministic, needs no install, and surfaces the actual defect — a config that disabled the defense. pnpm audit --prod is the secondary signal: it shows you bad versions already in the tree, which is the symptom of the absent window, not the gap itself. Lead with the read. An audit that opens with “10 vulnerabilities” and treats the version numbers as the problem has quietly downgraded a supply-chain finding into a dependency-update ticket — and the next bad release sails straight through, because nobody restored the window that would have caught it.
The build allow-list staying correct is exactly what lets the defect ship green. strictDepBuilds: false removes a guardrail; it does not break anything. With onlyBuiltDependencies and allowBuilds intact, next build and pnpm verify pass with all three flags off. That is why this finding is invisible to CI and visible only to a reading audit — and it is why the finding records the allow-list as healthy and not the gap, so a reader does not go hunting for a build failure that will never come.
The fix is a config change, not a version bump — and the order matters. Restoring minimumReleaseAge protects the next release: it is forward-looking. Bumping the advisory-bearing deps pnpm audit flagged is cleanup of versions already in the tree: it is backward-looking. Both belong in the fix, but the flags are load-bearing and the bumps are follow-on, because a window you restore today is what guards every install after it. The CI gate that enforces all of this on every push is named, not built — that is the signal-checks lesson in chapter 97’s job.
The healthy pieces are recorded as healthy, on purpose. The packageManager pin in package.json (pnpm@11.3.0) and the allowBuilds allow-list both pass — and the finding says so explicitly. This is not padding either. A launch review wants to know what you checked, not only what you found; naming the two controls that are correct is how a reader trusts that you ran the full dep-hygiene pass rather than stopping at the first red flag. The severity sits at high, not critical: a compromised transitive could own the whole runtime, which is severe, but unlike the live secret leak in finding 5 it is not actively exposing data today — it is an open door, not a fire — so it ranks just below the critical findings, and the two-line justification says exactly that.
The exact reference for the three flags your finding names — including that onlyBuiltDependencies is replaced by allowBuilds in v11.
pnpm's own threat-model writeup: why the 24-hour minimumReleaseAge window is a pre-install defense, not a post-install signal.
Palo Alto's analysis of the maintainer-compromise worm whose publish-to-yank window this finding's defense is built to close.
Moment of truth
Section titled “Moment of truth”Run this lesson’s gate:
pnpm test:lesson 8The suite reads findings/007-dep-hygiene.md off disk and checks the observable shape of your finding — that all four sections carry real content (a leftover TODO comment does not count), that the Rule names the pnpm supply-chain defaults and cites chapter 81 lesson 8, that the Location names all three disabled flags, names pnpm-workspace.yaml as where they live, and records both an rg/grep discovery command and the pnpm audit corroboration. It also probes the audit target itself: minimumReleaseAge: 0, blockExoticSubdeps: false, and strictDepBuilds: false must all still be present in pnpm-workspace.yaml. The target is read-only, so a passing gate proves you documented the gap rather than restored the flags. A green run looks like this:
$ node scripts/test-lesson.mjs 8
RUN v4.1.8 …/projects/Chapter 082/solution
✓ tests/lessons/Lesson 8.test.ts (14 tests) 8ms
Test Files 1 passed (1) Tests 14 passed (14)The test asserts the file’s shape; it cannot read prose for quality. Confirm these by hand:
pnpm audit and Dependabot named as post-install signals, minimumReleaseAge as the pre-install window — not just “the deps are outdated.”pnpm-workspace.yaml state and the pnpm audit --prod corroboration, with the read named as the load-bearing evidence and the audit as the backing signal.pnpm-workspace.yaml flags as the load-bearing change, not just a version bump — and names the dep bumps as the follow-on cleanup.allowBuilds allow-list and the packageManager pin as present-and-not-the-gap, so the full dep-hygiene pass is visible.