pnpm 11 holds a new release for 24 hours, and the nine waivers in our workspace file were written by pnpm, not by me
Munchable is a gut-health app: you scan a packaged food and it tells you whether that product fits the conditions you manage, with the reasons under the verdict. The conditions it reasons about are listed at munchable.ap
Munchable is a gut-health app: you scan a packaged food and it tells you whether that product fits the conditions you manage, with the reasons under the verdict. The conditions it reasons about are listed at munchable.app/conditions, and the recipe library it ships with is at munchable.app/recipes. The app is React Native through Expo, the marketing site is Next.js, and both live in one pnpm workspace.
That workspace file is about thirty lines long, and it contains three completely different mechanisms for holding a dependency still. This post is about the one I did not write.
A policy with waivers and no policy
Here is the block, verbatim:
minimumReleaseAgeExclude:
- [email protected] || 57.0.19
- '@expo/[email protected]'
- [email protected]
- [email protected]
- [email protected]
- '@expo/[email protected]'
- '@expo/[email protected]'
- [email protected]
- [email protected]
Nine entries, ten versions, every one of them Expo. And the setting those entries are exclusions from appears nowhere in the repository:
$ pnpm config get minimumReleaseAge
undefined
pnpm config list in this workspace prints minimumReleaseAgeExclude with all nine lines and no minimumReleaseAge beside it. Which looks, at first glance, like nine waivers against a rule nobody turned on.
They are not. pnpm 11 ships supply-chain protection on by default: minimumReleaseAge defaults to 1440, so a version published less than a day ago is not resolved at all. The release notes put it plainly, newly published packages are not resolved for 24 hours, because that is the window in which a compromised publish gets noticed and unpublished. Not setting the value is how you get it.
So the nine lines are not exceptions to a policy I chose. They are exceptions to a policy I inherited, and the interesting part is who typed them.
Loose mode writes your config for you
When minimumReleaseAge keeps its built-in value, its companion setting minimumReleaseAgeStrict is false. pnpm calls that loose mode, and in loose mode an install that resolves an immature version does not fail and does not ask. It adds the version to minimumReleaseAgeExclude in pnpm-workspace.yaml, prints one info line saying how many entries it persisted, and carries on.
Set minimumReleaseAge explicitly, to any value including the same 1440, and strict mode auto-enables. Then the same install collects every immature pick, direct and transitive, and prompts once with the full list. Approving writes the same lines. Declining aborts before the lockfile, the manifests or node_modules are touched.
That is a genuinely odd incentive gradient. Writing down the policy you already have is what turns it from a log into a gate. Leave the file quiet and the quarantine documents itself after the fact; state the number you are already running and you get asked permission.
Why an Expo app trips it nine times at once
The timestamps are the whole story. From the registry:
[email protected] 2026-09-18T08:06:55Z
[email protected] 2026-09-18T08:07:12Z
[email protected] 2026-09-18T08:08:13Z
@expo/[email protected] 2026-09-18T08:09:10Z
[email protected] 2026-09-18T08:09:14Z
[email protected] 2026-09-18T08:09:25Z
[email protected] 2026-09-18T08:10:13Z
@expo/[email protected] 2026-09-18T08:13:10Z
@expo/[email protected] 2026-09-18T08:23:40Z
Nine of the ten versions landed inside a seventeen minute window. The commit that carries them is timestamped three hours and change after the first of those publishes.
This is what an SDK release looks like from underneath. Expo does not ship one package, it ships a coordinated set, and expo install moves the set together because the pieces are only tested against each other. A quarantine that is evaluated per package therefore trips once per package: one upgrade, nine violations, nine waivers, all of them for the same decision I made once.
Three of those nine are not dependencies of mine at all. @expo/cli, expo-asset and @expo/ui appear in no package.json in this repo; they arrived underneath something that does. The quarantine covers transitive picks, so loose mode waives packages I never named, in a file I am supposed to be able to read as a statement of intent.
And the worst part is that it is per version. [email protected] is waived. [email protected] is not, and will not be, until the next upgrade adds a tenth line. The list only grows, and every entry in it is dead the moment the package moves again.
The other two pins in the same file
The waiver list sits between two other mechanisms for stopping a dependency from moving, both of which I did write.
overrides:
'@react-native/metro-config': 0.86.3
'@types/react': ~19.2.18
'@types/react-dom': ~19.2.7
patchedDependencies:
[email protected]: patches/[email protected]
The overrides exist because of optional peers declared as *. A library that says it works with any version of @types/react gets the newest one, which then demands a newer @types/react than Expo's SDK pins, and the hoisted node_modules can only hold one. Same story for @react-native/metro-config, where one package wants * and the rest of the React Native toolchain wants the version matching react-native itself.
The patch is the tap-to-focus support expo-camera does not ship, which I wrote about separately. What matters here is the key: [email protected], an exact version. 221 lines, ten files, 103 added lines and nothing removed, across Kotlin, Swift, the TypeScript source and the compiled JavaScript that actually gets imported.
So one file now holds three pins with three different failure modes:
- The patch fails loudly. Bump
expo-cameraand the install stops, because the patch key no longer matches anything. That is the behaviour you want from a pin that holds a native method the product depends on. - The overrides succeed silently. They are satisfied on every install and say nothing, which is correct until the day Expo moves off that types line and the pin is the thing holding you back.
- The quarantine waives silently. It is the only one of the three that edits the repository on your behalf.
The commit that added eight of the nine waivers is also the commit that moved the patch from [email protected] to 57.0.5, patch file and all. One SDK upgrade, three mechanisms, and only one of them asked me anything.
What I am doing about it
Not setting minimumReleaseAge. The default is the right default, and strict mode's prompt would land in the middle of an expo install that I have already decided to run, on a set of packages published by the same organisation in the same minute. A prompt that always gets the same answer is not a control.
What I did change is how I read the list. It is not configuration, it is an audit trail: every line records a version this workspace installed on the day it was published, and the publish timestamps above are checkable against the registry by anybody. When a line names a package no package.json mentions, that is worth a minute of reading before the next install, because the thing a 24 hour quarantine is actually protecting against is a version that should never have been published at all.
The pruning is the easy part. Any entry whose version no longer appears in pnpm-lock.yaml is inert and can go, and a grep over the lockfile answers that in one pass.
See the thing the dependencies are for
The product the version pinning serves is a camera and a verdict. The scan flow is demonstrated on the landing page at munchable.app, and the same reasoning that the app runs on a barcode is served as static pages: munchable.app/answers has one page per ingredient and condition, and munchable.app/hidden-gluten-ingredients is the reference list version of it.
If you run an Expo app on pnpm 11, it is worth checking whether your own workspace file has grown a waiver list. pnpm config get minimumReleaseAge printing undefined does not mean the quarantine is off. It means your package manager has been keeping notes.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.