<rss version="2.0">
  <channel>
    <title>Pnpm on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/pnpm/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sun, 06 Sep 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>I Gave Install-Script Permission to a Package I Don&#39;t Have</title>
      <link>https://llbbl.blog/2026/09/06/i-gave-installscript-permission-to.html</link>
      <pubDate>Sun, 06 Sep 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/09/06/i-gave-installscript-permission-to.html</guid>
      <description>&lt;p&gt;There&amp;rsquo;s exactly one file in one of my wiki projects that&amp;rsquo;s a security control rather than a config file, and I hadn&amp;rsquo;t read it in months.&lt;/p&gt;
&lt;p&gt;pnpm 11 removed &lt;code&gt;onlyBuiltDependencies&lt;/code&gt; along with four related settings and replaced all of them with a single &lt;code&gt;allowBuilds&lt;/code&gt; map, described in the docs as &amp;ldquo;a map of package matchers to explicitly allow (&lt;code&gt;true&lt;/code&gt;) or disallow (&lt;code&gt;false&lt;/code&gt;) script execution.&amp;rdquo; A postinstall script is arbitrary code running on your machine at install time with your permissions, so the list of packages allowed to have one is the shortest and highest-consequence list in the repo.&lt;/p&gt;
&lt;p&gt;Mine has six entries. Two &lt;code&gt;true&lt;/code&gt;, four &lt;code&gt;false&lt;/code&gt;. Here&amp;rsquo;s the &lt;code&gt;true&lt;/code&gt; half:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;allowBuilds&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;sharp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# native libvips bindings (image processing)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;@xenova/transformers&amp;#34;: &lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# native ML runtime&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And here&amp;rsquo;s the count:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ grep -c &amp;#39;transformers&amp;#39; pnpm-lock.yaml
0
$ grep -rn &amp;#39;xenova&amp;#39; --include=&amp;#39;*.json&amp;#39; --include=&amp;#39;*.yaml&amp;#39; --include=&amp;#39;*.ts&amp;#39; . | grep -v node_modules
pnpm-workspace.yaml:28:  &amp;#34;@xenova/transformers&amp;#34;: true # native ML runtime
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Zero&lt;/strong&gt; references in the lockfile. &lt;strong&gt;One&lt;/strong&gt; reference in the entire repository, and it&amp;rsquo;s the line granting the permission.&lt;/p&gt;
&lt;p&gt;This project used to run embeddings locally. That approach is gone, search goes through a hosted retrieval service now, and the project&amp;rsquo;s own direction notes say not to reintroduce local embeddings. The dependency left. The standing permission to execute native build scripts on my machine stayed behind, waiting for a package that&amp;rsquo;s never going to be installed.&lt;/p&gt;
&lt;h2 id=&#34;a-dead-grant-isnt-a-vulnerability-its-a-broken-instrument&#34;&gt;A dead grant isn&amp;rsquo;t a vulnerability, it&amp;rsquo;s a broken instrument&lt;/h2&gt;
&lt;p&gt;Nobody&amp;rsquo;s exploiting this. The package isn&amp;rsquo;t in the tree, so nothing runs. I want to be clear about that before anyone gets excited.&lt;/p&gt;
&lt;p&gt;What it costs me is the ability to trust the file. An allowlist works as a control only if every entry means &lt;em&gt;I read this package&amp;rsquo;s install script and I accept it&lt;/em&gt;. The moment one entry instead means &lt;em&gt;this was true in a previous version of the project&lt;/em&gt;, the list becomes a record of the past, and the next person to open it has no way to tell the two kinds of entry apart without re-deriving all six from the lockfile. The next person is me, in four months, at which point I will absolutely assume the file is current.&lt;/p&gt;
&lt;p&gt;This is the failure mode of every allowlist I&amp;rsquo;ve kept. They only grow. Removing an entry requires noticing that something left, and nothing tells you when a dependency stops existing.&lt;/p&gt;
&lt;h2 id=&#34;the-other-true-entry-has-the-opposite-problem&#34;&gt;The other &lt;code&gt;true&lt;/code&gt; entry has the opposite problem&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sharp&lt;/code&gt; is granted a native build. &lt;code&gt;sharp&lt;/code&gt; is also not in my &lt;code&gt;package.json&lt;/code&gt;. It shows up anyway:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ pnpm why sharp
sharp@0.35.3
└─┬ astro@7.2.4
  ├─┬ @astrojs/node@11.1.4
  │ └── &amp;lt;package&amp;gt; (dependencies)
  └── &amp;lt;package&amp;gt; (dependencies)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Astro declares it as an optional dependency. I wrote about that mechanic in a different repo a few days ago, so I&amp;rsquo;ll skip the re-explanation. What&amp;rsquo;s new to me here is that the same workspace file also pins it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;overrides&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;vite&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;^8.2.2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;sharp&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;^0.35.3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The comment sitting above that block warns that several of these pins are CVE remediations for transitive dependencies, and not to relax a bound without checking the advisory it was added for. That&amp;rsquo;s a good note. It&amp;rsquo;s also attached to a package I never asked for, whose version I control only because I reached into the resolver and overrode somebody else&amp;rsquo;s optional dependency.&lt;/p&gt;
&lt;h2 id=&#34;the-false-entries-turned-out-to-be-the-good-news&#34;&gt;The &lt;code&gt;false&lt;/code&gt; entries turned out to be the good news&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;@prisma/engines&amp;#34;: &lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;prisma&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;better-sqlite3&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;esbuild&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These four aren&amp;rsquo;t a new restriction, and it took me a minute to work out why they&amp;rsquo;re written down at all.&lt;/p&gt;
&lt;p&gt;Under pnpm 10, &lt;code&gt;onlyBuiltDependencies&lt;/code&gt; was an &lt;em&gt;exclusive&lt;/em&gt; allowlist. Anything not on it got skipped. All four of these were already being skipped, silently, for as long as this project has existed. pnpm 11&amp;rsquo;s &lt;code&gt;strictDepBuilds&lt;/code&gt; refuses to skip quietly, and exits non-zero when a dependency has an unreviewed build script.&lt;/p&gt;
&lt;p&gt;So the upgrade changed no behavior at all. It changed whether the behavior was &lt;em&gt;visible&lt;/em&gt;, and converted four invisible skips into four decisions I had to write down and sign. That&amp;rsquo;s the good version of a breaking change, and it&amp;rsquo;s the reason the dead &lt;code&gt;@xenova/transformers&lt;/code&gt; entry was sitting right there for me to trip over.&lt;/p&gt;
&lt;h2 id=&#34;one-more-thing-since-i-had-the-shell-open&#34;&gt;One more thing, since I had the shell open&lt;/h2&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ grep -c &amp;#39;0.34.5&amp;#39; pnpm-lock.yaml
0
$ du -sh node_modules/.pnpm/@img+sharp-libvips-darwin-arm64@1.2.4
 15M
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;sharp@0.34.5&lt;/code&gt; has zero references in the current lockfile and is still on disk, next to &lt;strong&gt;15M&lt;/strong&gt; of libvips binaries compiled for it, which in turn sit next to the &lt;strong&gt;17M&lt;/strong&gt; of libvips 1.3.2 that the current resolution actually uses. The override moved from &lt;code&gt;^0.34.4&lt;/code&gt; to &lt;code&gt;^0.35.3&lt;/code&gt; in a dependency sweep last week, and the old native payload never left. That&amp;rsquo;s &lt;code&gt;pnpm store prune&lt;/code&gt; territory rather than a correctness bug, but it&amp;rsquo;s 15M of compiled image-processing code on a machine whose project doesn&amp;rsquo;t declare an image-processing dependency.&lt;/p&gt;
&lt;h2 id=&#34;what-im-changing&#34;&gt;What I&amp;rsquo;m changing&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reconcile the allowlist against the lockfile in CI.&lt;/strong&gt; Every key in &lt;code&gt;allowBuilds&lt;/code&gt; should resolve to something in &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, or the build should complain. It&amp;rsquo;s maybe ten lines of test and I don&amp;rsquo;t have it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Say why, next to every entry.&lt;/strong&gt; Two of my six have a reason comment. The reason is worth more than the package name, because the reason is the thing that expires.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-read the file whenever an override moves.&lt;/strong&gt; The pins and the build grants describe the same dependency graph, and they drifted apart without a word.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The security value of an allowlist lives in the reviewing, not in the file. I&amp;rsquo;ve been maintaining the file.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://pnpm.io/settings/build&#34;&gt;pnpm build settings&lt;/a&gt; — &lt;code&gt;allowBuilds&lt;/code&gt; and &lt;code&gt;strictDepBuilds&lt;/code&gt;, plus the migration table from the five settings pnpm 11 removed&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pnpm.io/blog/releases/11.0&#34;&gt;pnpm 11.0 release notes&lt;/a&gt; — the removal of &lt;code&gt;onlyBuiltDependencies&lt;/code&gt; and friends&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pnpm.io/settings&#34;&gt;pnpm settings index&lt;/a&gt; — the full &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; surface&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>