<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://nesbitt.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://nesbitt.io/" rel="alternate" type="text/html" /><updated>2026-07-20T05:45:05+00:00</updated><id>https://nesbitt.io/feed.xml</id><title type="html">Andrew Nesbitt</title><subtitle>Package management and open source metadata expert. Building Ecosyste.ms, open datasets and tools for critical open source infrastructure.</subtitle><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><entry><title type="html">This Week in Package Management: 18 July 2026</title><link href="https://nesbitt.io/2026/07/18/this-week-in-package-management.html" rel="alternate" type="text/html" title="This Week in Package Management: 18 July 2026" /><published>2026-07-18T10:00:00+00:00</published><updated>2026-07-18T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/18/this-week-in-package-management</id><content type="html" xml:base="https://nesbitt.io/2026/07/18/this-week-in-package-management.html"><![CDATA[<p>Week nine of the roundup, built from the <a href="https://github.com/ecosyste-ms/package-managers-opml">package manager OPML feed collection</a> and whatever I’ve posted or boosted on <a href="https://mastodon.social/@andrewnez">Mastodon</a>.</p>

<h2 id="releases">Releases</h2>

<p><a href="https://github.com/pnpm/pnpm/releases/tag/v11.13.0">pnpm 11.13</a> adds <code class="language-plaintext highlighter-rouge">pnpm change</code>, a native changesets-compatible release planner that handles version bumping, dependent propagation, fixed groups and per-package release lanes. It also adds <code class="language-plaintext highlighter-rouge">pnpm team</code> for registry organisation and team membership, and a <code class="language-plaintext highlighter-rouge">versioning.epics</code> config that ties a group of packages to a lead package so their versions stay within a band derived from the lead’s major. <a href="https://github.com/pnpm/pnpm/releases/tag/v11.12.0">11.12</a> preceded it, letting a pnpmfile fetcher return <code class="language-plaintext highlighter-rouge">{ delegate: &lt;resolution&gt; }</code> so custom fetchers can hand off to the built-in ones portably across pnpm and pacquet.</p>

<p><a href="https://github.com/denoland/deno/releases/tag/v2.9.3">Deno 2.9.3</a> adds <code class="language-plaintext highlighter-rouge">deno add --no-save</code> and <code class="language-plaintext highlighter-rouge">--save-optional</code>, and <code class="language-plaintext highlighter-rouge">--min-dep-age</code> as a shorter alias for the minimum-release-age check.</p>

<p><a href="https://github.com/prefix-dev/pixi/releases/tag/v0.73.0">pixi 0.73.0</a> lets <code class="language-plaintext highlighter-rouge">{ workspace = true }</code> work in environment <code class="language-plaintext highlighter-rouge">[dependencies]</code> tables, so a version shared across features or targets is declared once in <code class="language-plaintext highlighter-rouge">[workspace.dependencies]</code> without needing the <code class="language-plaintext highlighter-rouge">pixi-build</code> preview.</p>

<p><a href="https://github.com/astral-sh/uv/releases/tag/0.11.29">uv 0.11.29</a> adds JSON output to <code class="language-plaintext highlighter-rouge">uv tree</code> and prefers local artifacts over URLs when installing from <code class="language-plaintext highlighter-rouge">pylock.toml</code>.</p>

<p><a href="https://github.com/verdaccio/verdaccio/releases/tag/v6.8.0">Verdaccio 6.8.0</a> fires the notification webhook on unpublish and single-version removal as well as publish. Templates get a <code class="language-plaintext highlighter-rouge">variable, and the</code> object exposes only <code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">groups</code> and <code class="language-plaintext highlighter-rouge">real_groups</code> so the auth token can never reach the notification endpoint.</p>

<p><a href="https://docs.zizmor.sh/release-notes/#1270">zizmor 1.27</a> adds experimental support for auditing GitHub’s new parallel steps pattern.</p>

<p><a href="https://blog.rust-lang.org/2026/07/16/Rust-1.97.1/">Rust 1.97.1</a> is a point release backporting an LLVM fix and disabling the IR change in 1.97.0 that made the miscompilation more likely to trigger.</p>

<p><a href="https://github.com/Homebrew/brew/releases/tag/6.0.11">Homebrew 6.0.11</a> merges <code class="language-plaintext highlighter-rouge">brew vulns</code>, so CVE scanning of installed formulae is now built in; I wrote up <a href="/2026/07/17/plumbing-homebrew-into-the-vulnerability-ecosystem">the work behind it</a>.</p>

<p>Also out: <a href="https://github.com/npm/cli/releases/tag/v12.0.1">npm 12.0.1</a>, <a href="https://github.com/gomods/athens/releases/tag/v0.18.1">Athens 0.18.1</a>, <a href="https://github.com/microsoft/vcpkg-tool/releases/tag/2026-07-13">vcpkg 2026-07-13</a>, <a href="https://github.com/sbt/sbt/releases/tag/v2.0.2">sbt 2.0.2</a>, <a href="https://github.com/NixOS/nix/releases/tag/2.35.1">Nix 2.35.1</a>, <a href="https://github.com/jdx/mise/releases/tag/v2026.7.10">mise 2026.7.10</a>, <a href="https://github.com/pypa/pipx/releases/tag/1.16.0">pipx 1.16.0</a>, <a href="https://github.com/gradle/gradle/releases/tag/v9.7.0-RC1">Gradle 9.7.0-RC1</a>, <a href="https://github.com/renovatebot/renovate/releases/tag/43.268.4">Renovate 43.268.4</a>, <a href="https://github.com/dependabot/dependabot-core/releases/tag/v0.387.0">Dependabot Core 0.387.0</a>.</p>

<h2 id="security">Security</h2>

<p><a href="https://github.com/moby/moby/releases/tag/docker-v29.6.2">Docker Engine 29.6.2</a> fixes three CVEs: <a href="https://github.com/advisories/GHSA-hw3h-2gp9-cxpv">CVE-2026-15793</a>, where a git source checkout from a bundle file could lead to command injection; <a href="https://github.com/advisories/GHSA-qx3x-mv6r-52p6">CVE-2026-15792</a>, where incorrect parameters from a BuildKit frontend could cause a panic; and <a href="https://github.com/advisories/GHSA-32pv-7hq5-qhwj">CVE-2026-15791</a>, where an LLB file operation could be tricked into removing the contents of <code class="language-plaintext highlighter-rouge">/tmp</code>.</p>

<p><a href="https://github.com/sbt/sbt/releases/tag/v1.12.14">sbt 1.12.14</a> backports the fix for <a href="https://github.com/advisories/GHSA-j482-hm6j-v5jj">CVE-2026-26032</a> in the bundled Apache Ivy <code class="language-plaintext highlighter-rouge">PackagerResolver</code>.</p>

<h2 id="articles">Articles</h2>

<p><a href="https://blog.rust-lang.org/2026/07/13/crates-io-development-update/">crates.io: development update</a> (Tobias Bieniek, Rust Blog) covers six months of work: crate pages now have a Code tab that shows the exact files <code class="language-plaintext highlighter-rouge">cargo</code> downloads, which can differ from the linked repository; crates.io accounts are being untangled from GitHub logins; and the Svelte frontend migration is complete.</p>

<p><a href="https://phpunit.expert/articles/composer-and-packagist-under-supply-chain-stress.html">Composer and Packagist Under Supply-Chain Stress</a> (Sebastian Bergmann) reviews how the PHP ecosystem held up through 2025 and 2026, what it can borrow from other registries, and who owns the infrastructure.</p>

<p><a href="https://rustfoundation.org/media/my-first-month-as-ai-security-engineer-in-residence-at-the-rust-foundation/">My First Month as AI Security Engineer in Residence</a> (Jacob Finkelman, Rust Foundation) covers building a prioritised database of crates to scan, running early passes with Scrutineer, and working out when a bug found this way should be embargoed rather than reported openly.</p>

<h2 id="papers">Papers</h2>

<p><a href="https://arxiv.org/abs/2607.13021">Software Supply Chains are Dead: Use-Case-Oriented Regeneration</a> (arXiv) argues that supply-chain attacks have raised the cost of external dependencies while generative AI has lowered the cost of local implementation, and evaluates an agent workflow that synthesises only the slice of a dependency an application actually calls.</p>

<p><a href="https://arxiv.org/abs/2607.15143">Setup Complete, Now You Are Compromised: Weaponizing Setup Instructions Against AI Coding Agents</a> (Bagmar et al., arXiv) evaluates package-install-time supply-chain attacks delivered through project setup docs against production coding-agent harnesses: editing only a README, requirements file or Makefile redirects the agent to an untrusted registry, a known-vulnerable version, or a wrong-but-plausible package name.</p>

<p><a href="https://arxiv.org/abs/2607.14900">The Distributed Open-Source Vulnerability Ecosystem</a> (Mandl et al., arXiv) models vulnerability management as a distributed process and traces where scanners diverge on identical software inventories to specific stages of the ecosystem rather than any one tool or data source.</p>

<h2 id="elsewhere">Elsewhere</h2>

<p><a href="https://forgejo.org/2026-07-release-v16-0/">Forgejo 16.0</a> adds per-repository watch options, an Authorized Integrations mechanism for secret-less API access, and review comments that span multiple lines.</p>

<p><a href="https://www.ruby-lang.org/en/news/2026/07/14/ruby-4-0-6-released/">Ruby 4.0.6</a> is a routine bugfix release.</p>

<p><a href="https://opensourcesecurity.io/2026/2026-07-lightwell-mo-duffy/">Open Source Security: Project Lightwell</a> (Josh Bressers) is a podcast conversation with Máirín Duffy on Red Hat’s programme for routing AI-discovered vulnerabilities to upstream projects rather than carrying downstream patches.</p>

<p>Send links for next week to <a href="https://mastodon.social/@andrewnez">@andrewnez@mastodon.social</a>.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="weekly" /><summary type="html"><![CDATA[Releases, advisories, and articles from across the package management world]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Plumbing Homebrew into the vulnerability ecosystem</title><link href="https://nesbitt.io/2026/07/17/plumbing-homebrew-into-the-vulnerability-ecosystem.html" rel="alternate" type="text/html" title="Plumbing Homebrew into the vulnerability ecosystem" /><published>2026-07-17T10:00:00+00:00</published><updated>2026-07-17T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/17/plumbing-homebrew-into-the-vulnerability-ecosystem</id><content type="html" xml:base="https://nesbitt.io/2026/07/17/plumbing-homebrew-into-the-vulnerability-ecosystem.html"><![CDATA[<p>Back in January I <a href="/2026/01/08/brew-vulns-cve-scanning-for-homebrew">released brew-vulns</a>, a gem that added CVE scanning to Homebrew. It shelled out to <code class="language-plaintext highlighter-rouge">brew info --json=v2</code>, derived a source repository URL and version tag for each installed formula, and queried <a href="https://osv.dev">osv.dev</a> for anything known against that repo at that version. As of Homebrew 6.0.11 that command is built in and the gem is deprecated. Getting useful answers out of <code class="language-plaintext highlighter-rouge">brew vulns</code> in between meant changing what data Homebrew publishes about its own packages, registering Homebrew with the specs that vulnerability tooling is built on, standing up an advisory database, and porting Homebrew’s version comparison to Python so osv.dev can order our releases.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ brew vulns
Checking 142 packages for vulnerabilities...
(18 packages skipped - no supported source URL)

openssl@3 (3.4.1)
  CVE-2024-13176 (medium) - Timing side-channel in ECDSA signature...
    Fixed in: 3.4.2

Found 1 vulnerability in 1 package

10 resolved by formula patches (not counted; pass --no-ignore-patches to include):
  glibc: CVE-2024-2961, CVE-2024-33599, CVE-2024-33600, CVE-2025-0395, ...
</code></pre></div></div>

<p>About a month after the first release Mike McQuaid invited me to transfer the repo into the Homebrew org, and from then on it was being run against all of homebrew-core rather than just my laptop. That’s roughly 8,400 formulae, enough real data to find edge cases fixtures never do. OSV records with malformed <code class="language-plaintext highlighter-rouge">affected</code> ranges were passing versions through silently, so <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/62">range evaluation became fail-closed</a>. Vulnerability summaries occasionally contain terminal escape sequences and needed sanitising before printing. One pathological query could pull unbounded pages from the OSV API, so both pagination and the thread pool that fetches vulnerability detail got hard caps, and CI users needed a <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/79">distinct exit code</a> for “the scan errored” as opposed to “the scan found something”.</p>

<h3 id="formula-patches">Formula patches</h3>

<p>Hundreds of formulae in homebrew-core ship with at least one patch applied on top of the upstream tarball, and some of those patches fix CVEs, so the bottle a user installs is not affected even when the upstream version number falls inside an advisory’s range. A query keyed on repo and version reports those as open regardless. There was also no programmatic way to find out which formulae carried patches at all: <code class="language-plaintext highlighter-rouge">brew info --json</code> didn’t serialise the patch list, formulae.brew.sh didn’t show it, and formulae loaded from the API had an empty one, leaving a grep over a full homebrew-core checkout as the only option.</p>

<p>I opened a <a href="https://github.com/orgs/Homebrew/discussions/6869">discussion</a> proposing two changes to Homebrew itself: add patches to the JSON output, and let a patch declare which CVEs it resolves. Mike said yes to both more or less immediately. Patches <a href="https://github.com/Homebrew/brew/pull/22459">went into <code class="language-plaintext highlighter-rouge">Formula#to_hash</code></a> the same day, which surfaced 1,191 of them across 809 formulae, 972 with an external URL and 219 embedded in the formula file as <code class="language-plaintext highlighter-rouge">__END__</code> data. The <code class="language-plaintext highlighter-rouge">patch do</code> DSL then <a href="https://github.com/Homebrew/brew/pull/22466">gained <code class="language-plaintext highlighter-rouge">resolves</code> and <code class="language-plaintext highlighter-rouge">type</code></a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">patch</span> <span class="k">do</span>
  <span class="n">url</span> <span class="s2">"https://deb.debian.org/.../libquicktime_1.2.4-12.debian.tar.xz"</span>
  <span class="n">sha256</span> <span class="s2">"..."</span>
  <span class="n">type</span> <span class="ss">:backport</span>
  <span class="n">resolves</span> <span class="s2">"CVE-2016-2399"</span><span class="p">,</span> <span class="s2">"CVE-2017-9122"</span>
  <span class="n">apply</span> <span class="s2">"patches/CVE-2016-2399.patch"</span><span class="p">,</span> <span class="s2">"patches/CVE-2017-9122_et_al.patch"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The vocabulary matches CycloneDX’s <a href="https://cyclonedx.org/docs/1.6/json/#components_items_pedigree_patches"><code class="language-plaintext highlighter-rouge">pedigree.patches</code></a> so SBOM tooling can read it without translation, and CVE ids are auto-inferred from the patch URL and <code class="language-plaintext highlighter-rouge">apply</code> filenames, which meant glibc, unzip, zip and libquicktime populated without any formula edits.</p>

<p>That change sat in review for close to a month and shipped in Homebrew 6.0.4, at which point brew-vulns could <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/92">consume it</a>: any OSV result whose id or alias matches a <code class="language-plaintext highlighter-rouge">resolves</code> entry is reported as patched, excluded from the exit code, and marked <code class="language-plaintext highlighter-rouge">analysis.state: resolved</code> in CycloneDX output. Bo Anderson later <a href="https://github.com/Homebrew/brew/pull/23114">mapped <code class="language-plaintext highlighter-rouge">resolves</code> through the API layer</a> so it survives the JSON round-trip when formulae are loaded without a local homebrew-core checkout.</p>

<h3 id="annotation-backfill">Annotation backfill</h3>

<p>With the DSL merged, the <a href="https://github.com/Homebrew/homebrew-brew-vulns/issues/95">next job</a> was populating it across those 1,191 existing patches. Of the 972 external ones, 497 point at GitHub commit URLs (in principle queryable in OSV by commit hash), 272 point at raw files hosted in homebrew-core itself, 42 come from Debian, and the rest scatter across MacPorts, FreeBSD ports, Fedora, Gentoo and gists. <a href="https://github.com/p-linnane">Patrick Linnane</a> sampled 59 of the GitHub-commit patches and found zero that referenced a CVE; homebrew-core’s patches are almost entirely build and portability fixes, and a heuristic that greps formula source for CVE strings risks marking things patched that aren’t, since several of the CVE mentions in core are comments about <em>un</em>fixed issues.</p>

<p>So the approach flipped from annotating every patch by URL heuristic to running the scanner over every patched formula and only annotating where a real finding gets suppressed. That pass covered the 484 patched formulae with a resolvable source repo and turned up nine with open OSV findings; in every case the shipped patch was a build fix unrelated to the CVE, so zero new annotations came out of it.</p>

<p>A wider sweep querying OSV by commit hash for the GitHub-commit patches, reading DEP-3 headers on the Debian tarballs, and grepping local patch bodies found exactly one more: <code class="language-plaintext highlighter-rouge">nvi</code> carries a Debian regex patch that fixes <a href="https://github.com/Homebrew/homebrew-core/pull/290337">CVE-2015-2305</a>. Along the way, extending the scanner’s <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/97">homepage fallback</a> to cover formulae hosted outside GitHub, GitLab and Codeberg also turned up an unrelated data bug where <code class="language-plaintext highlighter-rouge">libart</code>’s homepage field pointed at <a href="https://github.com/Homebrew/homebrew-core/pull/290393">a completely different project</a>.</p>

<p>The glibc formula’s Debian patch tarball resolves ten CVEs, and querying OSV by the sourceware.org repo URL returns exactly one of them. The other nine exist in OSV only as records auto-converted from NVD, and those conversions carry no <code class="language-plaintext highlighter-rouge">affected[].package</code> data: you can <code class="language-plaintext highlighter-rouge">GET /v1/vulns/CVE-2024-2961</code> and read the record, but no query by package, purl or repository will ever return it. All of libquicktime’s patched CVEs were in the same state. So even a scanner with perfect repo-URL derivation has a recall ceiling set by how much of NVD has been enriched with package data upstream, and for the C libraries Homebrew ships a lot of, the answer is not much.</p>

<p>By this point Homebrew had structured <code class="language-plaintext highlighter-rouge">resolves</code> data, version history and patch metadata for every formula, which together are enough to act as an OSV data source. I <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/99">added an <code class="language-plaintext highlighter-rouge">--osv-export</code> flag</a> to the tap that walks every formula’s <code class="language-plaintext highlighter-rouge">resolves</code> list and emits one OSV-schema JSON record per CVE, under a <code class="language-plaintext highlighter-rouge">Homebrew</code> ecosystem with <code class="language-plaintext highlighter-rouge">pkg:brew/&lt;name&gt;</code> package URLs, <code class="language-plaintext highlighter-rouge">BREW-&lt;formula&gt;-&lt;CVE&gt;</code> ids, and the resolving patch details carried in <code class="language-plaintext highlighter-rouge">ecosystem_specific</code>. A demo repository held the initial 26 records with schema validation running in CI, and any OSV-aware tool reading that feed could answer whether a given Homebrew package version was affected by a given CVE without going near the upstream repo.</p>

<h3 id="spec-registrations">Spec registrations</h3>

<p>For anyone else’s tooling to read those records, two specs needed a Homebrew entry and osv.dev needed code to handle it. The <a href="https://github.com/package-url/purl-spec">package-url spec</a> defines the <code class="language-plaintext highlighter-rouge">pkg:</code> identifier format that OSV, CycloneDX, SPDX and most SBOM tooling use to name packages, and it maintains a registry of allowed types. A <code class="language-plaintext highlighter-rouge">pkg:brew</code> type had been <a href="https://github.com/package-url/purl-spec/issues/254">proposed in 2023</a> by William Woodruff and again by others afterwards, and it finally <a href="https://github.com/package-url/purl-spec/pull/796">landed</a> via Rui Chen earlier this year. One wrinkle specific to Homebrew is that formula names can contain <code class="language-plaintext highlighter-rouge">@</code> for version-pinned variants like <code class="language-plaintext highlighter-rouge">openssl@3</code> and <code class="language-plaintext highlighter-rouge">python@3.12</code>, and <code class="language-plaintext highlighter-rouge">@</code> is also purl’s version separator, so <code class="language-plaintext highlighter-rouge">pkg:brew/openssl@3</code> is ambiguous and the <code class="language-plaintext highlighter-rouge">@</code> in the name has to be <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/101">percent-encoded</a> as <code class="language-plaintext highlighter-rouge">pkg:brew/openssl%403</code>.</p>

<p>The <a href="https://github.com/ossf/osv-schema">OSV schema</a> keeps its own registry of ecosystem names and id prefixes, and records that use an unregistered one fail validation. Registering <a href="https://github.com/ossf/osv-schema/pull/576"><code class="language-plaintext highlighter-rouge">Homebrew</code> and the <code class="language-plaintext highlighter-rouge">BREW-</code> prefix</a> was straightforward, as was <a href="https://github.com/ossf/osv-schema/pull/580">documenting the database URL and how Homebrew versions sort</a>, which any tool evaluating an <code class="language-plaintext highlighter-rouge">affected</code> range needs to implement.</p>

<p>Getting osv.dev itself to ingest the feed is the piece <a href="https://github.com/google/osv.dev/issues/5659">still in progress</a>. Two additions are needed per ecosystem in the osv.dev importer: a purl-to-package mapping, which for <code class="language-plaintext highlighter-rouge">pkg:brew</code> is a couple of lines, and a version comparator so that range queries like “is <code class="language-plaintext highlighter-rouge">6.0_7</code> inside <code class="language-plaintext highlighter-rouge">introduced: 0, fixed: 6.0_8</code>” can be evaluated server-side. Homebrew versions are the upstream version string with an optional <code class="language-plaintext highlighter-rouge">_N</code> revision suffix that increments when a formula changes without an upstream release, and the upstream strings are whatever each project uses, semver or otherwise. The comparator is therefore a Python port of Homebrew’s Ruby <code class="language-plaintext highlighter-rouge">Version#&lt;=&gt;</code> logic, which runs to a couple of hundred lines of edge cases. The <a href="https://github.com/google/osv.dev/pull/5660">draft PR</a> is open; until it merges and ingestion is enabled, <code class="language-plaintext highlighter-rouge">brew vulns</code> can’t query <code class="language-plaintext highlighter-rouge">{ecosystem: "Homebrew"}</code> over the API and has to fetch the advisory database’s static JSON directly instead.</p>

<h3 id="advisory-database">Advisory database</h3>

<p>I transferred the demo repository into the Homebrew org once the schema registration merged, initially as <code class="language-plaintext highlighter-rouge">Homebrew/homebrew-advisory-database</code>. That name lasted about a day: the <code class="language-plaintext highlighter-rouge">homebrew-</code> prefix is how the <code class="language-plaintext highlighter-rouge">setup-homebrew</code> CI action identifies taps, and CI runs started failing on an attempt to tap a directory full of JSON files, so it became <a href="https://github.com/Homebrew/advisory-database"><code class="language-plaintext highlighter-rouge">Homebrew/advisory-database</code></a> and every reference to the old name across brew and the repo’s own workflows <a href="https://github.com/Homebrew/brew/pull/23117">had to follow</a>.</p>

<p>The repo holds one JSON file per advisory, currently 33 across six formulae (glibc, libquicktime, lrzsz, nvi, unzip, zip), all generated from <code class="language-plaintext highlighter-rouge">resolves</code> annotations. A daily GitHub Actions workflow regenerates them, and because the org’s branch ruleset requires pull requests on <code class="language-plaintext highlighter-rouge">main</code> the workflow <a href="https://github.com/Homebrew/advisory-database/pull/7">opens a PR</a> instead of pushing. Regeneration originally ran <code class="language-plaintext highlighter-rouge">gem install brew-vulns</code> followed by <code class="language-plaintext highlighter-rouge">brew vulns --osv-export</code>, which meant the tap couldn’t be archived until brew had a native replacement; that arrived as <a href="https://github.com/Homebrew/brew/pull/23106"><code class="language-plaintext highlighter-rouge">brew generate-vulns-advisories</code></a>, a hidden dev command, and the <a href="https://github.com/Homebrew/advisory-database/pull/4">workflow switched over</a>.</p>

<p>The <code class="language-plaintext highlighter-rouge">fixed</code> boundary in each record needed more care than “whatever version ships today”. If unzip’s formula gained a CVE-2014-8139 patch at revision <code class="language-plaintext highlighter-rouge">6.0_6</code> and the current revision is <code class="language-plaintext highlighter-rouge">6.0_8</code>, a record saying <code class="language-plaintext highlighter-rouge">fixed: 6.0_8</code> wrongly flags <code class="language-plaintext highlighter-rouge">6.0_6</code> and <code class="language-plaintext highlighter-rouge">6.0_7</code> as vulnerable. The generator now <a href="https://github.com/Homebrew/brew/pull/23115">walks homebrew-core’s git history</a> via <code class="language-plaintext highlighter-rouge">FormulaVersions</code> to find the first revision where the resolving patch appeared, and <a href="https://github.com/Homebrew/brew/pull/23112">merges into existing records</a> rather than overwriting them so a hand-corrected boundary or a curated record survives the next nightly run. The <a href="https://github.com/Homebrew/advisory-database/blob/main/CONTRIBUTING.md">contributing guide</a> covers the curated path for cases the generator can’t express, using a <code class="language-plaintext highlighter-rouge">BREW-0000-0000</code> placeholder id that a maintainer replaces on merge.</p>

<h3 id="merging-into-homebrewbrew">Merging into Homebrew/brew</h3>

<p>Around this point I was added as a Homebrew maintainer, which made the next stretch a lot smoother since it involved landing a run of interdependent changes across brew, homebrew-core and the advisory database in quick succession. The scanner <a href="https://github.com/Homebrew/brew/pull/23080">moved into <code class="language-plaintext highlighter-rouge">Library/Homebrew/cmd/vulns.rb</code></a> with a supporting <code class="language-plaintext highlighter-rouge">vulns/</code> module directory, following the same pattern as <code class="language-plaintext highlighter-rouge">brew livecheck</code>, and the <a href="https://github.com/Homebrew/homebrew-brew-vulns/issues/111">tracking issue</a> has the full design doc listing everything that changed on the way in.</p>

<p>User-facing commands in brew’s <code class="language-plaintext highlighter-rouge">cmd/</code> directory can’t lazy-load gem groups (a RuboCop cop enforces it), which was the biggest constraint on the port: anything the command needs has to be in the vendored bundle already or reimplemented, and adding five gems to every Homebrew install for one subcommand wasn’t going to fly. The tap depended on <a href="https://rubygems.org/gems/purl"><code class="language-plaintext highlighter-rouge">purl</code></a>, <a href="https://rubygems.org/gems/vers"><code class="language-plaintext highlighter-rouge">vers</code></a>, <a href="https://rubygems.org/gems/cvss-suite"><code class="language-plaintext highlighter-rouge">cvss-suite</code></a>, <a href="https://rubygems.org/gems/sbom"><code class="language-plaintext highlighter-rouge">sbom</code></a> and <a href="https://rubygems.org/gems/sarif-ruby"><code class="language-plaintext highlighter-rouge">sarif-ruby</code></a>, and all five went.</p>

<p>The <code class="language-plaintext highlighter-rouge">sbom</code> and <code class="language-plaintext highlighter-rouge">sarif-ruby</code> gems were only used for <code class="language-plaintext highlighter-rouge">--cyclonedx</code> and <code class="language-plaintext highlighter-rouge">--sarif</code> output, so those flags are dropped for now and will return as direct JSON emission validated against the published schemas. The <code class="language-plaintext highlighter-rouge">purl</code> gem was used exactly once, to extract the ecosystem token from an OSV record’s <code class="language-plaintext highlighter-rouge">affected.package.purl</code> field, and since the merged code only evaluates <code class="language-plaintext highlighter-rouge">SEMVER</code> ranges locally that lookup is never reached.</p>

<p>The <code class="language-plaintext highlighter-rouge">vers</code> gem became a small <code class="language-plaintext highlighter-rouge">Homebrew::Vulns::Semver</code> module implementing strict SemVer 2.0 comparison. Homebrew’s own <code class="language-plaintext highlighter-rouge">Version</code> class isn’t a drop-in here: it sorts <code class="language-plaintext highlighter-rouge">1.0.0-alpha.1</code> above <code class="language-plaintext highlighter-rouge">1.0.0-alpha.beta</code> where the SemVer spec says numeric prerelease identifiers sort below alphanumeric ones, and it compares <code class="language-plaintext highlighter-rouge">+build</code> metadata where SemVer says build metadata is ignored for ordering. Getting either wrong turns a range boundary into a false positive or negative.</p>

<p>The <code class="language-plaintext highlighter-rouge">cvss-suite</code> gem became <code class="language-plaintext highlighter-rouge">Homebrew::Vulns::CVSS</code>, which implements the CVSS v3.0/v3.1 base-score formula and buckets the result. CVSS v4.0 scoring requires a roughly 270-entry MacroVector lookup table plus interpolation between entries, so v4 vectors currently fall through to the next <code class="language-plaintext highlighter-rouge">severity</code> entry on the record; almost every record with a v4 vector also carries a v3 one, so in practice little is lost.</p>

<p>The tap’s <code class="language-plaintext highlighter-rouge">--all</code> flag was going to become <code class="language-plaintext highlighter-rouge">--eval-all</code> to match other brew commands, until <code class="language-plaintext highlighter-rouge">--eval-all</code> was replaced across brew with a tap-trust mechanism while the PR was in review, so with no arguments <code class="language-plaintext highlighter-rouge">brew vulns</code> now scans every formula from a trusted tap and falls back to installed formulae otherwise. The tap also always read the current formula definition for the source URL and version, so an old installed keg would be checked against whatever version the formula had since been bumped to. The built-in command reads the <a href="https://github.com/Homebrew/brew/pull/23140">installed keg’s SPDX SBOM</a> instead, and if an outdated keg predates SBOM generation the scan warns and exits nonzero rather than reporting a version it never checked.</p>

<p>All of that shipped together in Homebrew 6.0.11, and the tap is now <a href="https://github.com/Homebrew/homebrew-brew-vulns/pull/112">a deprecation stub</a> pointing at the built-in command.</p>

<h3 id="coverage">Coverage</h3>

<p>The command as shipped queries OSV’s GIT ecosystem by repo URL, which covers about 73% of homebrew-core (6,210 of 8,492 formulae); the other 2,282 have no derivable GitHub, GitLab or Codeberg URL and are skipped, with a count reported at the end of the run. Closing that gap means finding other keys that join a formula to an advisory, and doing the joining once at authoring time in the advisory database rather than on every user’s machine. That way <code class="language-plaintext highlighter-rouge">brew vulns</code> stays at a single batched API call however many matching strategies feed the database behind it.</p>

<p>Of the skipped formulae, 322 have a <code class="language-plaintext highlighter-rouge">stable.url</code> pointing at a language package registry (145 PyPI, 144 npm, plus Maven, Hackage, CPAN, crates and a few others), and a URL like <code class="language-plaintext highlighter-rouge">files.pythonhosted.org/.../foo-1.2.tar.gz</code> encodes enough to build a <code class="language-plaintext highlighter-rouge">pkg:pypi/foo@1.2</code> purl and query OSV’s PyPI ecosystem directly. The same applies to the 12,283 <code class="language-plaintext highlighter-rouge">resource</code> blocks across homebrew-core, which are vendored dependencies inside formulae; about 91% of those are registry URLs (over 10,000 PyPI alone) and none are scanned today.</p>

<p>That leaves roughly 1,960 formulae hosted on SourceForge, GNU mirrors, gnome.org download servers, Debian archives and the like, with no direct package identifier at all. For those the join goes through <a href="https://repology.org">Repology</a>, which already indexes Homebrew alongside every major Linux distro: look up the formula name, get back the Debian, Alpine and openSUSE source-package names for the same upstream, query OSV’s distro ecosystems for those, and collect the CVE aliases.</p>

<p>Repology’s <a href="https://dumps.repology.org/">bulk dumps</a> turned out to be 10GB+ PostgreSQL restores rather than anything directly loadable, so the <a href="https://github.com/Homebrew/advisory-database/pull/11">index builder</a> paginates the API instead, about 43 pages at one request per second, and writes a distilled <code class="language-plaintext highlighter-rouge">formula -&gt; {distro -&gt; srcname}</code> map into the advisory repo. Name matching needs normalisation (<code class="language-plaintext highlighter-rouge">postgresql@16</code> maps to Repology’s <code class="language-plaintext highlighter-rouge">postgresql</code>, <code class="language-plaintext highlighter-rouge">libmatio</code> maps to <code class="language-plaintext highlighter-rouge">matio</code>, and Repology sometimes lists a Debian binary package where the OSV feed keys on the source package), and corrections to Repology’s project grouping go upstream to <a href="https://github.com/repology/repology-rules">repology-rules</a> instead of into a local override layer.</p>

<p>OSV has no CPAN, CTAN or LuaRocks ecosystem, and 61 formulae vendor at least one CPAN module, so that path reads the <a href="https://github.com/briandfoy/cpan-security-advisory">cpan-security-advisory</a> database directly. Between the registry URLs, the Repology mapping and the CPAN feed, roughly 96% of homebrew-core ends up with at least one automated join key, with a few hundred formulae left over that will need a hand-curated mapping file in the advisory repo.</p>

<h3 id="next">Next</h3>

<p>The scan output can surface in more of Homebrew than a dedicated subcommand. Running the scanner once inside <code class="language-plaintext highlighter-rouge">brew generate-formula-api</code> and attaching a <code class="language-plaintext highlighter-rouge">vulnerabilities</code> field to each formula’s JSON would put open findings on <a href="https://formulae.brew.sh">formulae.brew.sh</a> formula pages and in <code class="language-plaintext highlighter-rouge">brew info</code> without a live query, since both already read that JSON. From there <code class="language-plaintext highlighter-rouge">brew install</code> could warn when the version being installed has an open advisory, and <code class="language-plaintext highlighter-rouge">brew upgrade</code> could list which CVEs each pending bump resolves alongside the version change, so the security case for an upgrade is visible when you’re deciding whether to run it.</p>

<p>Every installed keg already carries an SPDX SBOM listing its dependencies with versions, and the built-in command reads that SBOM for the top-level source URL now, so the inventory for scanning a formula’s whole dependency tree is already on disk and the remaining work is the same matching problem as everything above. Casks are harder because a large share of them package closed-source applications, with no upstream repo to query and no visible dependency list to walk. The <a href="/2026/07/01/the-cra-is-not-about-open-source">EU Cyber Resilience Act</a> will change that from outside Homebrew: vendors selling into Europe will have to produce SBOMs for their products, and as those start to appear for the apps that casks install, scanning a cask’s dependency tree becomes the same job as scanning a keg’s.</p>

<p>On the maintainer side, a <a href="https://github.com/Homebrew/homebrew-core">homebrew-core</a> PR that bumps a formula is the natural place to report which CVEs the bump closes, which are still open at the new version, and which affect any vendored <code class="language-plaintext highlighter-rouge">resource</code> blocks. Running the full matcher against the one changed formula is cheap enough to sit inside <code class="language-plaintext highlighter-rouge">brew test-bot</code> alongside the existing audit checks. Anything it finds that isn’t already a <code class="language-plaintext highlighter-rouge">BREW-*</code> record is a candidate for the advisory database, giving it a second feed driven by formula changes as well as the nightly advisory sweep. Restoring <code class="language-plaintext highlighter-rouge">--sarif</code> and <code class="language-plaintext highlighter-rouge">--cyclonedx</code> output brings back the code-scanning and SBOM integrations for downstream projects that want to scan their own Brewfiles in CI.</p>

<p>Underneath all of that, <a href="https://github.com/google/osv.dev/pull/5660">osv.dev ingestion</a> is still the gate for querying <code class="language-plaintext highlighter-rouge">{ecosystem: "Homebrew"}</code> directly rather than fetching the advisory JSON, and CVSS v4 scoring still needs its lookup table before v4-only records get a severity label.</p>

<p>Thanks to <a href="https://github.com/MikeMcQuaid">Mike McQuaid</a> and Patrick for all the help getting this over the line so far, and to the rest of the Homebrew maintainers for reviews across an unreasonable number of pull requests. If you’re on Homebrew 6.0.11 or later <code class="language-plaintext highlighter-rouge">brew vulns</code> is already there; the <a href="https://github.com/Homebrew/advisory-database">advisory database</a> and the <a href="https://github.com/orgs/Homebrew/discussions/6869">discussion thread</a> that most of this fell out of are good places to follow along.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="homebrew" /><category term="security" /><summary type="html"><![CDATA[One command, six repos, three standards bodies, an advisory database, and a version comparator written in the wrong language.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">This Week in Package Management: 11 July 2026</title><link href="https://nesbitt.io/2026/07/11/this-week-in-package-management.html" rel="alternate" type="text/html" title="This Week in Package Management: 11 July 2026" /><published>2026-07-11T10:00:00+00:00</published><updated>2026-07-11T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/11/this-week-in-package-management</id><content type="html" xml:base="https://nesbitt.io/2026/07/11/this-week-in-package-management.html"><![CDATA[<p>Week eight of the roundup, built from the <a href="https://github.com/ecosyste-ms/package-managers-opml">package manager OPML feed collection</a> and whatever I’ve posted or boosted on <a href="https://mastodon.social/@andrewnez">Mastodon</a>.</p>

<h2 id="releases">Releases</h2>

<p><a href="https://github.com/npm/cli/releases/tag/v12.0.0">npm 12.0.0</a> is out. <code class="language-plaintext highlighter-rouge">allow-git</code> and <code class="language-plaintext highlighter-rouge">allow-remote</code> now default to <code class="language-plaintext highlighter-rouge">none</code>, so installing git dependencies or user-supplied tarball URLs needs explicit opt-in. <code class="language-plaintext highlighter-rouge">npm shrinkwrap</code> is removed and <code class="language-plaintext highlighter-rouge">npm-shrinkwrap.json</code> is no longer honoured at the project root or inside dependency tarballs. Unknown <code class="language-plaintext highlighter-rouge">.npmrc</code> keys, unknown CLI flags and abbreviated flags now error instead of warn, root <code class="language-plaintext highlighter-rouge">preinstall</code> runs before dependencies are installed, and <code class="language-plaintext highlighter-rouge">npm adduser</code>, <code class="language-plaintext highlighter-rouge">star</code> and <code class="language-plaintext highlighter-rouge">unstar</code> are gone.</p>

<p><a href="https://pnpm.io/blog/releases/11.10">pnpm 11.10</a> adds an <code class="language-plaintext highlighter-rouge">_auth</code> setting that takes registry credentials as a single URL-keyed structure, so CI can pass them via <code class="language-plaintext highlighter-rouge">pnpm_config__auth</code> without the shell-quoting problems that broke the per-registry env vars. It also adds <code class="language-plaintext highlighter-rouge">pnpm prefix</code> and <code class="language-plaintext highlighter-rouge">pnpm issues</code>, and <code class="language-plaintext highlighter-rouge">pnpm self-update</code> can now install v12, the Rust port. <a href="https://github.com/pnpm/pnpm/releases/tag/v11.11.0">11.11</a> followed with a <code class="language-plaintext highlighter-rouge">pnpm access</code> command for managing registry visibility, MFA requirements and team access, and lets <code class="language-plaintext highlighter-rouge">allowBuilds</code> entries for git-hosted packages match by repository URL without pinning a commit hash.</p>

<p><a href="https://github.com/astral-sh/uv/releases/tag/0.11.28">uv 0.11.28</a> hardens ZIP handling against parser differentials via an updated <code class="language-plaintext highlighter-rouge">astral-async-zip</code>, so uv may now reject malformed wheels it previously accepted, matching last week’s tar work. <a href="https://github.com/astral-sh/uv/releases/tag/0.11.27">0.11.27</a> preceded it with resolver performance work: SIMD TOML parsing, interned <code class="language-plaintext highlighter-rouge">requires-python</code> specifiers and cached lock markers.</p>

<p><a href="https://go.dev/doc/devel/release#go1.26.5">Go 1.26.5</a> is a security release fixing issues in <code class="language-plaintext highlighter-rouge">crypto/tls</code> and <code class="language-plaintext highlighter-rouge">os</code>, alongside bug fixes to the compiler, runtime and <code class="language-plaintext highlighter-rouge">go</code> command.</p>

<p><a href="https://blog.rust-lang.org/2026/07/09/Rust-1.97.0/">Rust 1.97.0</a> stabilises <code class="language-plaintext highlighter-rouge">resolver.lockfile-path</code> in Cargo config for pointing at a lockfile outside a read-only source directory, and <code class="language-plaintext highlighter-rouge">build.warnings</code> for turning warnings into errors without <code class="language-plaintext highlighter-rouge">-Dwarnings</code> invalidating the build cache. <code class="language-plaintext highlighter-rouge">cargo clean</code> now refuses a <code class="language-plaintext highlighter-rouge">--target-dir</code> that doesn’t look like a Cargo target directory.</p>

<p><a href="https://github.com/microsoft/winget-cli/releases/tag/v1.29.280">winget 1.29</a> adds an experimental source priority feature: sources get a numeric priority via <code class="language-plaintext highlighter-rouge">winget source add</code> or <code class="language-plaintext highlighter-rouge">source edit</code>, and higher-priority sources sort first when a search matches packages in more than one.</p>

<p><a href="https://github.com/spack/spack/releases/tag/v1.2.1">Spack 1.2.1</a> fixes a hang in the new installer when running under <code class="language-plaintext highlighter-rouge">forkserver</code> and restores solver performance on macOS.</p>

<p><a href="https://github.com/CocoaPods/CocoaPods/releases/tag/1.17.0">CocoaPods 1.17.0</a> adds <code class="language-plaintext highlighter-rouge">--no-lint</code> to <code class="language-plaintext highlighter-rouge">pod repo push</code> to skip the lint phase when publishing, and updates <code class="language-plaintext highlighter-rouge">ruby-macho</code> so mergeable libraries are detected.</p>

<p><a href="https://github.com/hexpm/hex/releases/tag/v2.5.1">Hex 2.5.1</a> adds <code class="language-plaintext highlighter-rouge">ignore_advisories</code> and <code class="language-plaintext highlighter-rouge">ignore_retirements</code> to the <code class="language-plaintext highlighter-rouge">mix.exs</code> <code class="language-plaintext highlighter-rouge">:hex</code> block and as environment variables, so acknowledged CVEs and retirements can be listed once and <code class="language-plaintext highlighter-rouge">mix hex.audit</code> reports them separately without failing. Advisories can be ignored by any aliased identifier.</p>

<p><a href="https://github.com/jdx/mise/releases/tag/v2026.7.4">mise 2026.7.4</a> graduates <code class="language-plaintext highlighter-rouge">mise bootstrap</code> and <code class="language-plaintext highlighter-rouge">mise dotfiles</code> out of experimental mode, so system packages, repos, user services and shell activation now work without <code class="language-plaintext highlighter-rouge">MISE_EXPERIMENTAL</code>. <a href="https://github.com/jdx/mise/releases/tag/v2026.7.5">2026.7.5</a> followed, sharing config trust across git worktrees so trusting a repo once covers every <code class="language-plaintext highlighter-rouge">git worktree add</code> checkout, and fixing npm-backed tools on npm 12.</p>

<p>Also out: <a href="https://github.com/Homebrew/brew/releases/tag/6.0.9">Homebrew 6.0.9</a>, <a href="https://blog.rubygems.org/2026/07/10/4.0.16-released.html">RubyGems 4.0.16</a>, <a href="https://github.com/ruby/rubygems/releases/tag/bundler-v4.0.16">Bundler 4.0.16</a>, <a href="https://github.com/rust-lang/cargo/releases/tag/0.98.0">Cargo 0.98.0</a>, <a href="https://github.com/asdf-vm/asdf/releases/tag/v0.20.0">asdf 0.20.0</a>, <a href="https://github.com/pypa/hatch/releases/tag/hatch-v1.17.1">Hatch 1.17.1</a>, <a href="https://github.com/pypa/hatch/releases/tag/hatchling-v1.31.0">Hatchling 1.31.0</a>, <a href="https://github.com/prefix-dev/pixi/releases/tag/v0.72.2">pixi 0.72.2</a>, <a href="https://github.com/yarnpkg/berry/releases/tag/%40yarnpkg%2Fcli%2F4.17.1">Yarn 4.17.1</a>, <a href="https://github.com/denoland/deno/releases/tag/v2.9.2">Deno 2.9.2</a>, <a href="https://github.com/helm/helm/releases/tag/v4.2.3">Helm 4.2.3</a>, <a href="https://github.com/helm/helm/releases/tag/v3.21.3">Helm 3.21.3</a>, <a href="https://github.com/podman-container-tools/podman/releases/tag/v6.0.1">Podman 6.0.1</a>, <a href="https://github.com/NixOS/nix/releases/tag/2.34.8">Nix 2.34.8</a>, <a href="https://github.com/gradle/gradle/releases/tag/v9.7.0-M3">Gradle 9.7.0-M3</a>, <a href="https://github.com/apache/maven/releases/tag/maven-3.10.0-rc-1">Maven 3.10.0-rc-1</a>, <a href="https://github.com/renovatebot/renovate/releases/tag/43.258.0">Renovate 43.258.0</a>, <a href="https://github.com/dependabot/dependabot-core/releases/tag/v0.385.0">Dependabot Core 0.385.0</a>, <a href="https://go.dev/dl/#go1.27rc2">Go 1.27rc2</a>.</p>

<h2 id="security">Security</h2>

<p><a href="https://github.com/ocaml/opam/releases/tag/2.5.2">opam 2.5.2</a> fixes CVE-2026-57825: a package could install files anywhere on the system by including a symlink to an external directory, bypassing the user prompt that direct external paths trigger.</p>

<p>pnpm <a href="https://github.com/pnpm/pnpm/releases/tag/v11.11.0">11.11.0</a> and <a href="https://github.com/pnpm/pnpm/releases/tag/v10.34.5">10.34.5</a> fix two path traversals: a crafted <code class="language-plaintext highlighter-rouge">pnpm-lock.yaml</code> dependency key could write package content outside the virtual store, and a dependency whose manifest <code class="language-plaintext highlighter-rouge">name</code> was a scoped traversal like <code class="language-plaintext highlighter-rouge">@x/../../../&lt;path&gt;</code> could be written outside <code class="language-plaintext highlighter-rouge">node_modules</code> during install even with <code class="language-plaintext highlighter-rouge">--ignore-scripts</code>.</p>

<p><a href="https://github.com/oras-project/oras/releases/tag/v1.3.3">ORAS 1.3.3</a> picks up oras-go 2.6.2 to fix <a href="https://github.com/advisories/GHSA-fxhp-mv3v-67qp">CVE-2026-50163</a>: a crafted OCI artifact with a relative hardlink target could get <code class="language-plaintext highlighter-rouge">oras pull</code> to link into a file in the invoker’s working directory rather than the extract directory.</p>

<h2 id="articles">Articles</h2>

<p><a href="https://blog.packagist.com/immutable-versions-on-packagist/">Immutable Versions on Packagist</a> (Packagist Blog) is the next post in the Composer supply chain series: once a stable version is published its git reference is now fixed, retag attempts are blocked with an email to the maintainer, and deletions become soft with a public transparency log.</p>

<p><a href="https://blog.yossarian.net/2026/07/07/You-shouldnt-trust-trusted-publishing">You shouldn’t trust Trusted Publishing</a> (William Woodruff) argues Trusted Publishing is an authentication mechanism between CI and a registry, not a signal that a package is safe, and that PyPI keeps it out of the badge UI for exactly that reason.</p>

<h2 id="elsewhere">Elsewhere</h2>

<p>Trail of Bits published <a href="https://pytransparency.dev/">a proposal for PyPI transparency logs</a>: an append-only log of every distribution file the index serves, with inclusion proofs exposed through the Simple API, so a compromised index serving different artifacts to different users becomes detectable. There is a <a href="https://github.com/trailofbits/pypi-transparency/blob/main/peps/pep-xxxx.rst">draft PEP</a>, a test log, and <a href="https://github.com/trailofbits/pypi-transparency">source</a>.</p>

<p>The <a href="https://programme.europython.eu/europython-2026/talk/SBREFN/">EuroPython 2026 Packaging Summit</a> schedule is up for 13 July in Kraków, with <a href="https://hackmd.io/@jezdez/europython2026-packaging-summit">public notes</a> and late topic proposals still open.</p>

<p><a href="https://opensourcesecurity.io/2026/2026-07-rfmf-lori-niko/">Open Source Security: Rust Foundation Maintainers Fund</a> (Josh Bressers) is a podcast conversation with Lori Lorusso and Niko Matsakis on how the fund is structured and where the money goes.</p>

<p>The Nix Foundation is <a href="https://nixos.org/blog/announcements/2026/docs-funding-2026/">fundraising for a documentation team</a> via Open Collective, with itemised monthly and one-off funding targets for paid contributors working on onboarding and reference material.</p>

<p><a href="https://github.blog/open-source/git/highlights-from-git-2-55/">Git 2.55</a> adds <code class="language-plaintext highlighter-rouge">git history fixup &lt;commit&gt;</code> for amending an earlier commit and replaying its descendants in one step, incremental multi-pack index repacking, a built-in inotify filesystem monitor on Linux, and a <code class="language-plaintext highlighter-rouge">hook.&lt;name&gt;.parallel</code> config for running hooks concurrently.</p>

<p><a href="https://arxiv.org/abs/2607.04614">Beyond Compliance: A Large Scale Study on the Completeness and Consistency of the GitHub SBOMs</a> (Bhuiyan et al., arXiv) measures GitHub’s auto-generated SBOMs across ecosystems and finds version and licence coverage varies enough by language that reliability depends on which ecosystem you’re in.</p>

<h2 id="git-pkgs">git-pkgs</h2>

<p>I tagged six repos this week:</p>

<ul>
  <li><a href="https://github.com/git-pkgs/brief/releases/tag/v0.9.3">brief v0.9.3</a></li>
  <li><a href="https://github.com/git-pkgs/enrichment/releases/tag/v0.6.0">enrichment v0.6.0</a></li>
  <li><a href="https://github.com/git-pkgs/purl/releases/tag/v0.1.14">purl v0.1.14</a></li>
  <li><a href="https://github.com/git-pkgs/sarif/releases/tag/v0.1.0">sarif v0.1.0</a></li>
  <li><a href="https://github.com/git-pkgs/sbom/releases/tag/v0.1.3">sbom v0.1.3</a></li>
  <li><a href="https://github.com/git-pkgs/vulns/releases/tag/v0.2.0">vulns v0.2.0</a></li>
</ul>

<p>Send links for next week to <a href="https://mastodon.social/@andrewnez">@andrewnez@mastodon.social</a>.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="weekly" /><summary type="html"><![CDATA[Releases, advisories, and articles from across the package management world]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Package Management as Org Chart</title><link href="https://nesbitt.io/2026/07/10/package-management-as-org-chart.html" rel="alternate" type="text/html" title="Package Management as Org Chart" /><published>2026-07-10T10:00:00+00:00</published><updated>2026-07-10T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/10/package-management-as-org-chart</id><content type="html" xml:base="https://nesbitt.io/2026/07/10/package-management-as-org-chart.html"><![CDATA[<p>Conway’s Law says organisations produce systems that copy their own communication structure. Dependency management tooling is part of the system. A resolution strategy is an opinion about how disagreements get settled; a manifest format records who is allowed to depend on whom.</p>

<p><strong>Monorepo, single version policy</strong>: every package in the tree must agree on one version of each dependency; upgrading anything means upgrading everyone at once. The org chart has been forbidden from appearing in the code. It works when there’s a standing migration workforce whose job is dragging every consumer along.</p>

<p><strong>Monorepo with workspaces</strong>: one repository, but each package keeps its own manifest and can pin its own versions. One strong platform team, and leadership that believes coordination was solved at the 2019 offsite.</p>

<p><strong>Git submodules</strong>: a dependency is referenced by exact commit SHA in a separate repository, and updating it is a manual two-step that never gets automated. Two teams agreed to collaborate and recorded, to the commit hash, exactly how little they meant it.</p>

<p><strong>Bazel</strong>: every dependency edge between targets is declared explicitly in a BUILD file; nothing is ambient or inferred. Adopted when the org grew past the point where anyone knew who depended on whom by asking, so the build system was made to enforce what the humans had lost track of. Typically legible to one person, who came from Google.</p>

<p><strong>Nix / Guix</strong>: builds are pure functions of their declared inputs; anything not listed in the derivation doesn’t exist at build time. “Works on my machine” has been made a structurally impossible sentence, at the cost of most of the hiring pipeline.</p>

<p><strong>Maven nearest-wins mediation</strong>: when two paths through the tree want different versions of the same artifact, Maven picks whichever is fewer hops from the root, regardless of which is newer or satisfies more constraints. Conflict resolution by proximity to the top, which is also how the org settles most disagreements.</p>

<p><strong>Artifactory in front of the public registries, private namespace full of forks</strong>: every install goes through a proxy the org controls, and packages that needed patching were forked in rather than contributed back. Trust is granted by a system rather than a person, and the forks date from an argument Legal won that hasn’t been reopened since.</p>

<p><strong>deb/rpm packages for the in-house application</strong>: the app is built into an OS package and installed by the system package manager, and releasing goes through the same gate as a kernel update. Ops won, and has governed since. Releases are events with a runbook and at least one person whose job title contains the word “release”.</p>

<p><strong>Docker</strong>: the application ships with its own copy of the operating system, so the deployment environment is whatever the developer decided at build time. Dev seceded from Ops and took the OS with them, and Ops can no longer reject an artifact that contains everything Ops used to control. The company finds out how many copies of Debian it’s running when the next xz happens.</p>

<p><strong>Terraform modules from a private registry</strong>: infrastructure is packaged as versioned modules that application teams consume like any other dependency. The infra team built an interface so application teams would stop paging them, and now gets paged about the interface.</p>

<p><strong><a href="https://module-federation.io/">Module federation</a></strong>: separately built and deployed JavaScript bundles negotiate shared dependency versions with each other at runtime, in the browser. Teams report to different VPs who won’t share a meeting, so version resolution was deferred to the last possible moment, on the user’s machine, because nowhere earlier in the pipeline could agreement be reached.</p>

<p><strong>peerDependencies</strong>: the package declares a version constraint on a dependency its host must provide, without shipping anything to satisfy it. A framework team issuing policy to application teams: you will be on React 18, we will check, satisfying the constraint is your problem.</p>

<p><strong>Vendored dependencies</strong>: the source of each dependency is copied into the repository and committed; upstream can change or vanish without effect. An org that was hurt by an upstream once and now takes hostages; the cost is a manual merge that lives permanently in next sprint.</p>

<p><strong>No lockfile, <code class="language-plaintext highlighter-rouge">latest</code> everywhere</strong>: each install resolves fresh against the registry, and the dependency set is whatever’s newest at that moment. The founder still commits to main, had a bad experience with <code class="language-plaintext highlighter-rouge">npm-shrinkwrap.json</code> years ago, and won’t touch a lockfile.</p>

<p><strong>semantic-release on every merge</strong>: the version number is computed from <a href="https://www.conventionalcommits.org/">Conventional Commits</a> prefixes and a release is cut automatically whenever main changes. Publishing became a side effect of the commit prefix, because being the person who cut the release had become a liability.</p>

<p><strong>Go MVS</strong>: resolves each dependency to the highest version anything in the graph explicitly requires, and no higher; nothing upgrades just because a newer version exists. Designed by someone who looked at SAT-solver resolution and concluded the problem was self-inflicted, and it encodes an org where change only happens when someone puts the requirement in writing.</p>

<p><strong>Brewfile</strong>: the development environment is declared as a list of Homebrew packages that <code class="language-plaintext highlighter-rouge">brew bundle</code> installs in order. Onboarding as ritual: the file fails on line 14, and the actual mechanism for getting a working machine is a Slack thread called <code class="language-plaintext highlighter-rouge">#dev-setup-help</code>.</p>

<hr />

<p>Most dependency strategies are attempts to avoid interpersonal negotiation. The tooling doesn’t remove the disagreement, it just picks who loses by default.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="dependencies" /><category term="satire" /><summary type="html"><![CDATA[Conway's Law applied to dependency management designs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Unboxed: Zig</title><link href="https://nesbitt.io/2026/07/09/unboxed-zig.html" rel="alternate" type="text/html" title="Unboxed: Zig" /><published>2026-07-09T10:00:00+00:00</published><updated>2026-07-09T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/09/unboxed-zig</id><content type="html" xml:base="https://nesbitt.io/2026/07/09/unboxed-zig.html"><![CDATA[<p>This is the first in a series of posts working through individual package managers against a fixed set of headings, so they can be compared directly. The headings come from earlier posts: the <a href="/2025/12/29/categorizing-package-manager-clients.html">client</a> and <a href="/2025/12/29/categorizing-package-registries.html">registry</a> categorisations, the <a href="/2025/12/22/package-registries-are-governance-as-a-service.html">governance</a> post, and the <a href="/2026/05/05/package-manager-threat-models.html">threat model</a>.</p>

<p>Zig’s package manager has been built into the <code class="language-plaintext highlighter-rouge">zig</code> binary since <a href="https://ziglang.org/download/0.11.0/release-notes.html#Package-Management">0.11 in August 2023</a>, with no separate tool and no central registry. A <code class="language-plaintext highlighter-rouge">build.zig.zon</code> file lists dependencies as URLs with content hashes, and <code class="language-plaintext highlighter-rouge">zig build</code> fetches and compiles everything together. The language and the tool are both run by the <a href="https://ziglang.org/zsf/">Zig Software Foundation</a>, a 501(c)(3).</p>

<h2 id="how-it-works">How it works</h2>

<p>A Zig project has a <code class="language-plaintext highlighter-rouge">build.zig</code> program at its root, describing targets, compile flags, and which dependencies to link where, alongside a <code class="language-plaintext highlighter-rouge">build.zig.zon</code> data file listing metadata and dependencies. Despite the similar names, <code class="language-plaintext highlighter-rouge">build.zig.zon</code> is inert data that can be parsed without running anything, while <code class="language-plaintext highlighter-rouge">build.zig</code> is arbitrary Zig code that gets compiled and executed. Which of the two an operation touches is where most of the security properties come from.</p>

<div class="language-zig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="p">{</span>
    <span class="p">.</span><span class="py">name</span> <span class="o">=</span> <span class="p">.</span><span class="py">example</span><span class="p">,</span>
    <span class="p">.</span><span class="py">version</span> <span class="o">=</span> <span class="s">"0.3.1"</span><span class="p">,</span>
    <span class="p">.</span><span class="py">fingerprint</span> <span class="o">=</span> <span class="mi">0x6a8091f57c7f07ff</span><span class="p">,</span>
    <span class="p">.</span><span class="py">minimum_zig_version</span> <span class="o">=</span> <span class="s">"0.16.0"</span><span class="p">,</span>
    <span class="p">.</span><span class="py">dependencies</span> <span class="o">=</span> <span class="o">.</span><span class="p">{</span>
        <span class="p">.</span><span class="py">known_folders</span> <span class="o">=</span> <span class="o">.</span><span class="p">{</span>
            <span class="p">.</span><span class="py">url</span> <span class="o">=</span> <span class="s">"https://github.com/ziglibs/known-folders/archive/refs/tags/1.1.0.tar.gz"</span><span class="p">,</span>
            <span class="p">.</span><span class="py">hash</span> <span class="o">=</span> <span class="s">"known_folders-1.1.0-Fy-PJtnVAAC1Qq48Hf6_4er0Ku98mFvx99UUwo9-mrJd"</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">.</span><span class="py">tracy</span> <span class="o">=</span> <span class="o">.</span><span class="p">{</span>
            <span class="p">.</span><span class="py">url</span> <span class="o">=</span> <span class="s">"git+https://github.com/wolfpld/tracy#v0.11.1"</span><span class="p">,</span>
            <span class="p">.</span><span class="py">hash</span> <span class="o">=</span> <span class="s">"N-V-__8AAKw3UgOhKDsrn8hRlOoGmVBl5x91fMi0WQwVaokf"</span><span class="p">,</span>
            <span class="p">.</span><span class="py">lazy</span> <span class="o">=</span> <span class="kc">true</span><span class="p">,</span>
        <span class="p">},</span>
    <span class="p">},</span>
    <span class="p">.</span><span class="py">paths</span> <span class="o">=</span> <span class="o">.</span><span class="p">{</span>
        <span class="s">"build.zig"</span><span class="p">,</span>
        <span class="s">"build.zig.zon"</span><span class="p">,</span>
        <span class="s">"src"</span><span class="p">,</span>
        <span class="s">"LICENSE"</span><span class="p">,</span>
    <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.zon</code> extension is <a href="https://github.com/ziglang/zig/blob/master/lib/std/zon.zig">Zig Object Notation</a>, a subset of Zig’s syntax restricted to literals and anonymous structs with no expressions, imports, or function calls. The <a href="https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md"><code class="language-plaintext highlighter-rouge">fingerprint</code></a> is a 64-bit integer combining a random 32-bit id with a checksum of the <code class="language-plaintext highlighter-rouge">name</code>, generated once when the package is created. It stays the same through renames and repo moves, so it’s the package’s identity independent of whichever URL currently serves it.</p>

<p>Each dependency is either a <code class="language-plaintext highlighter-rouge">.url</code> and <code class="language-plaintext highlighter-rouge">.hash</code> pair or a local <code class="language-plaintext highlighter-rouge">.path</code>. The hash is computed over the extracted files after applying the <code class="language-plaintext highlighter-rouge">.paths</code> inclusion list from the dependency’s own manifest, so a package author decides which files count towards its identity. The <a href="https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md">documentation</a> is explicit that the hash is the identity and the URL is just one place to get it: “packages do not come from a <code class="language-plaintext highlighter-rouge">url</code>; they come from a <code class="language-plaintext highlighter-rouge">hash</code>. <code class="language-plaintext highlighter-rouge">url</code> is just one of many possible mirrors.”</p>

<p>The <a href="https://github.com/ziglang/zig/pull/22994">hash string</a> is <code class="language-plaintext highlighter-rouge">$name-$version-</code> followed by URL-safe base64 packing the fingerprint id, decompressed size, and 200 bits of truncated SHA-256, so a <code class="language-plaintext highlighter-rouge">zig-pkg/</code> directory listing stays readable without a lookup table. A dependency with no <code class="language-plaintext highlighter-rouge">build.zig.zon</code> of its own, like <code class="language-plaintext highlighter-rouge">tracy</code> above, is a “naked” package and gets the placeholders <code class="language-plaintext highlighter-rouge">N-V-</code> and a sentinel id instead. <code class="language-plaintext highlighter-rouge">.lazy = true</code> defers fetching until the dependency is referenced from <code class="language-plaintext highlighter-rouge">build.zig</code>.</p>

<p>Dependencies are fetched by downloading the URL, verifying it against the pinned hash, unpacking into a project-local <code class="language-plaintext highlighter-rouge">zig-pkg/</code> directory named by the hash, and recursing into that dependency’s own <code class="language-plaintext highlighter-rouge">.zon</code>. A canonical recompressed tarball is kept in the <a href="https://ziglang.org/download/0.16.0/release-notes.html#Fetch-Packages-Into-Project-Local-Directory">global cache</a> so a second project using the same hash doesn’t redownload. <code class="language-plaintext highlighter-rouge">zig fetch &lt;url&gt;</code> on its own downloads one URL and prints the computed hash for pasting into a manifest, without recursing. Supported schemes are <code class="language-plaintext highlighter-rouge">http(s)://</code> for tarballs and git bundles, <code class="language-plaintext highlighter-rouge">git+http(s)://</code> for the git smart protocol with a ref in the fragment, <code class="language-plaintext highlighter-rouge">file://</code>, and bare paths.</p>

<p><code class="language-plaintext highlighter-rouge">zig build</code> compiles <code class="language-plaintext highlighter-rouge">build.zig</code> together with every dependency’s <code class="language-plaintext highlighter-rouge">build.zig</code> into a single executable and runs it, so a transitive dependency’s build code is linked into the same program as yours rather than invoked as a separate process per package. Calling into a dependency from your build script <a href="https://github.com/ziglang/zig/blob/master/lib/std/Build.zig">runs that dependency’s build script</a>, which can do the same for its own dependencies.</p>

<p>Every <code class="language-plaintext highlighter-rouge">.zon</code> in the tree pins its direct dependencies by content hash, so there’s no version resolution step and the full tree is determined once the root’s hashes are fixed. Two dependencies that pin different hashes of the same upstream both get fetched and both exist, with deduplication by hash only. Version selection using the <code class="language-plaintext highlighter-rouge">fingerprint</code> field is <a href="https://github.com/ziglang/zig/issues/14288">planned</a> and <a href="https://kristoff.it/blog/zig-self-hosted-now-what/">expected to look like Go’s minimal version selection</a>, but as of 0.16 the only cross-tree check is that the <a href="https://ziglang.org/download/0.16.0/release-notes.html#Fetch-Packages-Into-Project-Local-Directory">same fingerprint and version can’t appear with two different hashes</a>.</p>

<p>0.14 introduced the <a href="https://ziglang.org/download/0.14.0/release-notes.html">current hash format and the <code class="language-plaintext highlighter-rouge">fingerprint</code> field</a>. 0.16 moved extracted packages from the global cache into a <a href="https://ziglang.org/devlog/2026/#2026-02-06">project-local <code class="language-plaintext highlighter-rouge">zig-pkg/</code> directory</a> and added <a href="https://ziglang.org/download/0.16.0/release-notes.html#Ability-to-Override-Packages-Locally"><code class="language-plaintext highlighter-rouge">--fork</code></a> for overriding a package across the whole tree. On 30 June the fetching logic, HTTP client, TLS, and git protocol were <a href="https://ziglang.org/devlog/2026/?2026-06-30#2026-06-30">moved out of the compiler binary</a> into the build-system process, shipped as source.</p>

<h2 id="categorisation">Categorisation</h2>

<h3 id="categorisation-client">Client</h3>

<p><strong>Resolution algorithm.</strong> None: each manifest pins exact content hashes with no version ranges. Closest to the <a href="/2025/12/29/categorizing-package-manager-clients.html#resolution-algorithms">explicit-dependencies bucket</a> alongside Nix and Guix, though without a <a href="/2026/07/07/content-addressing-in-package-managers.html">content-addressed store</a> underneath, and with minimal version selection <a href="https://github.com/ziglang/zig/issues/14288">planned</a>.</p>

<p><strong>Lockfile.</strong> Manifest is the lock: <code class="language-plaintext highlighter-rouge">build.zig.zon</code> pins content hashes for every direct dependency and each dependency does the same for its own, with no separate file. Doesn’t fit any of the <a href="/2025/12/29/categorizing-package-manager-clients.html#lockfiles-and-reproducibility">existing buckets</a> cleanly: reproducible without a lockfile like Go, but by pinning hashes in the manifest rather than by deterministic resolution.</p>

<p><strong>Build hooks.</strong> Allowed: <code class="language-plaintext highlighter-rouge">build.zig</code> is a Zig program compiled and run at build time with every dependency’s <code class="language-plaintext highlighter-rouge">build.zig</code> compiled into the same process, comparable to Cargo’s <code class="language-plaintext highlighter-rouge">build.rs</code>, Gradle, or Swift’s <code class="language-plaintext highlighter-rouge">Package.swift</code>.</p>

<p><strong>Tuesday test.</strong> <a href="/2026/04/15/the-tuesday-test.html">Fails</a>, i.e. the install can observe inputs the manifest doesn’t declare: <code class="language-plaintext highlighter-rouge">build.zig</code> is Turing-complete and runs for every dependency, though <code class="language-plaintext highlighter-rouge">.zon</code> on its own is pure data and would pass alone.</p>

<p><strong>Manifest format.</strong> Custom for metadata (<code class="language-plaintext highlighter-rouge">.zon</code>), host language for build logic (<code class="language-plaintext highlighter-rouge">build.zig</code>), where most tools in the <a href="/2025/12/29/categorizing-package-manager-clients.html#manifest-format">categorisation</a> put both in one file. Being a bespoke format rather than JSON or TOML, <code class="language-plaintext highlighter-rouge">.zon</code> needs its own parser in <a href="/2026/01/29/zig-and-the-mxn-supply-chain-problem.html">every cross-ecosystem tool</a> that reads manifests, <a href="https://ecosyste.ms">ecosyste.ms</a> and <a href="https://github.com/git-pkgs">git-pkgs</a> included.</p>

<h3 id="categorisation-registry">Registry</h3>

<p><strong>Architecture.</strong> <a href="/2025/12/29/categorizing-package-registries.html#registry-architecture">Source host as registry</a>, alongside Go modules, Deno, and Carthage, with no central index and packages served as tarballs or git refs at whatever URL the author chose.</p>

<p><strong>Review model.</strong> None: whoever controls the URL controls what’s served at it.</p>

<p><strong>Namespacing.</strong> URL-based for location, but identity is the <code class="language-plaintext highlighter-rouge">name</code> plus a self-generated 64-bit <code class="language-plaintext highlighter-rouge">fingerprint</code>, which doesn’t match any bucket in the <a href="/2025/12/29/categorizing-package-registries.html#namespacing">namespacing categorisation</a>.</p>

<p><strong>Distribution model.</strong> Source only, compiled on the client.</p>

<p><strong>Ecosystem scope.</strong> Language-specific, though Zig’s C interop and bundled <code class="language-plaintext highlighter-rouge">zig cc</code> mean C and C++ libraries are commonly packaged as Zig dependencies too.</p>

<p><strong>Version retention.</strong> Delegated to the source host, so a GitHub release stays available until the repo owner deletes it or the account goes away, and there’s no yank mechanism.</p>

<p><strong>Size.</strong> Without a central index there’s no canonical count, though <a href="https://zigistry.dev/">Zigistry</a> scrapes GitHub for repos with a <code class="language-plaintext highlighter-rouge">build.zig.zon</code> and indexes what it finds.</p>

<p><strong>Mirroring.</strong> Trivial in principle since the client accepts any URL and verifies by hash, though each <code class="language-plaintext highlighter-rouge">.zon</code> names one URL per dependency, so if that URL goes dead the depender has to edit their manifest.</p>

<h2 id="governance">Governance</h2>

<p>The <a href="https://ziglang.org/zsf/">Zig Software Foundation</a> governs the language and the tool, funded by donations, but package hosting and name authority are outside its remit since there’s no registry for it to operate.</p>

<p>Name ownership rests on the <code class="language-plaintext highlighter-rouge">fingerprint</code>, and the <a href="https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md">manifest documentation</a> says a fork of a maintained project should regenerate it, calling a fork that keeps the upstream’s value “hostile, attempting to take control over the original project’s identity”. Nothing enforces that and there’s no arbiter to appeal to. Availability, account recovery, DMCA takedowns and abuse handling all fall to whichever source host the URL points at, which in practice is overwhelmingly GitHub. Removal of a malicious version is something only the repo owner or the source host can do, and neither has any way to signal downstream <code class="language-plaintext highlighter-rouge">.zon</code> files that already pin its hash. Changes to the package manager itself go through the <a href="https://github.com/ziglang/zig/issues">Zig issue tracker</a>.</p>

<p>Loris Cro of the ZSF wrote in 2022 that <a href="https://kristoff.it/blog/zig-self-hosted-now-what/">“we don’t plan to create an official package index”</a>, so the absence of a <a href="/2025/12/22/package-registries-are-governance-as-a-service.html">governance provider</a> is a design choice rather than a gap.</p>

<h2 id="comparisons">Comparisons</h2>

<p>The closest existing designs are Go modules and pre-JSR Deno (when it imported dependencies directly by URL): URL-addressed source, no publish step, no central registry, integrity by content hash. Go’s <code class="language-plaintext highlighter-rouge">go.sum</code> and checksum database are the nearest equivalent to <code class="language-plaintext highlighter-rouge">.zon</code> hashes, though Go verifies against a public log rather than only what the depending project committed. Swift Package Manager is close on the client side, with a host-language build manifest and git-URL dependencies, but resolves version ranges against tags rather than pinning a single hash. Bazel’s <a href="https://bazel.build/rules/lib/repo/http#http_archive"><code class="language-plaintext highlighter-rouge">http_archive</code></a> takes a URL list plus a <code class="language-plaintext highlighter-rouge">sha256</code> with the hash as the identity and the URLs as interchangeable mirrors, then evaluates a hermetic Starlark <code class="language-plaintext highlighter-rouge">BUILD</code> file rather than a Turing-complete build program.</p>

<p>The <code class="language-plaintext highlighter-rouge">fingerprint</code> is what none of those have: a package identity that survives a URL change, whereas in Go the identity is the module path and moving a repo creates a new module. The <a href="https://ziglang.org/download/0.16.0/release-notes.html#Ability-to-Override-Packages-Locally"><code class="language-plaintext highlighter-rouge">--fork</code> flag</a> uses it to substitute a local checkout for every occurrence of a package across the whole dependency tree, regardless of which URLs the intermediate <code class="language-plaintext highlighter-rouge">.zon</code> files pinned. It is a CLI argument only, not a manifest field, and the release notes call that <a href="https://ziglang.org/download/0.16.0/release-notes.html#Ability-to-Override-Packages-Locally">“appropriately ephemeral”</a>.</p>

<p>There is no <code class="language-plaintext highlighter-rouge">build.zig.zon</code> field equivalent to Cargo’s <a href="https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html"><code class="language-plaintext highlighter-rouge">[patch]</code></a>, Go’s <a href="https://go.dev/ref/mod#go-mod-file-replace"><code class="language-plaintext highlighter-rouge">replace</code></a>, npm’s <code class="language-plaintext highlighter-rouge">overrides</code> or pub’s <code class="language-plaintext highlighter-rouge">dependency_overrides</code>, and <a href="https://github.com/ziglang/zig/issues/14288">no update subcommand</a> for direct dependencies either.</p>

<p>With exact-hash pins at every level and no ranges to re-resolve, a fix in a low-level dependency reaches a project only when every intermediate <code class="language-plaintext highlighter-rouge">.zon</code> on the path has been re-released with the new hash. Until then the root’s options are forking the intermediates, vendoring a patched <code class="language-plaintext highlighter-rouge">zig-pkg/</code> into source control, or passing <code class="language-plaintext highlighter-rouge">--fork</code> on every build. The flag isn’t recorded anywhere, so the same commit builds a different tree depending on whether it was passed, in a design where the committed manifest is otherwise a complete lock. Once <a href="https://github.com/ziglang/zig/issues/14288">minimal version selection</a> lands, listing the fixed version in the root <code class="language-plaintext highlighter-rouge">.zon</code> would be enough, as it is in <code class="language-plaintext highlighter-rouge">go.mod</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">.paths</code> filter is close to the <code class="language-plaintext highlighter-rouge">files</code> array in <code class="language-plaintext highlighter-rouge">package.json</code> or <code class="language-plaintext highlighter-rouge">include</code> in <code class="language-plaintext highlighter-rouge">Cargo.toml</code>, except that the hash is computed after the filter is applied rather than over a tarball the author uploaded, so the inclusion list is part of the identity rather than a packaging convenience.</p>

<h2 id="threat-model">Threat model</h2>

<h3 id="client">Client</h3>

<p><strong>Code execution at install time.</strong> <code class="language-plaintext highlighter-rouge">zig build --fetch</code> runs nothing from the package: it downloads each URL, verifies the hash, extracts and parses each dependency’s <code class="language-plaintext highlighter-rouge">.zon</code> as data, and recurses without compiling <code class="language-plaintext highlighter-rouge">build.zig</code>. <code class="language-plaintext highlighter-rouge">zig build</code> compiles your <code class="language-plaintext highlighter-rouge">build.zig</code> and every dependency’s <code class="language-plaintext highlighter-rouge">build.zig</code> into <a href="https://github.com/ziglang/zig/blob/master/lib/std/Build.zig">one executable</a> and runs it, so a dependency’s build script executes with your privileges the first time you build. There’s no flag to disable that, since the build script is how a dependency exposes its artifacts. A <code class="language-plaintext highlighter-rouge">.lazy = true</code> dependency isn’t fetched, and its <code class="language-plaintext highlighter-rouge">build.zig</code> isn’t compiled in, until <a href="https://github.com/ziglang/zig/blob/master/lib/std/Build.zig"><code class="language-plaintext highlighter-rouge">b.lazyDependency</code></a> is called for it.</p>

<p><strong>Code execution before install time.</strong> Since <code class="language-plaintext highlighter-rouge">.zon</code> is data, parsing a manifest and running <code class="language-plaintext highlighter-rouge">zig fetch</code> on an untrusted URL are safe modulo the extractor, but <code class="language-plaintext highlighter-rouge">zig build</code> on an untrusted checkout compiles and runs <code class="language-plaintext highlighter-rouge">build.zig</code>, and so do <code class="language-plaintext highlighter-rouge">zig build -h</code> and <code class="language-plaintext highlighter-rouge">--list-steps</code>, since the step list comes from executing the configure phase.</p>

<p><strong>Lockfile guarantees by design.</strong> The manifest pins 200 bits of SHA-256 for every direct dependency, and each of those pins its own, so the whole tree is fixed by content and every install is a locked install. The guarantee is per-project and starts at first fetch: the hash protects a project from the URL’s contents changing later, but there’s no independent record of what the URL served at the moment the hash was first written down. Go’s <a href="https://go.dev/ref/mod#checksum-database">checksum database</a> is the counterexample, a public log that records what each module path served so a substituted first fetch would disagree with everyone else’s. Without an equivalent, two Zig projects adding the same URL a week apart can pin different hashes with nothing to flag the discrepancy. Plain <code class="language-plaintext highlighter-rouge">http://</code> URLs are accepted without a warning, and an already-unpacked <code class="language-plaintext highlighter-rouge">zig-pkg/&lt;hash&gt;/</code> directory is not re-hashed before reuse.</p>

<p><strong>Package name identity.</strong> <code class="language-plaintext highlighter-rouge">name</code> is a bare Zig identifier, at most 32 bytes, case-sensitive, ASCII only, so the usual normalisation gaps don’t apply. Deduplication and <code class="language-plaintext highlighter-rouge">--fork</code> match on <code class="language-plaintext highlighter-rouge">name</code> plus <code class="language-plaintext highlighter-rouge">fingerprint</code>, so two packages sharing a <code class="language-plaintext highlighter-rouge">name</code> with different fingerprints coexist in the same tree.</p>

<p><strong>Resolution across multiple sources.</strong> Each dependency entry names exactly one URL, so there’s no source ordering to get wrong and the dependency-confusion pattern doesn’t apply. The related failure mode is a stale URL: the <code class="language-plaintext highlighter-rouge">.zon</code> records where the author fetched from, and if that host disappears there’s no automatic fallback even though any mirror serving the same bytes would satisfy the hash.</p>

<h3 id="registry">Registry</h3>

<p><strong>Namespace allocation.</strong> Fingerprints are self-asserted rather than allocated, so a fork can write the upstream’s value into its own manifest verbatim and be matched as the same package. Typosquatting on the URL is the source host’s problem.</p>

<p><strong>Maintainer lifecycle.</strong> Maintainership is control of the source repo, so adding a maintainer means adding a GitHub collaborator, and nothing signals that to anyone with the package in their tree. Account recovery is whatever the source host provides, and a lapsed custom domain in a <code class="language-plaintext highlighter-rouge">.url</code> is a straightforward takeover, mitigated only by the hash pin in existing dependers’ manifests.</p>

<p><strong>Immutability of published versions.</strong> The hash in a depender’s <code class="language-plaintext highlighter-rouge">.zon</code> guarantees they get the bytes they locked but not their continued availability, and the tag or release the URL points at can be replaced with something else that new dependers will then pin. Git tags can be <a href="https://arxiv.org/abs/2606.31354">force-pushed</a>, GitHub release assets can be replaced, and there’s no transparency log recording what a URL served when.</p>

<p><strong>Provenance from source to artifact.</strong> The artifact is the source, so there’s no build step to attest. The hash proves you got what the depender pinned but not who wrote it or whether the URL’s repo matches the tarball, and trusted publishing would need somewhere to publish to.</p>

<p><strong>The minimum viable publish credential.</strong> This is whatever the source host issues: for GitHub, a token or SSH key with push access to the repo, with scoping, expiry and 2FA on GitHub’s side and no separate publish surface for a Zig-specific credential to protect.</p>

<p><strong>Blast radius and detection.</strong> Anomaly detection on publish, malicious-version markers that clients check, and central audit logging are all registry functions that nothing here stands in for. A compromised release spreads only as fast as dependers update their <code class="language-plaintext highlighter-rouge">.hash</code> fields, which is slower than a registry ecosystem with floating ranges but also means there’s no single place to pull it from once it’s out, and detection falls to whoever’s reading diffs on the source host.</p>

<h3 id="the-tools-own-supply-chain">The tool’s own supply chain</h3>

<p>The Zig compiler’s <a href="https://github.com/ziglang/zig/blob/master/build.zig.zon">own <code class="language-plaintext highlighter-rouge">build.zig.zon</code></a> declares zero external dependencies, with the only entries being <code class="language-plaintext highlighter-rouge">.path</code> references into the source tree. The toolchain components it needs are <a href="https://ziglang.org/download/0.16.0/release-notes.html#Toolchain">vendored into the release tarballs</a> rather than fetched. Since the <a href="https://ziglang.org/devlog/2026/?2026-06-30#2026-06-30">30 June change</a> the HTTP client, TLS stack, git protocol and decompressors ship as Zig source rather than compiled into the <code class="language-plaintext highlighter-rouge">zig</code> binary, which the devlog notes runs in <code class="language-plaintext highlighter-rouge">ReleaseSafe</code> so safety checks stay on for the networking code.</p>

<h2 id="summary">Summary</h2>

<p>Closest relatives are Go modules and pre-JSR Deno on the registry side, Swift Package Manager on the client side, and Bazel’s <code class="language-plaintext highlighter-rouge">http_archive</code> on the fetch model. Treating the content hash as the identity, with the URL just one place to fetch it, is the right way round, and <code class="language-plaintext highlighter-rouge">.zon</code> being pure data means the dependency graph can be read without executing anything. The compiler declaring zero external dependencies of its own is a supply chain position I’d like to see more of, and moving the networking stack out into <code class="language-plaintext highlighter-rouge">ReleaseSafe</code> source that can be patched without rebuilding the compiler is a good recent change.</p>

<p><code class="language-plaintext highlighter-rouge">.zon</code> being a bespoke format rather than JSON or TOML means I’ve had to write a parser for it in both <a href="https://ecosyste.ms">ecosyste.ms</a> and <a href="https://github.com/git-pkgs">git-pkgs</a>, and so will everyone else building cross-ecosystem tooling. The tool ends up on the wrong side of the <a href="/2026/04/15/the-tuesday-test.html">Tuesday test</a> because <code class="language-plaintext highlighter-rouge">build.zig</code> is a full Zig program, when the pure-data <code class="language-plaintext highlighter-rouge">.zon</code> had it most of the way to passing. The governance functions a registry would provide fall to GitHub instead, which is now the arbiter of availability, account recovery and abuse handling for Zig packages without either party having chosen that.</p>

<p>Nothing in the manifest can override a transitive dependency, so if a low-level package ships a security fix and an intermediate hasn’t re-released, the options are forking the intermediate or passing <code class="language-plaintext highlighter-rouge">--fork</code> on every build, and the flag leaves nothing in the repo to record that it was passed. Cargo, Go, npm, pub, Mix and Bazel all have a manifest field for this, and exact-hash pinning makes its absence matter more here than it would with version ranges.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="unboxed" /><category term="zig" /><summary type="html"><![CDATA[Zig's package manager: mechanics, categorisation, governance, threat model.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Content addressing in package managers</title><link href="https://nesbitt.io/2026/07/07/content-addressing-in-package-managers.html" rel="alternate" type="text/html" title="Content addressing in package managers" /><published>2026-07-07T10:00:00+00:00</published><updated>2026-07-07T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/07/content-addressing-in-package-managers</id><content type="html" xml:base="https://nesbitt.io/2026/07/07/content-addressing-in-package-managers.html"><![CDATA[<p>Content addressing identifies a piece of data by a cryptographic hash of its contents rather than by a name or a location. Two copies of the same bytes get the same identifier wherever they came from, a single changed bit produces a completely different one, and because the identifier is derived from the data itself it works as a lookup key and an integrity check at the same time.</p>

<p>I keep running into content addressing in package management, usually solving one problem at one layer, and I’ve been collecting the places it appears. A package manager content-addressed end to end would have its registry index, its package metadata, every released artifact, and the files inside them all named by hash, so any of it could be fetched from any source and verified locally. That idea has been floating around for as long as I’ve been paying attention, and several of the systems below get a long way towards it without any one of them yet covering the whole stack. This is a survey of the pieces as they exist today: what gets hashed, and what each hash is used for.</p>

<h3 id="what-gets-hashed">What gets hashed</h3>

<p><strong>Individual files within a package.</strong> <a href="https://pnpm.io/symlinked-node-modules-structure">pnpm</a> keeps a shared on-disk store, normally one per filesystem, where every file from every installed package lives once, named by its content hash, and each project’s <code class="language-plaintext highlighter-rouge">node_modules</code> is built from hard links into it, so ten projects depending on the same version of React share one physical copy of every file. OCI registries store each image layer as a blob keyed by its sha256 digest and reference layers from manifests by digest, so pushing an image whose base layers already exist on the registry transfers only the new ones. The same blob store and manifest format now carries Helm charts, WebAssembly modules, and arbitrary files via <a href="https://oras.land/">ORAS</a>, which makes an OCI registry a general-purpose content-addressed package store as much as a container host.</p>

<p>The <a href="https://github.com/bazelbuild/remote-apis">remote execution API</a> used by Bazel and Buck stores build inputs and outputs in a content-addressable store of individual file blobs, and <a href="https://ostreedev.github.io/ostree/">ostree</a> does the same for whole operating system trees underneath Flatpak and rpm-ostree. <code class="language-plaintext highlighter-rouge">nix store optimise</code> walks an existing Nix store and replaces duplicate files across unrelated packages with hard links to a single copy.</p>

<p><strong>A single distributed artifact.</strong> One tarball, gem, wheel, jar, or crate, one hash, recorded in almost every lockfile format: npm and Yarn’s <code class="language-plaintext highlighter-rouge">integrity</code> field in <a href="https://www.w3.org/TR/SRI/">SRI</a> notation, Cargo.lock’s <code class="language-plaintext highlighter-rouge">checksum</code>, <a href="https://go.dev/ref/mod#go-sum-files"><code class="language-plaintext highlighter-rouge">go.sum</code></a>, composer.lock, mix.lock, poetry.lock, uv.lock, pubspec.lock, and Bundler’s <a href="https://bundler.io/blog/2025/01/29/checksums.html"><code class="language-plaintext highlighter-rouge">CHECKSUMS</code></a> block in Gemfile.lock. Registries publish the same hash alongside the artifact so a client with no prior state has a reference to check against: PyPI’s <code class="language-plaintext highlighter-rouge">#sha256=</code> URL fragments and <a href="https://peps.python.org/pep-0691/">PEP 691</a> <code class="language-plaintext highlighter-rouge">hashes</code> objects, Maven’s <code class="language-plaintext highlighter-rouge">.sha1</code> and <code class="language-plaintext highlighter-rouge">.sha256</code> sidecar files, the crates.io index <code class="language-plaintext highlighter-rouge">cksum</code>, npm’s <code class="language-plaintext highlighter-rouge">dist.integrity</code>, Hex’s inner and outer checksums, conda’s per-package <code class="language-plaintext highlighter-rouge">sha256</code> in each channel’s <code class="language-plaintext highlighter-rouge">repodata.json</code>.</p>

<p>The same level also covers Homebrew’s per-bottle <code class="language-plaintext highlighter-rouge">sha256</code>, the hash listed for each <code class="language-plaintext highlighter-rouge">.deb</code> in a Debian <code class="language-plaintext highlighter-rouge">Packages</code> file, <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource Integrity</a> attributes on <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> tags, and the expected hash passed to Nix’s <code class="language-plaintext highlighter-rouge">fetchurl</code> and other fixed-output derivations. The digest of an OCI image manifest sits here too, with the difference that it is routinely used as the fetch key, as in <code class="language-plaintext highlighter-rouge">alpine@sha256:…</code>, as well as a check after fetching by tag.</p>

<p><strong>A resolved dependency graph.</strong> A lockfile taken as a whole records one selected dependency resolution across many packages: the exact set of artifacts one project resolved to at one moment. Nix’s <a href="https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake-lock"><code class="language-plaintext highlighter-rouge">flake.lock</code></a> is the most explicit form, recording a content hash for every flake input transitively. A <code class="language-plaintext highlighter-rouge">Cargo.lock</code>, <code class="language-plaintext highlighter-rouge">Gemfile.lock</code>, or <code class="language-plaintext highlighter-rouge">package-lock.json</code> does the same in its own format, and committing one to a git repository makes the resolution itself content-addressed by way of the containing commit, which <code class="language-plaintext highlighter-rouge">npm ci</code>, <code class="language-plaintext highlighter-rouge">bundle install --frozen</code>, and <code class="language-plaintext highlighter-rouge">cargo build --locked</code> then treat as authoritative.</p>

<p><strong>Every version of one package.</strong> A hash over the complete list of releases for a single package name at a point in time snapshots that package’s publishing history. A <a href="https://theupdateframework.io/">TUF</a> repository can express this as a delegated targets file scoped to one package, listing each release with its artifact hash, with the file’s own hash recorded in the snapshot metadata above it. <a href="https://gittuf.dev/">gittuf</a> records every state a git reference has held as a signed, hash-chained reference state log, which for a repository that tags its releases is a verifiable history of every version it has published.</p>

<p>Hackage’s append-only <code class="language-plaintext highlighter-rouge">01-index.tar</code> accumulates per-package entries that build the same record over time. The crates.io git index keeps one file per crate listing all published versions, so the git blob hash of that file is also such a snapshot, though that is a side effect of the index living in a git repository; since the <a href="https://doc.rust-lang.org/cargo/reference/registry-index.html#sparse-protocol">sparse protocol</a> became Cargo’s default in 1.70 most clients fetch per-crate JSON over HTTP and never see the git form.</p>

<p><strong>Every package in a registry.</strong> A hash over the full index of a registry at a point in time pins the complete set of packages and versions available from it. Debian’s <a href="https://wiki.debian.org/DebianRepository/Format"><code class="language-plaintext highlighter-rouge">Release</code></a> file contains hashes of the <code class="language-plaintext highlighter-rouge">Packages</code> files for each component and architecture, which in turn list a hash for every <code class="language-plaintext highlighter-rouge">.deb</code>, giving a three-level chain from one signed file down to every byte in the archive; Alpine’s <code class="language-plaintext highlighter-rouge">APKINDEX</code> and RPM’s <code class="language-plaintext highlighter-rouge">repomd.xml</code> follow the same pattern.</p>

<p>Registries that keep their index in git get this property from git itself: the HEAD commit of the crates.io index, a Homebrew tap, <a href="https://github.com/NixOS/nixpkgs">nixpkgs</a>, or a Guix channel is a content-addressed snapshot of every package definition it holds. TUF expresses the same thing as <code class="language-plaintext highlighter-rouge">snapshot.json</code> and <code class="language-plaintext highlighter-rouge">timestamp.json</code> metadata, and Hackage layers the <a href="https://hackage.haskell.org/package/hackage-security">hackage-security</a> TUF implementation over its index tarball.</p>

<p><strong>Metadata about artifacts.</strong> Signatures, attestations, and provenance documents are themselves data with their own hashes, and usually contain hashes of the things they describe. The simplest form is a signature authenticating one object: Maven <code class="language-plaintext highlighter-rouge">.asc</code> files are detached signatures over individual artifacts, Debian’s <code class="language-plaintext highlighter-rouge">Release.gpg</code> and inline <code class="language-plaintext highlighter-rouge">InRelease</code> sign the repository metadata, and RPM carries signatures and digests inside the package header.</p>

<p>Metadata documents can also reference each other by hash to form their own chain, as TUF’s timestamp metadata authenticates the current snapshot metadata, which pins the version and optionally the hash of each targets metadata file, which in turn records hashes of the target artifacts, with root metadata separately distributing the trusted keys for each role; an OCI image manifest references its config JSON by digest alongside the layer digests, and <code class="language-plaintext highlighter-rouge">go.sum</code> records a hash of each module’s <code class="language-plaintext highlighter-rouge">go.mod</code> file separately from the module zip. <a href="https://docs.sigstore.dev/">Sigstore</a> bundles, npm <a href="https://docs.npmjs.com/generating-provenance-statements">provenance</a>, and PyPI <a href="https://peps.python.org/pep-0740/">PEP 740</a> attestations sit in both halves at once, being signatures over an artifact digest that are then themselves logged by hash in the <a href="https://docs.sigstore.dev/logging/overview/">Rekor</a> transparency log.</p>

<p><strong>The inputs to a build.</strong> A hash can be taken over a build recipe and everything it depends on before any output exists, so the identifier names the description of the work and is known ahead of the result. <a href="https://nixos.org/">Nix</a> input-addressed derivations compute the store path from a hash of the <code class="language-plaintext highlighter-rouge">.drv</code> file and the store paths of every input, and <a href="https://guix.gnu.org/">Guix</a> works the same way.</p>

<p><a href="https://bazel.build/remote/caching">Bazel</a> and Buck compute an action digest from the command line, the environment, and the content hashes of every input file, and use it as the lookup key into a remote execution cache; Docker BuildKit keys its layer cache on each instruction plus the hashes of the files it can see; and ccache and sccache hash the compiler binary, flags, and preprocessed source to decide whether a translation unit needs recompiling. Nix’s experimental <a href="https://nix.dev/manual/nix/latest/development/experimental-features#xp-feature-ca-derivations">content-addressed derivations</a> flip the relationship and derive the store path from a hash of the build output, which puts the hash itself back at the single-artifact level while keeping the input-keyed workflow around it.</p>

<p><a href="https://www.softwareheritage.org/">Software Heritage</a> applies content addressing across most of these levels at once for archived source code: a <a href="https://www.swhid.org/">SWHID</a> can name an individual file (<code class="language-plaintext highlighter-rouge">swh:1:cnt:</code>), a directory tree (<code class="language-plaintext highlighter-rouge">swh:1:dir:</code>), a revision (<code class="language-plaintext highlighter-rouge">swh:1:rev:</code>), a release (<code class="language-plaintext highlighter-rouge">swh:1:rel:</code>), or a snapshot of every reference in a repository (<code class="language-plaintext highlighter-rouge">swh:1:snp:</code>), each computed as a hash over the level below. The scheme is git-compatible by construction, so a git commit SHA-1 and the corresponding <code class="language-plaintext highlighter-rouge">swh:1:rev:</code> identifier encode the same digest.</p>

<h3 id="same-contents-different-hash">Same contents, different hash</h3>

<p>Hashing an archive as one opaque blob is simple but makes the result sensitive to bytes that have no effect on the code inside: gzip compression level and OS flag, tar member ordering, embedded mtimes, even the timezone of the machine that built it, so two archives with identical extracted contents can hash differently. A change to <code class="language-plaintext highlighter-rouge">git archive</code>’s gzip output in early 2023 <a href="https://github.blog/2023-02-21-update-on-the-future-stability-of-source-code-archives-and-hashes/">broke every tool</a> that had pinned a hash of a GitHub source tarball, including Bazel rules, Homebrew formulae, and Nix fixed-output fetchers, while consumers hashing the extracted tree were unaffected; GitHub reverted the change within days.</p>

<p>One answer is to skip the container and hash the file tree directly: Go’s <a href="https://pkg.go.dev/golang.org/x/mod/sumdb/dirhash"><code class="language-plaintext highlighter-rouge">dirhash</code></a> computes SHA-256 of each file’s contents, sorts the resulting <code class="language-plaintext highlighter-rouge">hash  path</code> lines, and records the hash of that listing as the <code class="language-plaintext highlighter-rouge">h1:</code> value in <code class="language-plaintext highlighter-rouge">go.sum</code>, so the zip’s encoding, compression, and per-file metadata never enter into it. A second <code class="language-plaintext highlighter-rouge">h1:</code> over just <code class="language-plaintext highlighter-rouge">go.mod</code> lets dependency resolution be verified without fetching the rest of the module. Zig’s <a href="https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md"><code class="language-plaintext highlighter-rouge">.hash</code></a> is computed over the extracted tree filtered to the package’s own <code class="language-plaintext highlighter-rouge">.paths</code> inclusion list, so the archive encoding never enters into it and the author decides which files count.</p>

<p>The other answer is to define a canonical archive format and hash that: Nix’s <a href="https://nix.dev/manual/nix/latest/store/file-system-object/content-address.html#serial-nix-archive">NAR</a> serialisation strips timestamps, normalises permissions, and sorts entries deterministically, so the same tree always produces the same archive bytes. Debian’s <a href="https://reproducible-builds.org/">reproducible builds</a> effort and the work on Python wheel reproducibility approach the same problem from the build side, making toolchains produce byte-identical output for identical source. Output-addressed schemes such as Nix’s CA derivations benefit most from getting this right, since identical inputs producing different output bytes means different store paths and no cache reuse across independent builds.</p>

<p>Which hash function is used, and whether the identifier records that choice, determines how hard it is to migrate later. Most of the schemes above encode the algorithm or a hash-scheme version in the identifier: SRI strings carry a <code class="language-plaintext highlighter-rouge">sha384-</code> or <code class="language-plaintext highlighter-rouge">sha512-</code> prefix, OCI digests are written <code class="language-plaintext highlighter-rouge">sha256:…</code>, <code class="language-plaintext highlighter-rouge">go.sum</code> lines begin with <code class="language-plaintext highlighter-rouge">h1:</code>, and IPFS CIDs embed a <a href="https://multiformats.io/multihash/">multihash</a> code that self-describes both the function and the digest length. SWHIDs record a scheme version and object type in the prefix but currently fix the digest itself to git-compatible SHA-1.</p>

<p>Git originally hard-coded SHA-1 with no algorithm marker anywhere in the object format, and the <a href="https://git-scm.com/docs/hash-function-transition">transition to SHA-256</a>, proposed in 2017, has taken nearly a decade of design and interop work; SHA-256 repositories can now be created, but SHA-1 remains the default and interoperability between the two object formats is still incomplete. npm’s older <code class="language-plaintext highlighter-rouge">dist.shasum</code> field is bare hex SHA-1 with the same limitation, which is why packuments now carry both it and an SRI-format <code class="language-plaintext highlighter-rouge">dist.integrity</code> field.</p>

<h3 id="what-the-hash-is-used-for">What the hash is used for</h3>

<p><strong>Verification.</strong> Comparing fetched bytes against a hash recorded earlier, either in a lockfile written at resolution time or in metadata served by the registry, so a tampered or corrupted download is rejected before it is unpacked. This is the most widespread use and the only one most lockfile formats implement, and because the check sits after the fetch it can be, and in several clients is, made optional or advisory.</p>

<p><strong>Addressing.</strong> Using the hash as the key the client fetches by, so the request names the expected content and a wrong response is detectable on arrival without consulting anything else. Pulling an OCI image by digest and resolving an IPFS CID are both this. Unlike verification, addressing is hard to make optional, since a client that asked for a hash and accepted non-matching bytes would not have the object it requested.</p>

<p><strong>Deduplication.</strong> Using the hash as a storage or transfer key so identical content is held or sent once. pnpm’s file store and OCI’s blob layer both share content across packages this way, the first within one machine and the second across every client of a registry.</p>

<p><strong>Build caching.</strong> Treating a hash over the inputs to a computation as a cache key, so a build step whose inputs match a previous run can take that run’s output instead of repeating the work. Nix derivations and Bazel actions are the two implementations most package-adjacent tooling borrows from.</p>

<p><strong>Tamper evidence.</strong> Placing hashes as nodes in a Merkle structure so that a small trusted root, such as a signed tree head or a TUF timestamp file, is enough to authenticate everything it transitively references. Go’s checksum database and Debian’s <code class="language-plaintext highlighter-rouge">Release</code> chain both let untrusted mirrors serve the full archive on the strength of one signature at the top.</p>

<h3 id="trust-and-transport">Trust and transport</h3>

<p>One consequence of addressing and tamper evidence taken together is that the question of where bytes come from can be separated from the question of whether to trust them: once a name has been bound to a hash by whatever authority you accept for that, the bytes can arrive from anywhere and be checked locally. Go’s module system is designed around this split, with <code class="language-plaintext highlighter-rouge">GOPROXY</code> free to point at any proxy or chain of proxies, public or private, while the checksum database at <code class="language-plaintext highlighter-rouge">sum.golang.org</code> independently confirms that what arrived matches what everyone else has seen for that module version.</p>

<p>Nix can pull from any binary cache added as a <a href="https://nix.dev/manual/nix/latest/command-ref/conf-file#conf-substituters">substituter</a>, with each substituted path verified against a signature from a cache key the user has trusted, and fixed-output or content-addressed paths additionally checked against the content hash encoded in the path itself. Debian and its derivatives have run on third-party mirrors for over two decades on the strength of one signed <code class="language-plaintext highlighter-rouge">Release</code> file per suite chaining down to every <code class="language-plaintext highlighter-rouge">.deb</code> in the archive. An OCI pull pinned to a digest can be served by any registry mirror or pull-through cache, since the manifest and every layer are checked against their digests regardless of which host the bytes came from. Zig’s <a href="https://github.com/ziglang/zig/blob/master/doc/build.zig.zon.md">manifest documentation</a> states the same principle directly, calling the <code class="language-plaintext highlighter-rouge">.url</code> “just one of many possible mirrors” for the content the <code class="language-plaintext highlighter-rouge">.hash</code> identifies.</p>

<p>All of which assumes the binding from name to hash has already been established somewhere trusted, because content addressing does not decide which hash a name should point to in the first place. Which account is permitted to publish <code class="language-plaintext highlighter-rouge">rails 8.0.0</code>, and which artifact that version resolves to, is a governance question answered by the registry’s account system and the maintainers who hold the credentials, and a hash can only confirm that you received what they published.</p>

<p>Attacks on package supply chains tend to target that binding rather than the bytes themselves: account takeover and malicious maintainer handover publish hostile artifacts under an existing trusted name, typosquatting registers a name close enough to be mistaken for one, and dependency confusion arranges for a different registry to win resolution for the same name; in each case the hostile artifact’s hash is then recorded and propagated as faithfully as any other. In <a href="https://en.wikipedia.org/wiki/Zooko%27s_triangle">Zooko’s</a> terms, a hash is secure and globally consistent and means nothing to a person, while a name is the part a person can actually use and is only as trustworthy as whoever controls it; binding one to the other is the job that registries, maintainer accounts, signatures, and transparency logs are there to do.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="security" /><category term="reference" /><summary type="html"><![CDATA[Names are for humans, hashes are for everything else]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">This Week in Package Management: 4 July 2026</title><link href="https://nesbitt.io/2026/07/04/this-week-in-package-management.html" rel="alternate" type="text/html" title="This Week in Package Management: 4 July 2026" /><published>2026-07-04T10:00:00+00:00</published><updated>2026-07-04T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/04/this-week-in-package-management</id><content type="html" xml:base="https://nesbitt.io/2026/07/04/this-week-in-package-management.html"><![CDATA[<p>Week seven of the roundup, built from the <a href="https://github.com/ecosyste-ms/package-managers-opml">package manager OPML feed collection</a> and whatever I’ve posted or boosted on <a href="https://mastodon.social/@andrewnez">Mastodon</a>.</p>

<h2 id="releases">Releases</h2>

<p><a href="https://github.com/hexpm/hex/releases/tag/v2.5.0">Hex 2.5.0</a> adds organisation-defined dependency policies: an organisation publishes a named policy through its repository, a project opts in via <code class="language-plaintext highlighter-rouge">HEX_POLICY</code> or the <code class="language-plaintext highlighter-rouge">:hex</code> block in <code class="language-plaintext highlighter-rouge">mix.exs</code>, and resolution then filters out versions that carry an advisory above a given severity, are retired for listed reasons, or are newer than a release-age threshold.</p>

<p><a href="https://github.com/conan-io/conan/releases/tag/2.30.0">Conan 2.30.0</a> adds SPDX expression support to its SBOM generation, a <code class="language-plaintext highlighter-rouge">conf=~</code> unset operator, and IntelCC support in the Meson, Autotools and Premake toolchains.</p>

<p><a href="https://github.com/astral-sh/uv/releases/tag/0.11.25">uv 0.11.25</a> hardens tar handling against parser differentials via an updated <code class="language-plaintext highlighter-rouge">astral-tokio-tar</code>, so uv may now reject malformed source distributions it previously accepted, and writes a full lockfile into tool receipts. <a href="https://github.com/astral-sh/uv/releases/tag/0.11.26">0.11.26</a> followed with resolver performance work reusing state across PubGrub iterations.</p>

<p><a href="https://github.com/prefix-dev/pixi/releases/tag/v0.72.0">pixi 0.72.0</a> adds inline package manifests, so a git dependency without a Pixi Build manifest of its own can have build backend metadata set directly in the consuming project’s <code class="language-plaintext highlighter-rouge">dependencies</code> table.</p>

<p><a href="https://github.com/jdx/mise/releases/tag/v2026.7.0">mise 2026.7.0</a> adds <code class="language-plaintext highlighter-rouge">mise install --monorepo</code> with a tri-state <code class="language-plaintext highlighter-rouge">[monorepo].lockfile</code> setting for a single root lockfile across config roots, a per-tool <code class="language-plaintext highlighter-rouge">github_attestations</code> option to disable GitHub Artifact Attestation verification for one tool, and enables shell expansion in <code class="language-plaintext highlighter-rouge">env</code> by default.</p>

<p><a href="https://blog.rust-lang.org/2026/06/30/Rust-1.96.1/">Rust 1.96.1</a> is a point release fixing missing retries and timeouts in Cargo’s HTTP client and patching three libssh2 CVEs in the copy compiled into Cargo.</p>

<p><a href="https://github.com/npm/cli/releases/tag/v12.0.0-pre.2">npm 12.0.0-pre.2</a> graduates the linked install strategy from experimental to stable and moves install-script approval under a dedicated <code class="language-plaintext highlighter-rouge">npm install-scripts</code> namespace.</p>

<p><a href="https://github.com/Homebrew/homebrew-brew-vulns/releases/tag/v0.4.0">brew-vulns 0.4.0</a>, which I maintain, uses the <code class="language-plaintext highlighter-rouge">resolves</code> field on formula patches added in Homebrew 6.0.4 to suppress false positives where the formula already carries a patch for the CVE, and records those as <code class="language-plaintext highlighter-rouge">analysis.state = resolved</code> in CycloneDX output.</p>

<p>Also out: <a href="https://github.com/Homebrew/brew/releases/tag/6.0.6">Homebrew 6.0.6</a>, <a href="https://github.com/sbt/sbt/releases/tag/v2.0.1">sbt 2.0.1</a>, <a href="https://github.com/rust-lang/cargo/releases/tag/0.97.2">Cargo 0.97.2</a>, <a href="https://github.com/denoland/deno/releases/tag/v2.9.1">Deno 2.9.1</a>, <a href="https://github.com/podman-container-tools/podman/releases/tag/v5.8.4">Podman 5.8.4</a>, <a href="https://github.com/goharbor/harbor/releases/tag/v2.15.2">Harbor 2.15.2</a>, <a href="https://github.com/dependabot/dependabot-core/releases/tag/v0.384.0">Dependabot Core 0.384.0</a>, <a href="https://diffoscope.org/news/diffoscope-323-released/">diffoscope 323</a>.</p>

<h2 id="security">Security</h2>

<p><a href="https://github.com/composer/composer/releases/tag/2.10.2">Composer 2.10.2</a> and LTS <a href="https://github.com/composer/composer/releases/tag/2.2.29">2.2.29</a> fix path traversal via package <code class="language-plaintext highlighter-rouge">bin</code> entries (<a href="https://github.com/advisories/GHSA-gjfg-22fp-rrxx">GHSA-gjfg-22fp-rrxx</a>), credential leakage in verbose output (<a href="https://github.com/advisories/GHSA-g6xq-892h-64w3">GHSA-g6xq-892h-64w3</a>), missing package name validation (<a href="https://github.com/advisories/GHSA-499r-g7pc-vmp9">GHSA-499r-g7pc-vmp9</a>), and three further hardening changes around HTTP redirects, phar metadata and JSON error output.</p>

<p><a href="https://guix.gnu.org/blog/2026/guix-substitute-pull-vulnerabilities/">Guix substitute and pull vulnerabilities</a> fixes four issues in the daemon and client: substitute extraction ran before hash verification so a malicious substitute could write arbitrary files as the daemon user, a substitute for one store item could be accepted in place of another, <code class="language-plaintext highlighter-rouge">file://</code> substitute URIs could leak daemon-readable files via error backtraces, and a malicious channel could overwrite user files via <code class="language-plaintext highlighter-rouge">guix pull</code> or <code class="language-plaintext highlighter-rouge">time-machine</code>. CVEs pending.</p>

<p><a href="https://blog.python.org/2026/06/mitigated-api-bypass-for-download-metadata-python-dot-org/">Mitigated: API bypass for download metadata on python.org</a> reports an authentication bypass in the python.org API where mixed handling of guest and API-key authentication could grant administrative privileges over download metadata; the fix separates the two modes and adds URL validation. Guix has a <a href="https://codeberg.org/guix/guix/issues/6992">tracking issue</a> on the implications for its importer.</p>

<p><a href="https://thehackernews.com/2026/06/hijacked-npm-and-go-packages-use-vs.html">Hijacked npm and Go packages use VS Code tasks to deploy infostealer</a> covers a campaign in which sixteen hijacked Go modules and a set of npm packages shipped fake font files and VS Code task definitions that fetched a Python infostealer.</p>

<h2 id="articles">Articles</h2>

<p><a href="https://fzakaria.com/2026/06/25/guixpkgs-every-guix-package-as-a-nix-flake">GuixPkgs: every Guix package as a Nix flake</a> (Farid Zakaria) imports the full Guix package set into Nix by building a primitive that exposes each Guix derivation through a flake.</p>

<p><a href="https://research.empiricalsecurity.com/research/the-vulnerability-identity-crisis">The Vulnerability Identity Crisis</a> (Jay Jacobs and Art Manion, Empirical Security) argues a vulnerability should be defined as a disposition, a structural property comprising a fault, conditions for exploitation, and a resulting security failure, rather than by surface characteristics.</p>

<p><a href="https://daniel.haxx.se/blog/2026/06/29/do-excellent-vulnerability-reports/">Do excellent vulnerability reports</a> (Daniel Stenberg) is a guide for reporters on what an open source project needs from a vulnerability report to act on it.</p>

<p><a href="https://ziglang.org/devlog/2026/?2026-06-30#2026-06-30">All Package Management Functionality Moved from Compiler to Build System</a> (Andrew Kelley, Zig devlog) shifts package fetching, the HTTP client, TLS, the Git protocol and archive handling out of the <code class="language-plaintext highlighter-rouge">zig</code> binary into the build-side maker process, shipped as source. Networking now runs in <code class="language-plaintext highlighter-rouge">ReleaseSafe</code> with safety checks on and can be patched without rebuilding the compiler.</p>

<h2 id="papers">Papers</h2>

<p><a href="https://arxiv.org/abs/2606.31354">Mutating the “Immutable”: A Large-Scale Study of Git Tag Alterations</a> (Rapaport et al., arXiv) analyses 328.4 million repositories from Software Heritage and finds 10.2 million tags that were deleted or force-pushed after creation, undermining the assumption that a tag is a stable reference for reproducible builds.</p>

<p><a href="https://arxiv.org/abs/2606.29785">Uncovering Similar but Different Packages in PyPI and Potential Security Threats</a> (Park et al., arXiv) measures package replication on PyPI, where a package duplicates most of an existing codebase, and traces how replicated packages propagate known vulnerabilities and provide cover for malicious variants.</p>

<p><a href="https://arxiv.org/abs/2607.02357">Cloak and Detonate: Scanner Evasion and Dynamic Detection of Agent Skill Malware</a> (Ji et al., arXiv) builds payload transforms that evade all eight tested agent-skill scanners at over 90% via self-extracting packing, and proposes SkillDetonate, a sandboxed runtime auditor that catches 97% by observing behaviour at OS boundaries instead of inspecting at install time.</p>

<p><a href="https://arxiv.org/abs/2607.02059">File-Level Copying Is an Implicit Dependency in Open Source</a> (He et al., arXiv) mines 690,500 copy events from World of Code and frames copied files as dependencies that lack the four signals a package manager provides: provenance, maintenance, security and compliance.</p>

<h2 id="elsewhere">Elsewhere</h2>

<p>The PSF has <a href="https://pyfound.blogspot.com/2026/06/packaging-council-inaugural-election.html">announced the inaugural Python Packaging Council election</a>, establishing a technical decision-making body for Python packaging specs that coordinates across tools and teams.</p>

<p>The <a href="https://opensourcepledge.com/">Open Source Pledge</a> has a redesigned site marking $7 million paid to maintainers by member companies so far.</p>

<p><a href="https://onceamaintainer.substack.com/p/once-a-maintainer-mike-dalessio">Once a Maintainer: Mike Dalessio</a> is an interview with the Nokogiri maintainer on the security work that comes with maintaining a widely depended-on gem.</p>

<h2 id="git-pkgs">git-pkgs</h2>

<p>I tagged fifteen repos this week:</p>

<ul>
  <li><a href="https://github.com/git-pkgs/git-pkgs/releases/tag/v0.17.0">git-pkgs v0.17.0</a></li>
  <li><a href="https://github.com/git-pkgs/brief/releases/tag/v0.9.2">brief v0.9.2</a></li>
  <li><a href="https://github.com/git-pkgs/managers/releases/tag/v0.10.1">managers v0.10.1</a></li>
  <li><a href="https://github.com/git-pkgs/forge/releases/tag/v0.6.0">forge v0.6.0</a></li>
  <li><a href="https://github.com/git-pkgs/registries/releases/tag/v0.6.2">registries v0.6.2</a></li>
  <li><a href="https://github.com/git-pkgs/proxy/releases/tag/v0.5.1">proxy v0.5.1</a></li>
  <li><a href="https://github.com/git-pkgs/manifests/releases/tag/v0.6.0">manifests v0.6.0</a></li>
  <li><a href="https://github.com/git-pkgs/enrichment/releases/tag/v0.4.1">enrichment v0.4.1</a></li>
  <li><a href="https://github.com/git-pkgs/resolve/releases/tag/v0.2.2">resolve v0.2.2</a></li>
  <li><a href="https://github.com/git-pkgs/outline/releases/tag/v0.1.6">outline v0.1.6</a></li>
  <li><a href="https://github.com/git-pkgs/purl/releases/tag/v0.1.13">purl v0.1.13</a></li>
  <li><a href="https://github.com/git-pkgs/vulns/releases/tag/v0.1.6">vulns v0.1.6</a></li>
  <li><a href="https://github.com/git-pkgs/pom/releases/tag/v0.1.5">pom v0.1.5</a></li>
  <li><a href="https://github.com/git-pkgs/capcheck/releases/tag/v0.1.1">capcheck v0.1.1</a></li>
  <li><a href="https://github.com/git-pkgs/distill/releases/tag/v0.1.0">distill v0.1.0</a></li>
</ul>

<p>Send links for next week to <a href="https://mastodon.social/@andrewnez">@andrewnez@mastodon.social</a>.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="weekly" /><summary type="html"><![CDATA[Releases, advisories, and articles from across the package management world]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The CRA is not about open source</title><link href="https://nesbitt.io/2026/07/01/the-cra-is-not-about-open-source.html" rel="alternate" type="text/html" title="The CRA is not about open source" /><published>2026-07-01T10:00:00+00:00</published><updated>2026-07-01T10:00:00+00:00</updated><id>https://nesbitt.io/2026/07/01/the-cra-is-not-about-open-source</id><content type="html" xml:base="https://nesbitt.io/2026/07/01/the-cra-is-not-about-open-source.html"><![CDATA[<p>At <a href="https://fosdem.org/2026/">FOSDEM</a> in February and again at <a href="https://www.unopensource.org">UN Open Source Week</a> last week, the <a href="https://eur-lex.europa.eu/eli/reg/2024/2847/oj">Cyber Resilience Act</a> was the answer on offer whenever anyone asked what governments are doing about open source security, and the foundations and corporate advocates presenting it framed it as good news for open source. It is the largest piece of software legislation the EU has passed, the open source community spent two years lobbying over its text, and its main obligations come into force in December 2027.</p>

<p>It is also not about open source. Where open source appears in the text, it appears as an exemption, a component risk, a compliance contact, or a paperwork trail.</p>

<p>The CRA is a product-safety law in the <a href="https://single-market-economy.ec.europa.eu/single-market/ce-marking_en">CE-mark</a> family, the same regime that already covers toys, radio equipment and machinery: rules a manufacturer must meet before selling a product in the EU, with a conformity mark at the end. The CRA extends that to anything with software in it. In <a href="/2026/06/30/taking-roads-and-bridges-literally.html">the previous post</a> I described this class of regulation as requiring haulage firms to certify which bridges their lorries crossed while employing no bridge inspectors, and the CRA is the regulation most often held up as the counterexample.</p>

<h2 id="scope">Scope</h2>

<p>The things the CRA defines are products, the companies that make or sell them, and components. There is nothing in it corresponding to a project, a package, a registry, or a maintainer. Open source is visible in the regulation only as a property a component of a regulated product can have, so in the CRA’s terms there is no OpenSSL, only products that incorporate OpenSSL.</p>

<p>Non-commercial open source is carved out of scope, which took a lot of lobbying and has been treated since as a win for the community. But exclusion from a product regulation is not the same as benefiting from it. A project outside the CRA’s scope is in the same position as one the drafters had never heard of.</p>

<p>The regulation establishes no fund and no inspection or maintenance body, and nothing in it requires a manufacturer to contribute upstream. The exemption is the edge of what product law can reach: software nobody is selling offers nothing for a market regulation to attach to.</p>

<p>The phrase “supply chain” appears in the text in its product-law sense, meaning the chain of companies who put a good on the market and can each be held responsible for it. That is a different thing from the transitive dependency graph the same phrase means in open source, and reading the regulation as if it governs the second because the vocabulary overlaps is a category error.</p>

<h2 id="dependencies">Dependencies</h2>

<p>The clause that bears most directly on open source dependencies requires a manufacturer to exercise due diligence when pulling in third-party components, open source included. A manufacturer discharging that duty has a menu of options:</p>

<ul>
  <li>vendor the dependency and patch it in-house</li>
  <li>replace it with a different dependency</li>
  <li>buy a commercially supported distribution that comes with its own paperwork</li>
  <li>run a scanner against the dependency tree and file the output</li>
</ul>

<p>None of those routes money to the upstream author, because the rational response to liability for a component is to reduce exposure to it rather than to invest in it. A haulage firm told it is answerable for the state of every bridge on its route will plan routes with fewer bridges on them before it considers starting a bridge-repair division. When open source licence terms became a perceived corporate risk, the tooling that appeared was <a href="https://en.wikipedia.org/wiki/Software_composition_analysis">licence scanners</a> and component-replacement policies, and security liability is the same shape of problem.</p>

<p>The closest the text gets to upstream is a clause requiring a manufacturer who finds a vulnerability in a component to report it to the maintainer. That duty assumes a maintainer is still there to receive it, which is what a maintenance regime would ensure and a product regulation cannot.</p>

<h2 id="stewards">Stewards</h2>

<p>The steward category covers organisations that support open source intended for commercial use without themselves selling a product. A steward’s obligations are lighter than a manufacturer’s: have a security policy, cooperate with regulators on request, and handle vulnerability reports for the projects it looks after.</p>

<p>Those are the duties of an organisation that would otherwise be a gap in a manufacturer’s paperwork, which in bridge terms means requiring an existing maintenance crew to post a contact number so that a haulier filling in forms has someone to name. Nothing in the steward provisions hires that crew or appoints an inspector, and the lighter obligations are still organised around the manufacturer’s product.</p>

<p>For the steward category to route money to open source, manufacturers would need to prefer steward-backed components enough to pay for them. The regulation gives a manufacturer no reason to: its due diligence can be discharged by any of the routes in the previous section with no steward involved. Anyone who does want to buy component assurance can already get it from commercial redistributors. <a href="https://www.redhat.com/">Red Hat</a> has been selling supported open source for over two decades, and <a href="https://www.chainguard.dev/">Chainguard</a> sells hardened images with the SBOM and vulnerability handling needed for a CRA technical file. The nearest thing to a saleable steward output is Article 25’s voluntary security attestation, left to a future delegated act. Becoming a steward today adds obligations to a foundation without creating anything a manufacturer is obliged to buy from it.</p>

<h2 id="sboms">SBOMs</h2>

<p>The SBOM requirement asks for top-level dependencies “at the very least,” which makes the transitive graph optional, and the SBOM goes into the manufacturer’s technical file alongside the rest of the compliance paperwork. That file is held by the manufacturer and handed to a regulator on request, with no right of access for consumers, researchers, or maintainers. This is how <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:52022XC0629(04)">CE-mark technical files</a> have always worked: a private record between the manufacturer and whoever might one day audit it.</p>

<p>The SBOM that results is a liability artifact rather than a transparency instrument:</p>

<ul>
  <li>the maintainer of a library inside a CE-marked product does not learn from the CRA process that they are there</li>
  <li>a researcher cannot use CRA filings to map which commercial products depend on which open source projects</li>
  <li>a consumer comparing two products cannot see which has the better-maintained dependency tree</li>
</ul>

<p>The CRA will not, to take a concrete case, put a public SBOM for Windows anywhere a member of the public can read it. The <a href="https://infobridge.fhwa.dot.gov/">National Bridge Inventory</a> from the previous post is open to anyone, and because the bridge regime attaches to the function a structure performs, the privately held Ambassador Bridge is federally inspected regardless of title. A library inside thousands of CE-marked products gets no equivalent record, because the CRA’s trigger is commercial activity and its technical file is a sealed dossier for apportioning fault after an incident.</p>

<h2 id="implementation">Implementation</h2>

<p>The machinery the CRA sets running is all on the product side: the Commission will sort products into risk classes, <a href="https://www.enisa.europa.eu/">ENISA</a> will run a reporting platform for manufacturers to notify exploited vulnerabilities in their own products, member states will appoint enforcement bodies, and <a href="https://digital-strategy.ec.europa.eu/en/policies/cra-standardisation">CEN, CENELEC and ETSI</a> are writing the standards manufacturers will be assessed against. None of that touches anything upstream of the manufacturer, because the regulation defines nothing there.</p>

<p>The second-order effects that do reach open source are modest: SBOM tooling will mature because more manufacturers need to produce SBOMs, though <a href="https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity">Executive Order 14028</a> had already pushed that along. Exploited-vulnerability reporting will be quicker in the specific case of a manufacturer notifying ENISA about its own product. Neither of those is particular to open source or acts on the upstream project where a vulnerability originates.</p>

<p>When <a href="https://en.wikipedia.org/wiki/Log4Shell">Log4Shell</a> prompted a European policy response, the instrument that came back was product law, which is structurally incapable of acting on the infrastructure that prompted it. The CRA does open source little direct harm, and the exemptions the community fought for are about as good as exemptions get.</p>

<p>The cost is that “we passed the CRA” is now available as the answer when open source maintenance comes up: a regulation with no maintenance regime in it occupies the slot where one would go. The open source community made the CRA an open source story through its own lobbying, and two years on is still reading itself into a text whose subject is manufacturers and consumers.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="open-source" /><category term="sustainability" /><category term="policy" /><summary type="html"><![CDATA[The CRA created an open-source steward role, then left maintenance unfunded.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Taking Roads and Bridges literally</title><link href="https://nesbitt.io/2026/06/30/taking-roads-and-bridges-literally.html" rel="alternate" type="text/html" title="Taking Roads and Bridges literally" /><published>2026-06-30T10:00:00+00:00</published><updated>2026-06-30T10:00:00+00:00</updated><id>https://nesbitt.io/2026/06/30/taking-roads-and-bridges-literally</id><content type="html" xml:base="https://nesbitt.io/2026/06/30/taking-roads-and-bridges-literally.html"><![CDATA[<p>I spent last week at <a href="https://www.unopensource.org">UN Open Source Week</a>, where officials from a dozen governments stood up in turn and described open source as critical infrastructure. That framing has been the standard one since Nadia Eghbal’s <a href="https://www.fordfoundation.org/work/learning/research-reports/roads-and-bridges-the-unseen-labor-behind-our-digital-infrastructure/"><em>Roads and Bridges</em></a> report for the Ford Foundation in 2016, and after ten years it has finally reached the audience it describes. Sitting in a UN conference room full of people whose job is public infrastructure, I started wondering what it would mean to stop treating the title as a metaphor and look at how bridges are actually maintained.</p>

<p><em>Roads and Bridges</em> was pitched at the audience that was listening in 2016, technology companies and philanthropic foundations, neither of which maintains bridges: they drive over them for free, pay for them through tax, and leave the upkeep to the state. Spelling out the National Bridge Inspection Standards and the Highway Trust Fund to that room would have meant describing a government programme to a non-government audience, so the report stopped at the analogy and asked the people present to help.</p>

<p>The decade since produced GitHub Sponsors, Open Collective, corporate OSPO budgets, and foundation grants: voluntary contributions from users of the infrastructure. The civil-engineering equivalent is <a href="https://en.wikipedia.org/wiki/Adopt-a-Highway">Adopt-a-Highway</a>, where a local business pays to pick litter from a stretch of road and gets its name on a sign. Adopt-a-Highway is a real programme that does some good, and no state relies on it to keep a bridge from falling into a river.</p>

<h2 id="the-national-bridge-inspection-standards">The National Bridge Inspection Standards</h2>

<p>In the United States, every public-road bridge with a span longer than twenty feet is maintained under a federal regime that has been running since 1971, set up after the <a href="https://en.wikipedia.org/wiki/Silver_Bridge">Silver Bridge collapse</a> killed 46 people in 1967. The <a href="https://www.fhwa.dot.gov/bridge/nbis2022.cfm">National Bridge Inspection Standards</a> are codified at <a href="https://www.ecfr.gov/current/title-23/chapter-I/subchapter-G/part-650/subpart-C">23 CFR 650 Subpart C</a> and they specify, among other things:</p>

<ul>
  <li><strong>Mandatory inspection.</strong> Every bridge is inspected on a fixed cycle by a <a href="https://www.fhwa.dot.gov/bridge/nbis/index.cfm">certified inspector</a>, regardless of who owns it or whether the owner consents: 24 months is the default routine interval, with shorter cycles for higher-risk structures and longer risk-based intervals where the data supports it.</li>
  <li><strong>Component condition ratings.</strong> Each inspection produces a 0–9 rating for the deck, the superstructure, and the substructure. “Poor condition,” and the older <a href="https://highways.dot.gov/highway-history/structurally-deficient-bridge-meaning-term">“structurally deficient”</a> label it replaced in 2018, are defined terms tied to those numbers rather than a judgement call.</li>
  <li><strong>Load posting.</strong> A bridge that has degraded below its design load is legally <a href="https://www.fhwa.dot.gov/bridge/loadrating/">derated</a>: a sign goes up restricting the weight of vehicles that may cross, so the bridge stays open at reduced capacity instead of being either ignored or closed.</li>
  <li><strong>An inventory of record.</strong> Every bridge has a structure number in the <a href="https://www.fhwa.dot.gov/bridge/nbi.cfm">National Bridge Inventory</a>, with an owner of record, and the whole dataset is <a href="https://infobridge.fhwa.dot.gov/">public and queryable</a>.</li>
  <li><strong>Formula funding.</strong> Federal fuel tax flows into the <a href="https://www.fhwa.dot.gov/highwaytrustfund/">Highway Trust Fund</a> and is allocated to states by formula. The money is recurring and predictable, not at the discretion of whoever happens to be feeling generous this quarter.</li>
  <li><strong>Post-incident investigation.</strong> After a major collapse the NTSB produces a formal accident report with findings of probable cause and recommendations: <a href="https://www.ntsb.gov/investigations/AccidentReports/Reports/HAR0803.pdf">I-35W in Minneapolis</a>, <a href="https://www.ntsb.gov/investigations/Pages/HWY22MH003.aspx">Fern Hollow in Pittsburgh</a>.</li>
</ul>

<p>Recognising a structure as a public bridge switches all of that on automatically: the inventory entry, the inspection cycle, the condition ratings, the funding line, the closure authority. For an open source project, the same recognition currently switches on a speech.</p>

<p>The government action that has followed regulates the <em>consumers</em> of open source rather than the infrastructure. The EU <a href="https://eur-lex.europa.eu/eli/reg/2024/2847/oj">Cyber Resilience Act</a>, US <a href="https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity">Executive Order 14028</a>, and the various SBOM mandates put obligations on companies that ship products containing open source, which in bridge terms is requiring haulage firms to certify which bridges their trucks crossed while not employing any bridge inspectors.</p>

<h2 id="ownership">Ownership</h2>

<p>Critical open source projects generally have identifiable owners (named maintainers, a foundation, a company), and the <a href="https://opensourcesecurity.io/2025/08-oss-one-person/">single-maintainer statistic</a> is alarming because the number is one rather than because it is unknown. What they lack is a <em>state</em> owner of record, but the bridge regime does not require one. The <a href="https://en.wikipedia.org/wiki/Ambassador_Bridge">Ambassador Bridge</a> between Detroit and Windsor carries around a quarter of road-borne US–Canada merchandise trade, is owned by a <a href="https://www.ambassadorbridge.com/">private company</a>, and is still subject to federal inspection because the regime attaches to the function the structure performs, not to who holds the title. We know exactly who owns it, and we have left them to it.</p>

<h2 id="null-results">Null results</h2>

<p>The part of NBIS with the least open source equivalent is the most boring: an inspection that finds nothing wrong is filed with exactly the same formality as one that finds a crack in a girder. The inspector records a 9 for the deck and a 9 for the superstructure, dates and signs it, and it goes into the National Bridge Inventory next to all the others, with date of last inspection a queryable field in its own right.</p>

<p>In open source, a review that finds nothing wrong is almost never published, because the only outputs with anywhere to go are findings: issues, pull requests, CVEs. The absence of a CVE against a project cannot distinguish “someone competent checked this and it was fine” from “nobody has looked.” Bug bounties, CVE credit, advisory acknowledgements, and conference talks all pay out on findings, and time spent confirming that a library is sound earns nothing and leaves no trace, which makes a clean review <a href="/2026/06/18/open-source-vs-the-invisible-hand.html">economically irrational</a> to perform. A bridge inspector is paid the same for a 9 as for a 3, and that flat rate is what makes routine inspection a job rather than a hobby.</p>

<p>A certified inspector’s “found sound” is a legal record because the state has defined who counts as an inspector and what an inspection consists of, whereas a drive-by GitHub comment saying “I looked at this and it seems fine” is correctly discounted to zero because there is no way to know who looked, how hard, or at what. Without a definition of what an inspection is and who is qualified to perform one, a clean report has no weight, so nobody bothers to write one.</p>

<h2 id="procurement">Procurement</h2>

<p>Germany’s <a href="https://www.sovereign.tech/">Sovereign Tech Agency</a> is the clearest example I know of a government treating open source as infrastructure it has some responsibility for, and the telling detail is the legal form. It was set up in 2022 as the Sovereign Tech <em>Fund</em> and <a href="https://www.sovereign.tech/news/sovereign-tech-agency">renamed</a> in 2024 to the Sovereign Tech <em>Agency</em>, a shift from a thing that disburses money to a thing that does work, and it pays maintainers under <a href="https://www.sovereign.tech/programs/applications#what-format-does-the-investment-take">service contracts</a> rather than grants, because German public-spending law makes it difficult for the government to give money away without defined consideration in return.</p>

<p>That constraint is usually described as a bureaucratic obstacle, but it is doing the same job here that it does in civil engineering, where state departments of transportation <a href="https://www.fhwa.dot.gov/bridge/nbis/docs/contractingoutbridgeinspection.pdf">procure</a> inspection and repair from contractors under agreements specifying scope, deliverables, schedule, and acceptance criteria. A contract casts the maintainer as a professional the state is buying from because it needs the work done, which is a more accurate and more dignified relationship than the grantee-and-benefactor one that most open source funding has settled into.</p>

<p>The procuring agency writes its own scope of work (which project, which components, what methodology, what gets published and by when), so a review delivered against that scope to a public agency carries weight because of who commissioned it and what they specified, not because of any certification the reviewer holds. Engineering standards have generally propagated this way, from large public buyers writing down what they will pay for and contractors converging on whatever wins the work, which also avoids the argument about who has the authority to set norms for open source: a scope of work claims no authority over the ecosystem, only over the purchase order.</p>

<h2 id="scope-of-work">Scope of work</h2>

<p>A maintenance contract for an open source project has an obvious scope, and most of it is work maintainers already do unpaid:</p>

<ul>
  <li>Test coverage</li>
  <li>Documentation</li>
  <li>Performance benchmarks</li>
  <li>Security review</li>
  <li>Compatibility matrices against supported language and platform versions</li>
  <li>Dependency updates</li>
</ul>

<p>Every item on that list produces an artifact whether or not it turns up a problem, since coverage is a percentage regardless, a compatibility matrix has a value in every cell, and a security review conducted on a given date against a stated methodology is a record even when the finding count is zero. Those artifacts map onto component condition ratings, a dated profile across several dimensions instead of a <a href="/2026/05/09/the-mismeasure-of-open-source.html">single health score</a>, each of which can be re-measured on a cycle. A profile can also carry the equivalent of a load posting, where the report records that the project is sound within stated limits (“maintained for LTS platforms only,” “not hardened against untrusted input”) without having to choose between a clean bill and a warning label, which is a state open source currently has no way to express. Aggregate the reports across enough contracts and the result is an inventory with a structure number, an owner of record, a date of last inspection, and a condition by component.</p>

<p>Documentation goes stale, dependencies become outdated, and CI matrices stop matching the platforms people use, on a shorter cycle than concrete weathers because the environment a library runs in is other software that is also changing. The right instrument is a maintenance contract on a term, not a one-off grant or a bounty, and “we funded that project in 2024” should sound as odd as “we inspected that bridge in 2024.” The boundary of that contract is condition assessment and upkeep rather than feature development, so the state is buying “keep it standing and report its condition,” which answers in advance the worry that government money would put government hands on a project’s roadmap.</p>

<p>The institution the <em>Roads and Bridges</em> metaphor pointed at already exists, with fifty years of regulation, case law, and procurement practice behind it, and none of it was built by asking trucking companies to sponsor their favourite overpass.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="open-source" /><category term="maintainers" /><category term="sustainability" /><summary type="html"><![CDATA[Reflections on UN Open Source Week 2026]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Unbundling the standard library</title><link href="https://nesbitt.io/2026/06/29/unbundling-the-standard-library.html" rel="alternate" type="text/html" title="Unbundling the standard library" /><published>2026-06-29T10:00:00+00:00</published><updated>2026-06-29T10:00:00+00:00</updated><id>https://nesbitt.io/2026/06/29/unbundling-the-standard-library</id><content type="html" xml:base="https://nesbitt.io/2026/06/29/unbundling-the-standard-library.html"><![CDATA[<p>I got sent a link to <a href="https://github.com/DanBurton/composition/pull/6">a pull request</a> against <a href="https://github.com/DanBurton">Dan Burton</a>’s <code class="language-plaintext highlighter-rouge">composition</code>, a tiny Haskell package whose whole gimmick is that it depends on nothing, not even <code class="language-plaintext highlighter-rouge">base</code>. The upcoming GHC 10.2 breaks it: built-in names resolve through a real module, <code class="language-plaintext highlighter-rouge">GHC.Essentials</code>, which is in <code class="language-plaintext highlighter-rouge">base</code>, so from 10.2 every package picks up an implicit <code class="language-plaintext highlighter-rouge">base</code> dependency whether the cabal file declares one or not. The PR added a flag to opt out of that. Dan closed it and shipped a 2.0 that depends on <code class="language-plaintext highlighter-rouge">base</code> like everything else, on the grounds that the zero-dependency claim had always been a polite fiction about where the compiler’s wired-in identifiers came from.</p>

<p>The zero-dependency claim was always a question of where the line falls between the compiler and its standard library, and in GHC 10.2 that line moved by one module. That sent me off cataloguing the same boundary in other ecosystems, but the part I didn’t expect was how much the answer changes what a vulnerability advisory looks like.</p>

<h3 id="advisory-format">Advisory format</h3>

<p>The clearest case I found is <a href="https://bugs.ruby-lang.org/issues/20516">Ruby Bug #20516</a>: Ruby 3.3.2 shipped weeks after the fix for CVE-2024-35176 still bundling the vulnerable rexml 3.2.6, because the fix had gone out as a gem release and the copy in the Ruby tarball hadn’t been bumped to match. The advisory for that CVE is filed against the gem <code class="language-plaintext highlighter-rouge">rexml</code>, but the vulnerable copy on a 3.3.2 install is there because the Ruby distribution put it there rather than because any project resolved it, and an SBOM generated from a <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> won’t list it at all; the gap exists because rexml is on both sides of that line at once.</p>

<p>Seven years earlier, before Ruby’s gemification had reached WEBrick, <a href="https://www.ruby-lang.org/en/news/2017/09/14/webrick-basic-auth-escape-sequence-injection-cve-2017-10784/">CVE-2017-10784</a> was filed against three Ruby version ranges and fixed by three coordinated point releases, with advisory text reading “all users running an affected release should upgrade immediately.” The 2024 REXML CVEs (<a href="https://github.com/advisories/GHSA-vg3r-rm7w-2xgh">CVE-2024-35176</a> and five more through the year) were each filed against <code class="language-plaintext highlighter-rouge">pkg:gem/rexml</code> with a gem version range, each fixed by a gem release that users could apply without waiting for a Ruby point release, though the bundled copy still had to be bumped on each maintained Ruby branch.</p>

<p><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-24329">CVE-2023-24329</a> in Python’s <code class="language-plaintext highlighter-rouge">urllib.parse</code> lists five separate CPython version ranges across 3.7 through 3.11 and the fix was five point releases; <a href="https://nvd.nist.gov/vuln/detail/CVE-2022-35256">CVE-2022-35256</a> in Node’s <code class="language-plaintext highlighter-rouge">http</code> parser is three Node release lines and three point releases. Go’s vulnerability database has a pseudo-module literally called <a href="https://pkg.go.dev/vuln/GO-2023-1571"><code class="language-plaintext highlighter-rouge">stdlib</code></a> so that toolchain CVEs can be expressed in the OSV schema at all, with <code class="language-plaintext highlighter-rouge">govulncheck</code> matching those entries against the Go version that built the binary rather than anything in <code class="language-plaintext highlighter-rouge">go.sum</code>.</p>

<h3 id="bundled-vs-packaged">Bundled vs packaged</h3>

<p>In most ecosystems the standard library is underneath the package manager: bundled into the compiler or runtime, with no registry page, no independent version number, and no lockfile entry. Python’s stdlib ships in the CPython tarball and <code class="language-plaintext highlighter-rouge">urllib</code> has no canonical PyPI package; Rust’s <code class="language-plaintext highlighter-rouge">std</code> and <code class="language-plaintext highlighter-rouge">core</code> are sysroot crates whose names <a href="https://crates.io/api/v1/crates/std">404 on crates.io</a>; the Go module spec <a href="https://go.dev/ref/mod#module-path">treats</a> any import path without a dot in the first segment as standard library and never resolves it from the network; <code class="language-plaintext highlighter-rouge">dart:core</code> is a URI scheme rather than a pub package; <code class="language-plaintext highlighter-rouge">java.base</code> is in the JDK image and <code class="language-plaintext highlighter-rouge">fs</code> and <code class="language-plaintext highlighter-rouge">http</code> are compiled into the Node binary.</p>

<p>A smaller group put the standard library inside the package manager, with a registry page and a version that appears in the dependency graph. Haskell’s <code class="language-plaintext highlighter-rouge">base</code> is on Hackage, <code class="language-plaintext highlighter-rouge">kotlin-stdlib</code> and <code class="language-plaintext highlighter-rouge">scala-library</code> are Maven Central artifacts that turn up in Gradle lockfiles, <code class="language-plaintext highlighter-rouge">FSharp.Core</code> is an <a href="https://fsharp.github.io/fsharp-compiler-docs/fsharp-core-notes.html">implicit <code class="language-plaintext highlighter-rouge">&lt;PackageReference&gt;</code></a> on NuGet, <code class="language-plaintext highlighter-rouge">elm/core</code> is a required line in every <code class="language-plaintext highlighter-rouge">elm.json</code>, and Deno’s <a href="https://deno.com/blog/std-on-jsr"><code class="language-plaintext highlighter-rouge">@std/*</code> is on JSR</a> with nothing equivalent baked into the binary.</p>

<p>A third group run both at once, with the same module available from the runtime distribution and from the registry under the same name. Perl’s <a href="https://perldoc.perl.org/perlsource#cpan%2F">dual-life modules</a> ship in the perl tarball and on CPAN, Ruby has <a href="https://stdgems.org/">three tiers</a> of non-gemified libraries, default gems, and bundled gems, R splits base packages from CRAN-updatable recommended packages, and Julia bundles its stdlibs as packages with fixed UUIDs that the resolver can satisfy from the General registry instead.</p>

<h3 id="migrations">Migrations</h3>

<p>Modules have been moving through Ruby’s tiers from stdlib to default gem to bundled gem for a decade, with the pace picking up in <a href="https://www.ruby-lang.org/en/news/2024/12/25/ruby-3-4-0-released/">3.4</a> (<code class="language-plaintext highlighter-rouge">csv</code>, <code class="language-plaintext highlighter-rouge">base64</code>, <code class="language-plaintext highlighter-rouge">bigdecimal</code>, <code class="language-plaintext highlighter-rouge">drb</code> and nine others to bundled) and <a href="https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/">4.0</a> (<code class="language-plaintext highlighter-rouge">ostruct</code>, <code class="language-plaintext highlighter-rouge">logger</code>, <code class="language-plaintext highlighter-rouge">irb</code>, <code class="language-plaintext highlighter-rouge">rdoc</code>). <a href="https://peps.python.org/pep-0594/">PEP 594</a> removed nineteen modules from CPython in 3.13, and <a href="https://openjdk.org/jeps/320">JEP 320</a> removed the Java EE and CORBA modules from the JDK in 11 with replacements on Maven Central.</p>

<p>The <code class="language-plaintext highlighter-rouge">dart:html</code> family is deprecated in favour of <a href="https://dart.dev/interop/js-interop/package-web"><code class="language-plaintext highlighter-rouge">package:web</code></a> on pub, and a subset of Julia’s bundled stdlibs became <a href="https://julialang.org/blog/2023/04/julia-1.9-highlights/">upgradable from the registry</a> in 1.9. In GHC 9.10 <code class="language-plaintext highlighter-rouge">base</code> was split over a new <code class="language-plaintext highlighter-rouge">ghc-internal</code> package as the first step toward a <code class="language-plaintext highlighter-rouge">base</code> that can be <a href="https://discourse.haskell.org/t/what-are-the-next-steps-for-reinstallable-base/13319">reinstalled independently</a> of the compiler version, which is the work Dan’s <code class="language-plaintext highlighter-rouge">composition</code> PR was responding to. Racket decomposed its whole distribution in <a href="https://blog.racket-lang.org/2014/02/racket-v60.html">a single 2014 release</a> into roughly two hundred catalog packages over a minimal core.</p>

<p>ASP.NET Core, one layer up from .NET’s base class library, is the one case I found that moved from registry packages back into the runtime: 1.0 shipped as ordinary NuGet packages that every web project restored individually; by 2.1 it had been <a href="https://natemcmaster.com/blog/2018/08/29/netcore-primitives-2/">reconstituted as a shared framework</a> installed alongside the runtime, and from 3.0 the package references were replaced by a single <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/metapackage-app">implicit <code class="language-plaintext highlighter-rouge">&lt;FrameworkReference&gt;</code></a>. The package model had produced large restore graphs, slow installs, and diamond conflicts between package versions that application authors had no way to untangle, because the whole framework had gone into the resolver at once rather than one module at a time.</p>

<h3 id="stated-reasons">Stated reasons</h3>

<p>The reason given most often in the migration documents is decoupling release cadence: a fix in one stdlib module shouldn’t have to wait for the next language version, and a language version shouldn’t be forced because one module needs a patch. The Julia 1.9 post is direct about stdlibs having been stuck at whatever version shipped with the binary, with <code class="language-plaintext highlighter-rouge">DelimitedFiles</code> made the first upgradable stdlib so a fix could ship from the registry while the library stayed in the distribution. The Dart migration to <code class="language-plaintext highlighter-rouge">package:web</code> exists because the old <code class="language-plaintext highlighter-rouge">dart:html</code> bindings were hand-maintained and chronically behind the browser APIs, while the package is generated from Web IDL and can be released whenever the IDL changes. Reinstallable <code class="language-plaintext highlighter-rouge">base</code> is meant to fix the long-standing Haskell annoyance that an upper bound on <code class="language-plaintext highlighter-rouge">base</code> is really a constraint on which GHC you can use.</p>

<p>The <code class="language-plaintext highlighter-rouge">cpan/</code> and <code class="language-plaintext highlighter-rouge">dist/</code> directories in the perl source tree exist so a dual-life module can be released on CPAN ahead of the next perl. Ruby’s default gems are the same idea under a different name, and a fix to <code class="language-plaintext highlighter-rouge">json</code> or <code class="language-plaintext highlighter-rouge">psych</code> reaches users as a <code class="language-plaintext highlighter-rouge">gem update</code> rather than three coordinated point releases.</p>

<p>The second stated reason is getting code out of the core distribution so the core team is no longer responsible for it. PEP 594’s title is “Removing dead batteries” and the rationale is about modules with no maintainer, obsolete protocols and design decisions from the 1990s; the <a href="https://github.com/youknowone/python-deadlib"><code class="language-plaintext highlighter-rouge">standard-*</code> packages on PyPI</a> that resurrect <code class="language-plaintext highlighter-rouge">telnetlib</code> and <code class="language-plaintext highlighter-rouge">cgi</code> are a third-party project, not a CPython deliverable. JEP 320’s rationale is that the Java EE and CORBA APIs were never really part of SE and standalone Maven artifacts had existed for years, and Nim has a <a href="https://github.com/nim-lang/graveyard">graveyard org</a> for modules removed from its stdlib.</p>

<p>Ruby’s default-to-bundled promotion is driven by maintenance handoff rather than release cadence: a default gem still has ruby-core as the maintainer of record while a bundled gem is an ordinary gem that happens to be in the install, and if nobody maintains a bundled gem after promotion that’s no longer ruby-core’s problem.</p>

<p>The migration documents give cadence and maintenance burden as reasons and don’t mention advisory format, probably because the benefit goes to scanner authors and SBOM consumers rather than the language teams doing the work. A CVE moving from prose against a runtime version to a purl with a version range is what matters for the <a href="/2026/05/01/patching-and-forking-in-package-managers">SBOM and CRA questions</a> I keep coming back to, and the transitional state where a module ships in both the runtime and the registry is where it breaks.</p>

<h3 id="dual-life-modules">Dual-life modules</h3>

<p>The rexml gap was a release-process slip, but dual-shipping produces the same problem by design. <a href="https://github.com/dotnet/announcements/issues/315">CVE-2024-30105</a> in .NET’s <code class="language-plaintext highlighter-rouge">System.Text.Json</code> has a Microsoft advisory listing the NuGet package range (<code class="language-plaintext highlighter-rouge">&gt;= 7.0.0, &lt;= 8.0.3</code>) and the runtime range (“.NET 8.0.6 or earlier”) side by side, because the library is both an out-of-band NuGet package and part of the shared framework, and the remediation is effectively “upgrade whichever one you’re getting it from.”</p>

<p>Go’s <a href="https://pkg.go.dev/vuln/GO-2023-1571">GO-2023-1571</a> lists both the <code class="language-plaintext highlighter-rouge">stdlib</code> pseudo-module and <code class="language-plaintext highlighter-rouge">golang.org/x/net</code> because the same HPACK code is in both. JavaFX was removed from the JDK in 11 and now ships as <code class="language-plaintext highlighter-rouge">pkg:maven/org.openjfx/*</code> on Maven Central, so <a href="https://github.com/advisories/GHSA-47g3-mf24-6559">CVE-2024-20925</a> is fixed for Java 11+ users by bumping a Maven dependency while Java 8 users, where it’s still bundled, get a JDK update for the same bug, and both paths appear in the one advisory. Any tool consuming these needs a per-ecosystem, per-module record of whether the runtime copy or the registry copy wins at load time, and that mapping isn’t part of any common advisory format.</p>

<p>Perl has been dual-shipping long enough to have tooling for it: <a href="https://perldoc.perl.org/Module::CoreList"><code class="language-plaintext highlighter-rouge">Module::CoreList</code></a> records which version of every dual-life module shipped in every perl release back to 5.000, the <code class="language-plaintext highlighter-rouge">%upstream</code> hash records whether <code class="language-plaintext highlighter-rouge">blead</code> or CPAN holds the canonical copy of each, and the <code class="language-plaintext highlighter-rouge">corelist</code> command queries both from the shell. That’s a machine-readable record of which version shipped with a given perl and where fixes land first; which copy actually loads is still a runtime introspection question, but the release-to-bundled-version mapping is the part other ecosystems leave implicit. Ruby has <a href="https://stdgems.org/">stdgems.org</a>, which publishes the equivalent mapping as JSON, though it’s a third-party project rather than part of the toolchain, and for Python, Java, Julia and Dart I can’t find anything comparable.</p>

<p>The record that would make the transitional state safe for a scanner has roughly the same fields everywhere: for each runtime release and each module, the bundled version, the registry coordinate it corresponds to, which side is canonical upstream, whether the registry copy can replace the bundled one in place, and which copy the toolchain resolves to when both are installed. <code class="language-plaintext highlighter-rouge">Module::CoreList</code> and stdgems.org each cover roughly the first three; nothing I’ve found covers all five, and the identity layer underneath has the same gap, since purl has no syntax for “rexml as bundled in ruby 3.3.2” as distinct from <code class="language-plaintext highlighter-rouge">pkg:gem/rexml@3.2.6</code>, which is what the Go <code class="language-plaintext highlighter-rouge">stdlib</code> pseudo-module works around for one ecosystem. The migration is happening across a dozen ecosystems and the bookkeeping exists, partially, in two of them.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="security" /><category term="supply-chain" /><summary type="html"><![CDATA[Batteries no longer included, available separately on aisle four]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://nesbitt.io/images/boxes.png" /><media:content medium="image" url="https://nesbitt.io/images/boxes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>