Skip to content
Chapter 82Lesson 8

Finding 7: the dep-hygiene gap

Every finding so far has lived inside the running app, from a swallowed role check to an unthrottled endpoint. This one lives one layer down, in the files that decide what code is allowed onto the machine at all. Your goal 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 launch here: this finding is a deterministic read of two config files, not a running-app test. You open pnpm-workspace.yaml, read three lines, and you have found it. The work is 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 four-section template every finding in this pass uses, with the Category and Severity header on top:

findings/007-dep-hygiene.md
# Finding 007 β€” Supply-chain defaults disabled in pnpm-workspace.yaml
**Category:** Dependency hygiene (security baseline).
**Severity:** high β€” …
## Rule
## Location
## Consequence
## Fix

Inexperienced teams resist this finding with one sentence: β€œbut we just enabled Dependabot.” That sentence is why the finding exists, because it confuses two controls that act at different moments. Dependabot and Renovate raise pull requests, and pnpm audit reports known-bad versions, but both fire after a compromised release has landed in the registry and likely in your tree. They are post-install signals. The flag this target turned off, minimumReleaseAge, is the only pre-install control of the three: it holds every install behind a 24-hour window, roughly the time the community needs to catch and yank a poisoned release before your machine pulls it. The real threat is typosquats and maintainer-compromise worms like Shai-Hulud, where the attacker’s window is the few hours between publishing the bad version and the registry pulling it. A team with Dependabot green and minimumReleaseAge: 0 is fully exposed in exactly that window.

The audit is a read: no install, no pnpm dev, no clicking. You rg the workspace file and confirm the three flags are off β€” minimumReleaseAge, blockExoticSubdeps, strictDepBuilds β€” then confirm two pieces that turn out healthy, so you do not waste a reviewer’s time on them: allowBuilds is a reviewed allow-list rather than a blanket β€œrun every build script,” and packageManager in package.json pins an exact pnpm version. You also rg the .npmrc, and your writeup should pre-empt the common misread: pnpm 11 reads these supply-chain settings from pnpm-workspace.yaml, never from .npmrc. A reviewer who greps .npmrc, finds nothing, and concludes the controls are fine has looked in the wrong file. Finally, pnpm audit --prod corroborates the point: advisory-bearing versions are already in the tree, pulled in transitively, exactly what a 24-hour window would have given the community a chance to flag.

Two things are out of scope. You do not patch the target: the three flags stay off, because the deliverable is a documented finding 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 belongs to the signal-checks lesson in chapter 97; name it as the fix’s forward thread. For the rule itself β€” the pnpm 11+ defaults, the threat model, the allow-list mechanics β€” lean on the supply-chain lesson in chapter 81 rather than re-deriving 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).
tested
The Location records all three disabled flags β€” 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.
tested
The finding records the healthy-and-not-the-gap pieces: the allowBuilds / onlyBuiltDependencies allow-list and the packageManager pin in package.json.
untested
The Consequence reads as a malicious release installing the day it ships with no window, and explicitly distinguishes pnpm audit and Dependabot as post-install signals from minimumReleaseAge as the pre-install defense β€” no β€œcould potentially” hedging.
untested
The Fix names keeping the three flags on (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.
untested
A severity is assigned and justified in two lines.
untested
The audit target still runs unchanged β€” the three flags are still disabled, proving you documented the gap rather than patched it.
tested

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. Do this before you open the walkthrough β€” the one rep this finding teaches is telling a pre-install defense apart from a post-install signal in your own words, and reading the worked finding first skips it.

Reference solution and walkthrough

Here is the file the audit lands on. Three top-level keys override pnpm 11’s safe defaults; the comment flagging them is the seed, which a real target would not have.

pnpm-workspace.yaml
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: 0
blockExoticSubdeps: false
strictDepBuilds: 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:
- sharp
allowBuilds:
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: false
overrides:
kysely: 0.28.17

The three flags at the top are the gap. Everything below them calibrates your eye not to over-report: onlyBuiltDependencies and allowBuilds are a reviewed allow-list, and it is correct. That correct allow-list is what lets the defect ship green β€” with it intact, next build and pnpm verify pass even with strictDepBuilds: false sitting right there. Relaxing strictDepBuilds removes a guardrail; it does not break the build, which is exactly why a reading audit catches this and a CI run does not.

The second file to read is .npmrc, and the point is what it does not contain.

.npmrc
engine-strict=true
auto-install-peers=true

That is the whole file: two settings, neither a supply-chain control. pnpm 11 reads minimumReleaseAge, blockExoticSubdeps, and strictDepBuilds from pnpm-workspace.yaml, never from .npmrc. Grepping .npmrc for them returns zero hits, and zero hits in the wrong file is not evidence the controls are healthy β€” so the finding records .npmrc as the not-where-the-settings-live file, to stop a later reader repeating the mistake.

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.

the discovery β€” two reads, one corroboration
# 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 --prod

The first grep returns the three disabled flags with no install; the second returns nothing, proving .npmrc is the wrong place to look. pnpm audit --prod then corroborates that advisory-bearing versions are already in the tree β€” high-severity command-injection advisories in systeminformation through @trigger.dev/sdk, and esbuild dev-server advisories through better-auth > drizzle-kit. Both are transitive, so the audit is the only way you would see them. They are not the finding; they are the symptom β€” bad versions the absent window let through. Lead with the read, the config that turns the defense off. Lead with the audit count instead and you have written a version-bump ticket, not a supply-chain finding.

This is the completed findings/007-dep-hygiene.md as it lands in the repo. Read it top to bottom; the sections build on each other.

findings/007-dep-hygiene.md
# 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.
## Rule
2 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 --prod
3 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 defense
blockExoticSubdeps: true # refuse git/tarball/exotic transitive specs
strictDepBuilds: true # fail install on an un-acknowledged build script
allowBuilds: # 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 distinction the whole category turns on. 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, showing bad versions already in the tree: the symptom of the absent window, not the gap. Lead with the read. An audit that opens with β€œ10 vulnerabilities” has quietly downgraded a supply-chain finding to a dependency-update ticket, and the next bad release sails straight through, because nobody restored the window that would have caught it.

The fix is a config change, not a version bump β€” and order matters. Restoring minimumReleaseAge is forward-looking: it protects the next release. Bumping the advisory-bearing deps is backward-looking cleanup of versions already in the tree. Both belong in the fix, but the flags are load-bearing and the bumps are follow-on, because the window you restore today guards every install after it. The CI gate that enforces 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 (pnpm@11.3.0) and the allowBuilds allow-list both pass, and the finding says so. A launch review wants to know what you checked, not only what you found; naming the controls that are correct is how a reader trusts you ran the full pass rather than stopping at the first red flag. Severity sits at high, not critical: a compromised transitive could own the whole runtime, but unlike the live secret leak in finding 5 it is not exposing data today β€” an open door, not a fire β€” so it ranks just below the critical findings, and the justification says exactly that.

Run this lesson’s gate:

Terminal window
pnpm test:lesson 8

The suite reads findings/007-dep-hygiene.md off disk and checks its shape: all four sections carry real content (a leftover TODO does not count), the Rule cites chapter 81 lesson 8, the Location names the three disabled flags and pnpm-workspace.yaml, and both the discovery command and the pnpm audit corroboration are recorded. It then probes the target, confirming minimumReleaseAge: 0, blockExoticSubdeps: false, and strictDepBuilds: false are all still present. 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:

Terminal window
$ 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 tests assert the file’s shape; they cannot read prose for quality. Confirm these by hand:

The Consequence distinguishes pre-install from post-install defenses β€” pnpm audit and Dependabot named as post-install signals, minimumReleaseAge as the pre-install window β€” not just β€œthe deps are outdated.”
untested
The Location records the 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.
untested
The Fix names the three pnpm-workspace.yaml flags as the load-bearing change, not just a version bump β€” and names the dep bumps as the follow-on cleanup.
untested
The finding records the healthy allowBuilds allow-list and the packageManager pin as present-and-not-the-gap, so the full dep-hygiene pass is visible.
untested
The severity justification holds up read aloud: two lines a launch reviewer who has not seen the code would accept.
untested