<?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-09-06T14:33:09+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: 5 September 2026</title><link href="https://nesbitt.io/2026/09/05/this-week-in-package-management.html" rel="alternate" type="text/html" title="This Week in Package Management: 5 September 2026" /><published>2026-09-05T10:00:00+00:00</published><updated>2026-09-05T10:00:00+00:00</updated><id>https://nesbitt.io/2026/09/05/this-week-in-package-management</id><content type="html" xml:base="https://nesbitt.io/2026/09/05/this-week-in-package-management.html"><![CDATA[<p>Week sixteen 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>. I added Terraform, OpenTofu, Elm and PSResourceGet release feeds to the OPML this week.</p>

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

<p><a href="https://pnpm.io/blog/releases/12.1">pnpm 12.1</a> adds the workspace task scheduler to the Rust CLI, dispatching each package as soon as its dependencies finish where the previous scheduler waited for a whole topological batch, and moves login credentials into structured global config. <a href="https://github.com/pnpm/pnpm/releases/tag/v12.3.0">12.3</a> makes the global command shims native executables on every platform and extends the trust-policy flags to <code class="language-plaintext highlighter-rouge">pnpm remove</code> and <code class="language-plaintext highlighter-rouge">pnpm update</code>. On the 11.x line, <a href="https://pnpm.io/blog/releases/11.25">11.25</a> backports the task scheduler and adds <code class="language-plaintext highlighter-rouge">--resume-from</code> for recursive runs.</p>

<p><a href="https://blog.rust-lang.org/2026/09/01/Rustup-1.29.1/">rustup 1.29.1</a> checks for toolchain updates in parallel during <code class="language-plaintext highlighter-rouge">rustup update</code> and installs multiple components concurrently in <code class="language-plaintext highlighter-rouge">rustup component add</code>.</p>

<p><a href="https://github.com/conan-io/conan/releases/tag/2.32.0">Conan 2.32.0</a> adds LoongArch64 host detection, Xcode 26.6 and gcc 16.2 support, and lets <code class="language-plaintext highlighter-rouge">XcodeToolchain</code> accept arbitrary xcconfig build settings.</p>

<p><a href="https://github.com/pdm-project/pdm/releases/tag/2.29.0">PDM 2.29.0</a> exports editable local dependencies with relative paths in requirements files, and restricts locked packages to the platform they were resolved for when appending targets with <code class="language-plaintext highlighter-rouge">pdm lock --platform</code>.</p>

<p><a href="https://docs.zizmor.sh/release-notes/#1300">zizmor 1.30</a>, the GitHub Actions workflow auditor, adds a <code class="language-plaintext highlighter-rouge">self-repository</code> audit and expands pre-commit support.</p>

<p><a href="https://github.com/renovatebot/renovate/releases/tag/44.59.0">Renovate 44.59.0</a> adds a manager for Microsoft’s Agent Package Manager, and <a href="https://github.com/renovatebot/renovate/releases/tag/44.60.0">44.60.0</a> lets security-update PRs be rate-limited and stops the Go proxy datasource caching transient errors, a long-standing cause of flapping Go update PRs.</p>

<p>The Maven 3.8.x branch has <a href="https://github.com/apache/maven/releases/tag/archive%2Fmaven-3.8.x">reached end of life</a>; the final state is archived under a tag and the branch removed.</p>

<p>Also out:</p>

<ul>
  <li><a href="https://github.com/Homebrew/brew/releases/tag/6.0.22">Homebrew 6.0.22</a></li>
  <li><a href="https://github.com/canonical/snapd/releases/tag/2.77.1">snapd 2.77.1</a></li>
  <li><a href="https://blog.rubygems.org/2026/09/02/4.0.20-released.html">RubyGems and Bundler 4.0.20</a></li>
  <li><a href="https://github.com/oven-sh/bun/releases/tag/bun-v1.4.2">Bun 1.4.2</a></li>
  <li><a href="https://github.com/verdaccio/verdaccio/releases/tag/v6.10.2">Verdaccio 6.10.2</a></li>
  <li><a href="https://go.dev/doc/devel/release#go1.27.1">Go 1.27.1 and 1.26.8</a></li>
  <li><a href="https://blog.rust-lang.org/2026/09/03/Rust-1.98.1/">Rust 1.98.1</a></li>
  <li><a href="https://github.com/astral-sh/uv/releases/tag/0.12.10">uv 0.12.10</a></li>
  <li><a href="https://github.com/pypa/pipx/releases/tag/1.17.2">pipx 1.17.2</a></li>
  <li><a href="https://github.com/conda/conda/releases/tag/26.7.2">conda 26.7.2</a></li>
  <li><a href="https://github.com/prefix-dev/pixi/releases/tag/v0.79.0">Pixi 0.79.0</a></li>
  <li><a href="https://github.com/jdx/mise/releases/tag/v2026.9.1">mise 2026.9.1</a></li>
  <li><a href="https://github.com/dependabot/dependabot-core/releases/tag/v0.394.0">Dependabot Core 0.394.0</a></li>
  <li><a href="https://github.com/moby/moby/releases/tag/docker-v29.8.0">Docker Engine 29.8.0</a></li>
  <li><a href="https://github.com/hashicorp/terraform/releases/tag/v1.16.1">Terraform 1.16.1</a></li>
  <li><a href="https://github.com/helm/helm/releases/tag/v4.3.0-rc.1">Helm 4.3.0-rc.1</a> and <a href="https://github.com/helm/helm/releases/tag/v3.22.0-rc.1">3.22.0-rc.1</a></li>
  <li><a href="https://github.com/ocaml/opam/releases/tag/2.6.0-beta2">opam 2.6.0-beta2</a></li>
  <li><a href="https://github.com/Perl-Toolchain-Gang/CPAN-Meta/releases/tag/2.150015">CPAN-Meta 2.150015</a></li>
</ul>

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

<p><a href="https://github.com/podman-container-tools/podman/releases/tag/v6.1.1">Podman 6.1.1</a> fixes <a href="https://github.com/containers/podman/security/advisories/GHSA-hfg8-hc9c-6c3h">CVE-2026-17106</a>, a path traversal where a crafted tar archive could write outside the extraction directory via malicious links.</p>

<p><a href="https://github.com/python-poetry/poetry/releases/tag/2.4.2">Poetry 2.4.2</a> fixes three issues: installing an artifact absent from the lockfile when the source omits its hash, a path traversal when downloading from a compromised URL, and a path traversal in sdist extraction on Python 3.10.0-3.10.12 and 3.11.0-3.11.4.</p>

<p><a href="https://github.com/coder/coder/security/advisories/GHSA-vx42-ghc9-gw65">Coder disclosed</a> that an attacker gained access to its Cloudflare account and added their own IPs to the module registry pool, serving credential-stealing modules for about fourteen hours on 31 August. Fixed in 2.37.0 with backports to 2.36.4, 2.35.7 and 2.34.9. This is <a href="/2026/05/24/signing-is-for-the-bad-days.html">what artifact signing is for</a>: the tampered modules came from the real domain over valid TLS, so only a signature check against a key held outside the Cloudflare account would have flagged them.</p>

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

<p><a href="https://www.b-list.org/weblog/2022/may/13/boring-python-dependencies/">Boring Python: dependency management</a> (James Bennett) is a revised edition of the 2022 post: it now recommends PEP 751 lock files over pinned requirements files, allows uv or PDM for local development while keeping pip-with-hashes for production installs, and adds a section on three-day dependency cooldowns.</p>

<p><a href="https://fzakaria.com/2026/09/01/the-holy-grail-of-nixpkgs-version-ranges">The Holy Grail of Nixpkgs Version Ranges</a> (Farid Zakaria) adds version-range constraints to nixpkgs by pairing a 309,000-version index of historical nixpkgs revisions with the clingo Answer Set Programming solver, so a query like <code class="language-plaintext highlighter-rouge">python@&gt;=3.10</code> resolves to a concrete set of revisions. Mixing revisions can produce glibc symbol mismatches, which the solver constrains against using binary compatibility metadata.</p>

<p><a href="https://anil.recoil.org/notes/scrutineer-local-llm">Security scanning my own code with Scrutineer and local coding models</a> (Anil Madhavapeddy): notes on running Scrutineer, the code-scanning workflow tool that I wrote, against his own repos with a local GLM 5.3 model in place of a hosted one. He argues the triage decisions maintainers make on findings should themselves be tracked and fed into later scans across an organisation.</p>

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

<p><a href="https://arxiv.org/abs/2608.20678">The Software Supply Chain as a Market for Lemons: A Multivocal Review of Trust Signal Collapse</a> (Paramitha et al., arXiv) reviews 252 web sources and 870 Reddit threads on how practitioners pick dependencies: the cheap signals they rely on (stars, download counts, contributor activity) are now cheaper to fake than to earn, and the authors recommend costly signals such as cryptographic attestation as mandatory defaults.</p>

<p><a href="https://arxiv.org/abs/2608.29283">A Multi-Month Study of Git Commit Signing</a> (Shittu et al., arXiv): 22 CS students configured commit signing independently, used it across four coursework projects and a second device, then examined a repository seeded with anomalous commits; nearly all signed every commit but over a quarter missed the anomalies.</p>

<p><a href="https://arxiv.org/abs/2609.02591">AgOSS: A Dataset and Multi-Layer Characterization of Open-Source Agricultural Software</a> (Dudhaiya et al., arXiv) applies OpenSSF Scorecard, governance metrics, SBOM dependency analysis and KEV matching to 66 agricultural OSS repositories and non-agricultural controls; the agricultural projects score lower on raw Scorecard results but the gap disappears once project size and maturity are accounted for.</p>

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

<p>NYU Tandon has <a href="https://engineering.nyu.edu/news/nyu-tandon-launches-initiative-close-critical-gap-open-source-security-and-train-students-who">launched</a> the NYU Software Supply Chain Security Operations Center, led by Justin Cappos, embedding master’s students in open source projects for year-long security placements; the first cohort of 8-10 starts January 2027 with support from Google and DTCC.</p>

<p><a href="https://opensourcesecurity.io/2026/2026-08-erik-sta/">Sovereign Tech Agency with Erik Möller</a> (Josh Bressers, Open Source Security): an interview with the STA’s Director of Programs on the Sovereign Tech Fund, its maintainer fellowships, funding for standards-body participation, and the Sovereign Tech Resilience programme for security audits and post-quantum work.</p>

<p><a href="https://blog.yossarian.net/2026/08/31/Introducing-vulnbrocards-com">Introducing vulnbrocards.com</a> (William Woodruff): the vulnerability-triage brocards from earlier posts now each have a stable URL.</p>

<p><a href="https://fastwonderblog.com/2026/09/01/part-3-determining-value-and-viability-using-chaoss-practitioner-guides/">Determining Value and Viability Using CHAOSS Practitioner Guides</a> (Dawn Foster) is part three of the series, covering the Demonstrating Organizational Value and Assessing Viability guides for justifying open source contribution work to leadership and evaluating dependency risk.</p>

<p><a href="https://sunnydeveloper.com/tracking-trends-in-open-source-ai-policy/">Tracking Trends in Open Source AI Policy</a> (Emma Irwin) applies the CHAOSS AI Alignment working group’s use-policy-specificity metric to 39 open source project AI policies: 25 address code contributions, none address environmental impact, infrastructure strain or notetaker bots.</p>

<p>GitHub added a <a href="https://github.blog/changelog/2026-09-04-new-api-endpoint-provides-privacy-safe-star-history-data">star history REST endpoint</a> that returns timestamped aggregate star counts for a repository without listing the accounts that starred it.</p>

<p>The Cargo team put out a <a href="https://github.com/rust-lang/cargo/issues/14136#issuecomment-5519248462">call for testing</a> for <code class="language-plaintext highlighter-rouge">-Zchecksum-freshness</code>, which detects the need to rebuild from file content checksums, replacing the mtime check. The plan is to stabilise it as <code class="language-plaintext highlighter-rouge">build.fingerprint = "content"</code> in <code class="language-plaintext highlighter-rouge">.cargo/config.toml</code>.</p>

<p>CERN is <a href="https://www.phoronix.com/news/CERN-Goes-Debian-Leaving-RHEL">migrating its 2,200 accelerator-control machines</a> from RHEL to Debian 13 by the end of 2026. The announcement calls the <code class="language-plaintext highlighter-rouge">-march=x86-64-v2</code> compiler default forced obsolescence for older hardware and lists gaps in Debian’s standard tooling for automated package building and publishing.</p>

<p>The recording of <a href="https://youtu.be/J5XSQZNhwbc">Is the InnerSource Commons Good for Open Source?</a>, the FOSS Backstage 2026 talk Ben Nickolls and I gave analysing 800 InnerSource Commons member companies, is now up.</p>

<p>FOSDEM 2027 <a href="https://fosdem.org/2027/">will be held</a> on 30-31 January.</p>

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

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

<ul>
  <li><a href="https://github.com/git-pkgs/git-pkgs/releases/tag/v0.20.0">git-pkgs v0.20.0</a></li>
  <li><a href="https://github.com/git-pkgs/archives/releases/tag/v0.7.0">archives v0.7.0</a></li>
  <li><a href="https://github.com/git-pkgs/artifacts/releases/tag/v0.2.1">artifacts v0.2.1</a></li>
  <li><a href="https://github.com/git-pkgs/brief/releases/tag/v0.13.0">brief v0.13.0</a></li>
  <li><a href="https://github.com/git-pkgs/capcheck/releases/tag/v0.1.4">capcheck v0.1.4</a></li>
  <li><a href="https://github.com/git-pkgs/changelog/releases/tag/v0.2.1">changelog v0.2.1</a></li>
  <li><a href="https://github.com/git-pkgs/clone/releases/tag/v0.7.3">clone v0.7.3</a></li>
  <li><a href="https://github.com/git-pkgs/dependents/releases/tag/v0.2.0">dependents v0.2.0</a></li>
  <li><a href="https://github.com/git-pkgs/distill/releases/tag/v0.1.2">distill v0.1.2</a></li>
  <li><a href="https://github.com/git-pkgs/enrichment/releases/tag/v0.7.1">enrichment v0.7.1</a></li>
  <li><a href="https://github.com/git-pkgs/forge/releases/tag/v0.10.0">forge v0.10.0</a></li>
  <li><a href="https://github.com/git-pkgs/licenses/releases/tag/v0.7.0">licenses v0.7.0</a></li>
  <li><a href="https://github.com/git-pkgs/magic/releases/tag/v0.3.1">magic v0.3.1</a></li>
  <li><a href="https://github.com/git-pkgs/manifests/releases/tag/v0.12.0">manifests v0.12.0</a></li>
  <li><a href="https://github.com/git-pkgs/outline/releases/tag/v0.2.2">outline v0.2.2</a></li>
  <li><a href="https://github.com/git-pkgs/pin/releases/tag/v0.2.1">pin v0.2.1</a></li>
  <li><a href="https://github.com/git-pkgs/provides/releases/tag/v0.2.1">provides v0.2.1</a></li>
  <li><a href="https://github.com/git-pkgs/proxy/releases/tag/v0.8.1">proxy v0.8.1</a></li>
  <li><a href="https://github.com/git-pkgs/purl/releases/tag/v0.1.20">purl v0.1.20</a></li>
  <li><a href="https://github.com/git-pkgs/registries/releases/tag/v0.9.1">registries v0.9.1</a></li>
  <li><a href="https://github.com/git-pkgs/sigstore/releases/tag/v0.2.1">sigstore v0.2.1</a></li>
  <li><a href="https://github.com/git-pkgs/vers/releases/tag/v0.7.0">vers v0.7.0</a></li>
  <li><a href="https://github.com/git-pkgs/vulns/releases/tag/v0.2.3">vulns v0.2.3</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">How much should you trust your OSS data?</title><link href="https://nesbitt.io/2026/09/04/how-much-should-you-trust-your-oss-data.html" rel="alternate" type="text/html" title="How much should you trust your OSS data?" /><published>2026-09-04T09:00:00+00:00</published><updated>2026-09-04T09:00:00+00:00</updated><id>https://nesbitt.io/2026/09/04/how-much-should-you-trust-your-oss-data</id><content type="html" xml:base="https://nesbitt.io/2026/09/04/how-much-should-you-trust-your-oss-data.html"><![CDATA[<p><em>By Sophia Vargas, Google Open Source &amp; Andrew Nesbitt, Ecosyste.ms. Originally published on the <a href="https://opensource.googleblog.com/2026/09/how-much-should-you-trust-your-oss-data.html">Google Open Source Blog</a>, 3 September 2026.</em></p>

<p>Every second, open source contribution quietly shapes the software we rely on, and yet our view of this open ecosystem is surprisingly opaque. Open source development is performed in public spaces — we can see the commits, issues and comments, the APIs and endpoints are free to use — the logs are just sitting there, so why can’t we just collect all of the data?</p>

<p><em>…</em>Said every researcher, everywhere. However in most cases of open source related data, we are only looking at <a href="https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&amp;arnumber=9463079">part of the whole</a>. Why am I writing this post? Because many of us (including many business decision-makers) <strong>are too comfortable with unsubstantiated data.</strong> <a href="https://arxiv.org/abs/2106.15611">We’ve gotten used to it</a>. Our models assume that it’s <a href="https://arxiv.org/abs/2203.10384">smelly</a> and we adjust the logic and weights to compromise. When it comes to open source, our confidence is even lower, even though our resulting decisions can <a href="https://www.youtube.com/watch?v=8Yr2gGsgRsY">directly impact individuals</a> whom we collectively depend on.</p>

<p>Let’s consider one of my favorite datasets: <a href="https://www.gharchive.org/">GHarchive</a>. Started as a <a href="https://changelog.com/podcast/144">hobby project</a> in 2011, this crawler has amassed more than 15 years of event data from GitHub. While this source provides a historical record of open source development on GitHub, as a real-time or comprehensive source of metrics, it’s unreliable and should not be a source for volume-based metrics.</p>

<p>In 2025, GHarchive captured 14% fewer events than in 2024, despite steady growth in <a href="https://innovationgraph.github.com/global-metrics/repositories">platform adoption</a>. Since 2025, we estimate that data retention in GHarchive has fallen to ~50% and in 2026 it may be as low as 20% for some event types (see figure below). Prior to 2025, you could make the general assumption that the majority of <a href="https://docs.github.com/en/rest/using-the-rest-api/github-event-types">events</a> would be represented in this pipeline. Since 2025, we must now assume we may be missing at least half of events and possibly more — not to mention all of the additional activity that’s left out of the event API (see GitHub’s <a href="https://docs.github.com/en/graphql">GraphQL</a> API.)</p>

<p><img src="/images/gharchive-retention.png" alt="GHarchive data retention by event type, 2020–2026" /></p>

<p>The <a href="https://github.com/igrigorik/gharchive.org">crawler</a> logic behind this dataset is simple: give me all the events from the <a href="https://docs.github.com/en/rest/using-the-rest-api/github-event-types">GitHub Event</a> stream (e.g. opening pull requests, commenting on issues etc). However, the GitHub API has <a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api">limitations</a> on the number of calls per hour as well as the number of events listed, so for days with a lot of spiky activity, the crawler will miss some. Although we never assumed that this dataset was collecting 100% of events, the current architecture is showing signs of strain. We suspect that this is due, in part, to the rate of <a href="https://innovationgraph.github.com/global-metrics/repositories">repository growth</a> and adoption of automated tooling on GitHub. In 2011, GitHub announced it reached <a href="https://en.wikipedia.org/wiki/Timeline_of_GitHub">2 million public repositories</a>, and by 2026, that figure surpassed <a href="https://innovationgraph.github.com/global-metrics/repositories">400 million</a>.</p>

<p>I want to acknowledge that <strong>building and sharing comprehensive open datasets at scale is hard.</strong> Have you ever built a pipeline only to discover that the variables changed mid year, the payload for one output is getting truncated, all your joins broke because one side of the dataset is case sensitive … I could go on. And these examples are just ordinary data issues. Building a dataset at the scale of GitHub where “Every second, <a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/">more than one new developer on average joined GitHub—over 36 million in the past year</a>“—you start running into a new set of challenges.</p>

<p>My own journey with open source related data began when I repeatedly found myself questioning how much we could trust our own <a href="https://opensource.googleblog.com/2026/08/adapting-open-source-practices-to-an-ai-first-world-a-retrospective-on-2025.html">metrics</a>. To expand my understanding of the nuances and the limitations of open source related datasets, I reached out to <a href="https://nesbitt.io/">Andrew Nesbitt</a>, who has spent years digging in data trenches for the benefit of the community. Together we converged on the following issues that we wanted to highlight for the broader community.</p>

<h2 id="assembling-assume-there-will-be-problems">Assembling: Assume there will be problems</h2>

<p>When I asked Andrew <em>‘can you summarize the challenges you have faced assembling comprehensive datasets?’</em> — <strong>“I just assume I’m going to have a terrible time anyway, so I start with my best effort and fill in the gaps”.</strong> While disappointing, this aligned with most data aggregation methods I’ve reviewed—tools such as <a href="https://chaoss.github.io/grimoirelab/">Grimoire labs</a> and <a href="https://ossinsight.io/">OSS insights</a> also require multiple processes for collection, combination and reconciliation. Even with these approaches, many sources have missing, incomplete, or inconsistent information.</p>

<p><strong>One source is probably not enough.</strong> If you are considering the use of an open source project, you may want to know how many maintainers work on this project, what versions are available, what their dependencies are and any active vulnerabilities or known issues. Each of these queries requires a distinct source—the development history, the dependency graph, the CVE database, etc. <a href="https://ecosyste.ms">Ecosyste.ms</a> strives to pull this information together into one place, but combining data from 1000+ datasets has its own unique set of challenges.</p>

<p>For example, <strong>my index is probably not your index.</strong> One perennial issue is inconsistent naming conventions across sources. Beyond variable type and format, repository names, versions, packages, tags, licenses, urls, etc. tend to be unique across platforms. Some are case sensitive, there are often duplicates, and anyone can change a name at any time… I’ve been keenly following the adoption of <a href="https://github.com/package-url">purl</a> and <a href="https://www.softwareheritage.org/software-hash-identifier-swhid/">SWHID</a>, but so far I have not found one name to rule them all.</p>

<p><strong>Now we have to keep this up to date:</strong> At the moment, there is no consistent way of sharing updates across platforms. Changes to names, APIs, deletions, etc. are more often discovered by errors and breakage than by scouring release notes. To keep Ecosyste.ms up to date, Andrew has written multiple <a href="https://github.com/ecosyste-ms/repos/blob/main/docs/syncing.md">syncing processes</a> that identify or infer updates that need to be accounted for. I asked Andrew <em>‘If you could ask a platform/data source to change one thing, what would it be?’</em>, <strong>“Can I crawl an endpoint that’s just NEW stuff?’</strong></p>

<h2 id="consuming-design-your-pipeline-for-your-use-case">Consuming: Design your pipeline for your use case</h2>

<p>Because of LLMs, <strong>“it’s now easier for anyone to try to access and build reports”</strong>. But those building quick reports are likely not going to go through the pain of being comprehensive. This is where aggregated sources like GHarchive and Ecosyste.ms thrive. As data providers, we’d love if data consumers knew that:</p>

<p><strong>How you collect data matters.</strong> If everyone wanted the same dataset, in the same format, at the same time, it would be simple. Depending on how the data is stored—centralized vs distributed and cached, relational vs graph, etc. —queries could be more efficient (in cost and computation) than exports or bulk requests faster than individual requests. This all depends on the topology of the infrastructure and the dataset. In a perfect world, data producers would design their architecture for their top user journeys. However open source related datasets serve a wide variety of user personas from corporations to non-profits, researchers to individual users, maintainers, funders, and many more, with a variety of demands from historical deep dives to realtime feedback. Data producers can’t design for all of these cases, so my challenge to them is to be more open about the best way to access this information.</p>

<p>At the end of the day, we have to <strong>respect the <a href="https://www.opensourcestories.org/stories/2023/critical-human-infrastructure/">human infrastructure</a>:</strong> Open source-related datasets are riddled with personally identifiable information (PII). Some individuals may be comfortable sharing their information with fellow contributors, but seeing it aggregated across platforms can be uncomfortable. Any source with PII should be handled with care: anonymize when you can and ensure you are in alignment with policies and regulations. Open source communities are real people so please, consume their data responsibly.</p>

<h2 id="interpreting-never-stop-asking-questions">Interpreting: Never stop asking questions</h2>

<p>While many have moved on from ‘data-driven’ to ‘AI-enabled’, the fact remains that ALL AI SYSTEMS DEPEND ON <a href="https://hackernoon.com/the-ai-hierarchy-of-needs-18f111fcc007">DATA</a>. Our data about open source will continue to be incomplete and imperfect, but by asking questions about our sources, acknowledging the gaps, and considering both the technical and human processes behind open source development, we can refine and improve on how we interpret our insights and models even if they don’t completely reflect reality.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="open-source" /><category term="ecosyste.ms" /><category term="metrics" /><summary type="html"><![CDATA[Every second, open source shapes our software, yet our view of this ecosystem is surprisingly opaque. How much can you really trust OSS data?]]></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">Git Submodules as a Package Manager</title><link href="https://nesbitt.io/2026/09/01/git-submodules-as-a-package-manager.html" rel="alternate" type="text/html" title="Git Submodules as a Package Manager" /><published>2026-09-01T09:00:00+00:00</published><updated>2026-09-01T09:00:00+00:00</updated><id>https://nesbitt.io/2026/09/01/git-submodules-as-a-package-manager</id><content type="html" xml:base="https://nesbitt.io/2026/09/01/git-submodules-as-a-package-manager.html"><![CDATA[<p>I added a worktree to a repository last week to try a branch alongside the main checkout, ran <code class="language-plaintext highlighter-rouge">git submodule update --init</code> in it because the build needed the vendored dependencies, and when I was done went to clean up with <code class="language-plaintext highlighter-rouge">git worktree remove</code>, which git refused. Per <a href="https://git-scm.com/docs/git-worktree">the man page</a> only clean worktrees can be removed, and “unclean worktrees or ones with submodules” need <code class="language-plaintext highlighter-rouge">--force</code>. Submodules get their own clause in that sentence, distinct from dirty state. <code class="language-plaintext highlighter-rouge">git worktree move</code> is stricter again and <a href="https://git-scm.com/docs/git-worktree#Documentation/git-worktree.txt-move">refuses outright</a> on any worktree containing submodules. I’d spent <a href="/2026/08/25/hardening-the-override-flag.html">the previous week</a> cataloguing how command-line tools harden their <code class="language-plaintext highlighter-rouge">--force</code> flags, and here was git requiring one because two of its own features had collided.</p>

<p>GitHub’s <a href="https://github.blog/2015-07-29-git-2-5-including-multiple-worktrees-and-triangular-workflows/">git 2.5 announcement</a> introduced <code class="language-plaintext highlighter-rouge">git worktree</code> in July 2015 with a one-line caveat: “It’s not recommended to use <code class="language-plaintext highlighter-rouge">git worktree</code> with a repository that contains submodules.” Eleven years later git still requires <code class="language-plaintext highlighter-rouge">--force</code> to remove a worktree that has submodules and refuses to move one. In between, <code class="language-plaintext highlighter-rouge">worktree add</code> had to be <a href="https://github.com/git/git/commit/4782cf2ab686bacca8d2908319981ac27d54ca25">patched to ignore <code class="language-plaintext highlighter-rouge">submodule.recurse</code></a> because honouring it made the internal <code class="language-plaintext highlighter-rouge">reset --hard</code> recurse into submodule paths that were still empty in the fresh worktree.</p>

<p>This got me thinking about submodules as a package manager. Most of the pieces are there and the behaviour roughly matches, but they don’t quite line up and the experience of using them is worse at almost every step. Enough projects have adopted them and then <a href="https://diziet.dreamwidth.org/14666.html">backed out</a> that <a href="https://news.ycombinator.com/item?id=31792303">“why are git submodules so bad”</a> is a recurring thread.</p>

<p>The gitlink in the superproject’s tree, a commit SHA recorded at a path with mode <code class="language-plaintext highlighter-rouge">160000</code>, is the lockfile entry, and the <a href="/2026/02/05/git-magic-files.html"><code class="language-plaintext highlighter-rouge">.gitmodules</code> file</a> mapping paths to fetch URLs is the manifest. <code class="language-plaintext highlighter-rouge">git submodule update</code> reads both and populates the working tree, which is the install step. The pin itself is as precise as any package manager’s: an exact commit identified by object ID.</p>

<h2 id="resolution">Resolution</h2>

<p>The gitlink records only which commit to check out, so <code class="language-plaintext highlighter-rouge">.gitmodules</code> carries a <code class="language-plaintext highlighter-rouge">url</code> per submodule and <code class="language-plaintext highlighter-rouge">update</code> clones from there, which is the only resolution mechanism. If the upstream repository is renamed, transferred to a different host, or taken private, every downstream pin breaks, even though the SHA is unchanged and the objects still exist in every clone that already has them. The manifest hard-codes a host because git has no lookup from a commit ID to servers that hold it.</p>

<p>Git also copies each URL into the superproject’s <code class="language-plaintext highlighter-rouge">.git/config</code> the first time <code class="language-plaintext highlighter-rouge">git submodule init</code> runs, under <code class="language-plaintext highlighter-rouge">submodule.&lt;name&gt;.url</code>, and later commands read it from there, ignoring <code class="language-plaintext highlighter-rouge">.gitmodules</code>. Editing the committed <code class="language-plaintext highlighter-rouge">.gitmodules</code> to point at a mirror or a fork leaves an already-initialised clone unchanged until <a href="https://git-scm.com/docs/git-submodule"><code class="language-plaintext highlighter-rouge">git submodule sync</code></a> copies the new value across.</p>

<p>The usual workaround in CI is git’s global <a href="https://git-scm.com/docs/git-config"><code class="language-plaintext highlighter-rouge">url.&lt;base&gt;.insteadOf</code></a> config, which rewrites any URL with a matching prefix before fetching, submodule URLs included. The common cases are rewriting <code class="language-plaintext highlighter-rouge">https://github.com/</code> to <code class="language-plaintext highlighter-rouge">git@github.com:</code> so an SSH deploy key applies, or redirecting an internal hostname to a mirror.</p>

<h2 id="installation">Installation</h2>

<p>A plain <code class="language-plaintext highlighter-rouge">git clone</code> writes the gitlink into the index so the submodule directory exists, and leaves it empty until <code class="language-plaintext highlighter-rouge">git submodule update --init</code> runs or the clone was made with <code class="language-plaintext highlighter-rouge">--recurse-submodules</code>. The <a href="https://git-scm.com/docs/git-config"><code class="language-plaintext highlighter-rouge">submodule.recurse</code></a> config setting makes <code class="language-plaintext highlighter-rouge">checkout</code>, <code class="language-plaintext highlighter-rouge">fetch</code>, <code class="language-plaintext highlighter-rouge">pull</code>, <code class="language-plaintext highlighter-rouge">grep</code> and several other commands recurse automatically, and it defaults to off.</p>

<p>By default <code class="language-plaintext highlighter-rouge">update</code> checks out the gitlink commit, detached, and two independent flags modify that:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--init</code>: copy any missing <code class="language-plaintext highlighter-rouge">.gitmodules</code> entries into <code class="language-plaintext highlighter-rouge">.git/config</code> first, required on first run and a no-op after</li>
  <li><code class="language-plaintext highlighter-rouge">--remote</code>: check out the tip of the submodule’s configured remote-tracking branch instead of the gitlink commit (the remote’s <code class="language-plaintext highlighter-rouge">HEAD</code> if <code class="language-plaintext highlighter-rouge">submodule.&lt;name&gt;.branch</code> is unset)</li>
</ul>

<p>The <a href="https://git-scm.com/docs/git-submodule">command reference</a> documents both, though the name <code class="language-plaintext highlighter-rouge">update</code> conflates “install what’s pinned” with “update to latest”. Switching branches in the superproject changes the gitlink in the index and leaves the submodule’s working tree wherever it already was, so <code class="language-plaintext highlighter-rouge">git status</code> immediately shows the submodule as modified. Passing <code class="language-plaintext highlighter-rouge">--recurse-submodules</code> to <code class="language-plaintext highlighter-rouge">checkout</code>, or setting <code class="language-plaintext highlighter-rouge">submodule.recurse</code>, brings the submodule working tree along with the branch switch. The Rust project’s <a href="https://blog.rust-lang.org/inside-rust/2026/06/04/how-josh-helps-rust-manage-code-across-multiple-repositories/">account of moving compiler subprojects off submodules</a> lists this cluster from experience: checkouts left empty or on the wrong commit after clone, unrelated submodule bumps landing in pull requests because a branch switch left the submodule dirty, and custom logic in the <code class="language-plaintext highlighter-rouge">bootstrap</code> build tool to check each submodule out to the right commit before building.</p>

<h2 id="storage">Storage</h2>

<p>A submodule’s git directory is stored under the superproject’s <code class="language-plaintext highlighter-rouge">$GIT_DIR/modules/&lt;name&gt;/</code>, with a <code class="language-plaintext highlighter-rouge">.git</code> file in the submodule’s working tree containing a <code class="language-plaintext highlighter-rouge">gitdir:</code> pointer back to it and a <code class="language-plaintext highlighter-rouge">core.worktree</code> setting pointing the other way. <a href="https://git-scm.com/docs/git-submodule"><code class="language-plaintext highlighter-rouge">git submodule absorbgitdirs</code></a> migrates older clones that still have a nested <code class="language-plaintext highlighter-rouge">.git/</code> directory. Each entry under <code class="language-plaintext highlighter-rouge">modules/</code> is a git directory with its own refs, HEAD, index, config, hooks, and by default its own object store. Removing a submodule is correspondingly spread across three places: <code class="language-plaintext highlighter-rouge">git rm &lt;path&gt;</code> drops the gitlink and the <code class="language-plaintext highlighter-rouge">.gitmodules</code> entry, <code class="language-plaintext highlighter-rouge">git submodule deinit &lt;path&gt;</code> clears the working tree and the <code class="language-plaintext highlighter-rouge">.git/config</code> entry, and the absorbed <code class="language-plaintext highlighter-rouge">$GIT_DIR/modules/&lt;name&gt;</code> directory that both leave behind is <a href="https://git-scm.com/docs/gitsubmodules#_forms">documented</a> as a manual <code class="language-plaintext highlighter-rouge">rm -rf</code>.</p>

<p>Worktrees and submodules collide over this layout because a linked worktree shares the superproject’s <code class="language-plaintext highlighter-rouge">$GIT_DIR</code> but has its own working tree, HEAD, and index under <code class="language-plaintext highlighter-rouge">$GIT_DIR/worktrees/&lt;id&gt;/</code>. Put two worktrees on different superproject branches and they reference the same submodule at two different commits. Each needs its own submodule checkout and index, tied to storage that’s partly per-worktree and partly shared. <code class="language-plaintext highlighter-rouge">worktree remove</code> requires the override rather than checking whether that state is disposable, and <code class="language-plaintext highlighter-rouge">worktree move</code> refuses because the pointer-file rewrite it would need is unimplemented.</p>

<p>Xavier Morel <a href="https://www.spinics.net/lists/git/msg520955.html">asked on the git list this March</a> whether a submodule checkout could itself be a worktree of an existing shared clone, having found bare repositories plus worktrees worked well for a set of related projects but that adding submodules on top always cloned fresh. An <a href="https://www.spinics.net/lists/git/msg523846.html">RFC</a> and a <a href="https://www.spinics.net/lists/git/msg523988.html">three-patch series</a> proposing <code class="language-plaintext highlighter-rouge">--recurse-submodules</code> for <code class="language-plaintext highlighter-rouge">git worktree add</code> followed in April, giving each linked worktree its own submodule git directory under <code class="language-plaintext highlighter-rouge">$GIT_COMMON_DIR/worktrees/&lt;id&gt;/modules/</code> and sharing the object storage between them by hardlink.</p>

<p>The same multiplication happens in a single-worktree clone when two submodules both depend on a third repository. Each path in the superproject gets its own <code class="language-plaintext highlighter-rouge">modules/</code> entry, its own object store unless alternates are configured by hand, and its own gitlink. The two pins can point at different commits of the same repository, and git treats them as unrelated checkouts. Package managers with a shared cache (cargo’s <a href="https://doc.rust-lang.org/cargo/guide/cargo-home.html">registry cache</a>, pnpm’s <a href="https://pnpm.io/motivation">content-addressable store</a>, the <a href="https://go.dev/ref/mod#module-cache">Go module cache</a>) store the bytes once and check them out per location.</p>

<h2 id="updating">Updating</h2>

<p>The gitlink holds one commit SHA, so moving a submodule forward means entering it, fetching, checking out the new commit, leaving, and <code class="language-plaintext highlighter-rouge">git add &lt;path&gt;</code> in the superproject to record the new gitlink. <code class="language-plaintext highlighter-rouge">git submodule update --remote</code> fetches the configured branch’s tip and checks that out instead of the recorded gitlink, and committing the result in the superproject is what moves the pin. <code class="language-plaintext highlighter-rouge">.gitmodules</code> can name a <code class="language-plaintext highlighter-rouge">branch</code> per submodule for <code class="language-plaintext highlighter-rouge">--remote</code> and the update bots to follow. A plain <code class="language-plaintext highlighter-rouge">update</code> ignores that field and checks out the gitlink SHA regardless. There is no syntax for a version range, a tag pattern, or a minimum commit, so the manifest’s only floating reference is a branch name and the gitlink is the only pin.</p>

<p>Dependabot and Renovate can both open pull requests bumping a gitlink. Dependabot’s <a href="https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file"><code class="language-plaintext highlighter-rouge">gitsubmodule</code> ecosystem</a> proposes a new gitlink SHA when the submodule’s configured branch moves, and Renovate’s <a href="https://docs.renovatebot.com/modules/manager/git-submodules/"><code class="language-plaintext highlighter-rouge">git-submodules</code> manager</a> does the same, shipping disabled by default; both follow branch tips because a branch name is the only reference the manifest exposes.</p>

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

<p><code class="language-plaintext highlighter-rouge">.gitmodules</code> is committed to the repository, so a hostile upstream controls its contents, and git parses it during <code class="language-plaintext highlighter-rouge">clone --recurse-submodules</code> before the user has seen any of the fetched files, a combination that has produced remote code execution repeatedly. <a href="https://nvd.nist.gov/vuln/detail/CVE-2018-11235">CVE-2018-11235</a> used <code class="language-plaintext highlighter-rouge">../</code> in a submodule’s name so its git directory, hooks included, was written outside <code class="language-plaintext highlighter-rouge">$GIT_DIR/modules/</code> and a <code class="language-plaintext highlighter-rouge">post-checkout</code> hook ran during clone. In <a href="https://nvd.nist.gov/vuln/detail/CVE-2018-17456">CVE-2018-17456</a> the submodule URL began with <code class="language-plaintext highlighter-rouge">-</code>, so the child <code class="language-plaintext highlighter-rouge">git clone</code> parsed it as an option, the class of bug git’s <a href="/2026/07/21/end-of-options.html"><code class="language-plaintext highlighter-rouge">--end-of-options</code></a> delimiter defends against. <a href="https://github.com/git/git/security/advisories/GHSA-3wp6-j8xr-qw85">CVE-2022-39253</a> was a disclosure bug: a symlink in a submodule’s object directory made a local-transport clone copy arbitrary files from the victim’s disk. The fix changed the <a href="https://git-scm.com/docs/git-config"><code class="language-plaintext highlighter-rouge">protocol.file.allow</code></a> default to <code class="language-plaintext highlighter-rouge">user</code>, so local-path submodules now need an explicit opt-in. <a href="https://github.com/git/git/security/advisories/GHSA-8h77-4q3w-gfgv">CVE-2024-32002</a> combined a symlink with a case-insensitive filesystem to write a hook into <code class="language-plaintext highlighter-rouge">.git/</code> during recursive clone. I covered the broader pattern of package-manager checkout paths as an attack surface in <a href="/2026/05/04/package-manager-cwes.html">the CWE field guide</a>.</p>

<h2 id="abstraction">Abstraction</h2>

<p>Submodules expose git’s internals directly: object IDs as the pin, detached HEADs after update, the <code class="language-plaintext highlighter-rouge">$GIT_DIR/modules/</code> layout, transport URLs in the manifest. A package manager wraps the equivalents behind a manifest format, a resolver, and a local cache; submodules surface them raw.</p>

<p>Most of the gaps map to things package managers already solved: a shared object cache, recursing into dependencies by default on clone and checkout, a single lifecycle for adding and removing a dependency, range constraints in the manifest. The <a href="https://www.spinics.net/lists/git/msg523988.html">April patch series</a> adding <code class="language-plaintext highlighter-rouge">--recurse-submodules</code> to <code class="language-plaintext highlighter-rouge">git worktree add</code> tackles one instance of the storage problem, giving each worktree its own submodule checkout over hardlinked shared storage. Resolution is the harder one: a commit SHA is a host-independent identity for the object, and the URL in <code class="language-plaintext highlighter-rouge">.gitmodules</code> is git’s only mapping from that identity to a server that holds it.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="git" /><category term="package-managers" /><category term="dependencies" /><summary type="html"><![CDATA[.gitmodules is a manifest and the gitlink is a lockfile entry.]]></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: 29 August 2026</title><link href="https://nesbitt.io/2026/08/29/this-week-in-package-management.html" rel="alternate" type="text/html" title="This Week in Package Management: 29 August 2026" /><published>2026-08-29T10:00:00+00:00</published><updated>2026-08-29T10:00:00+00:00</updated><id>https://nesbitt.io/2026/08/29/this-week-in-package-management</id><content type="html" xml:base="https://nesbitt.io/2026/08/29/this-week-in-package-management.html"><![CDATA[<p>Week fifteen 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="pnpm-12">pnpm 12</h2>

<p><a href="https://pnpm.io/blog/releases/12.0">pnpm 12.0</a> is the stable release of the Rust rewrite. The commands, flags, settings and lockfile format from pnpm 11 carry over unchanged; a companion <a href="https://pnpm.io/blog/whats-different-in-pnpm-12">what’s different</a> post lists the seven behavioural changes.</p>

<p>Git dependencies on GitHub, GitLab and Bitbucket now resolve through the host’s canonical HTTPS URL regardless of how they were specified, so lockfiles record only the canonical HTTPS URL for those hosts. Dependency cycles are broken canonically during peer resolution, so the lockfile is a pure function of the dependency graph. Cycle-heavy workspaces resolve peers 2-3x faster with roughly 25% less memory.</p>

<p>Globally installed tools resolve to the version pinned in the current project, and pnpm can provision npm, Yarn and Bun as managed tools with a <code class="language-plaintext highlighter-rouge">pnx</code> command for one-off runs. Unrecognised keys in <code class="language-plaintext highlighter-rouge">pnpm-workspace.yaml</code> are now errors.</p>

<p>On the 11.x branch, <a href="https://pnpm.io/blog/releases/11.23">11.23</a> reworks the <code class="language-plaintext highlighter-rouge">registries</code> setting so an Artifactory or GitLab registry can keep its tarball URLs out of <code class="language-plaintext highlighter-rouge">pnpm-lock.yaml</code>, and <a href="https://pnpm.io/blog/releases/11.24">11.24</a> restores <code class="language-plaintext highlighter-rouge">pnpm approve-builds --global</code> and groups recursive publishes by registry so a credential mismatch is caught before anything is uploaded.</p>

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

<p><a href="https://github.com/commercialhaskell/stack/releases/tag/rc%2Fv4.1.0.1">Stack 4.1.0.1 RC</a> adds cross-package Backpack support: when a package uses signatures and mixins to depend on an abstract interface from another package, Stack now generates the extra instantiation build steps Cabal needs. Private Backpack, with signatures and implementations in the same package, was already supported.</p>

<p><a href="https://github.com/microsoft/winget-cli/releases/tag/v1.29.290">winget 1.29.290</a> adds an experimental <code class="language-plaintext highlighter-rouge">sourcePriority</code> feature: sources can be given a numeric priority via <code class="language-plaintext highlighter-rouge">source add</code> or <code class="language-plaintext highlighter-rouge">source edit</code>, and higher-priority sources sort first in search results when other ranking factors are equal.</p>

<p><a href="https://github.com/apache/maven/releases/tag/maven-3.10.0-rc-1">Maven 3.10.0-rc-1</a> is the first release candidate for the 3.10 line, which aligns 3.x behaviour with Maven 4: classpath ordering, version-range resolution filtering and the Resolver 2.0.19 changes are backported, and the super POM drops the deprecated <code class="language-plaintext highlighter-rouge">release-profile</code> and default plugin management.</p>

<p><a href="https://github.com/renovatebot/renovate/releases/tag/44.42.0">Renovate 44.42.0</a> sets <code class="language-plaintext highlighter-rouge">CI=true</code> for every child process it spawns, and <a href="https://github.com/renovatebot/renovate/releases/tag/44.49.0">44.49.0</a> fetches changelog entries newest-first and stops once the target platform’s PR body length limit is reached, instead of fetching hundreds of releases.</p>

<p>Also out:</p>

<ul>
  <li><a href="https://github.com/Homebrew/brew/releases/tag/6.0.20">Homebrew 6.0.20</a></li>
  <li><a href="https://github.com/npm/cli/releases/tag/v11.19.1">npm 11.19.1</a></li>
  <li><a href="https://github.com/verdaccio/verdaccio/releases/tag/v6.10.1">Verdaccio 6.10.1</a></li>
  <li><a href="https://github.com/denoland/deno/releases/tag/v2.9.6">Deno 2.9.6</a></li>
  <li><a href="https://github.com/astral-sh/uv/releases/tag/0.12.7">uv 0.12.7</a></li>
  <li><a href="https://github.com/pypa/pipx/releases/tag/1.17.0">pipx 1.17.0</a></li>
  <li><a href="https://github.com/prefix-dev/pixi/releases/tag/v0.78.0">pixi 0.78.0</a></li>
  <li><a href="https://github.com/jdx/mise/releases/tag/v2026.8.14">mise 2026.8.14</a></li>
  <li><a href="https://github.com/macports/macports-base/releases/tag/v2.12.6">MacPorts 2.12.6</a></li>
  <li><a href="https://github.com/flatpak/flatpak/releases/tag/1.18.2">Flatpak 1.18.2</a></li>
  <li><a href="https://github.com/dependabot/dependabot-core/releases/tag/v0.393.0">Dependabot Core 0.393.0</a></li>
  <li><a href="https://github.com/gradle/gradle/releases/tag/v9.8.0-M2">Gradle 9.8.0-M2</a></li>
  <li><a href="https://github.com/sbt/sbt/releases/tag/v2.0.8">sbt 2.0.8</a></li>
</ul>

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

<p>The Renovate project <a href="https://github.com/renovatebot/renovate/discussions/45495">disclosed ten advisories</a> affecting the CLI, nine High and one Moderate: four command injection vectors, four credential exfiltration paths via malicious <code class="language-plaintext highlighter-rouge">Link</code> headers, TLS private key exposure in logs, and a <code class="language-plaintext highlighter-rouge">minimumReleaseAge</code> bypass. All are fixed in 44.14.7, released 7 August.</p>

<p><a href="https://github.com/composer/composer/releases/tag/2.10.3">Composer 2.10.3</a> and <a href="https://github.com/composer/composer/releases/tag/2.2.30">2.2.30</a> fix four issues: <a href="https://github.com/composer/composer/security/advisories/GHSA-96h3-5x6v-m776">CVE-2026-59944</a> (path traversal via symlinked <code class="language-plaintext highlighter-rouge">bin</code> entries), <a href="https://github.com/composer/composer/security/advisories/GHSA-rvx4-ffvw-m9q3">command injection via a malicious Perforce URL</a>, URL-embedded credentials leaking into more places than intended, and GitLab URL matching that could send credentials to the wrong domain.</p>

<p><a href="https://github.com/oras-project/oras/releases/tag/v1.3.4">ORAS 1.3.4</a> fixes three credential-scoping issues: mTLS client certificates supplied via <code class="language-plaintext highlighter-rouge">--cert-file</code> were presented to any HTTPS peer including cross-origin redirect and bearer-realm targets, custom <code class="language-plaintext highlighter-rouge">--header</code> values were forwarded to hosts other than the configured registry, and <code class="language-plaintext highlighter-rouge">--debug</code> traces logged pre-signed URL parameters, cookies, proxy authorisation and token response bodies.</p>

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

<p><a href="https://anil.recoil.org/notes/rumour-is-the-exploit">Rumour is the exploit</a> (Anil Madhavapeddy): after opening a public PR fixing a path traversal in OCaml’s cohttp, Anil’s server logs showed probes for the exact pattern within minutes, and an agent given only a rough description of the bug produced a working exploit in under a minute. He argues that once the existence of a fix is public an agent can rediscover the bug independently, so the coordinated-disclosure window that embargoes are meant to protect has closed for open source maintainers.</p>

<p><a href="https://blog.packagist.com/whats-new-in-private-packagist-august-2026-update/">What’s new in Private Packagist, August 2026</a> (Packagist blog): MFA enforcement now covers Composer authentication tokens, regenerated tokens invalidate the old one immediately (previously cached for 14 days), and security monitoring reads Composer 2.10’s <code class="language-plaintext highlighter-rouge">config.policy</code> section to determine which advisories to suppress.</p>

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

<p><a href="https://arxiv.org/abs/2608.22652">Evaluating Inference-Time Defenses Against Package Hallucination in LLM-Generated Code</a> (arXiv) shows that prior measurements overstate hallucinated-package rates by counting standard-library modules as hallucinations (by 9.4 percentage points for Python) and evaluates seven decoding-time strategies for reducing them.</p>

<p><a href="https://arxiv.org/abs/2608.20675">The Rising Cost of Trust: Practitioners’ Trust Signals, Controls, and Responses in the Software Supply Chain</a> (arXiv) is a semi-structured interview study of 38 industry and open source practitioners on which signals they use when deciding to depend on a package and what controls they apply.</p>

<p><a href="https://arxiv.org/abs/2608.27125">AROMA+: A Study of Factors Affecting Reproducible Builds in the Maven Ecosystem</a> (arXiv) automatically recovers the build environment for Maven Central releases and attempts to reproduce them: 32% of packages are feasible for automatic reproduction, of which 12% reproduce fully, and the recovered build specs match Reproducible Central’s manually curated ones field-for-field 99.8% of the time.</p>

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

<p>The Rust project <a href="https://blog.rust-lang.org/2026/08/26/announcing-our-first-maintainers-in-residence/">announced</a> its first Maintainers in Residence, funding five contributors (including rustup maintainer rami3l) full-time and two more via grants for at least twelve months from a Rust Foundation maintainers fund backed by Google, AWS and OpenAI.</p>

<p><a href="https://opensourcesecurity.io/2026/2026-08-paul-fettle-cve/">Fettle and the CVE problem</a> (Josh Bressers, Open Source Security): an interview with Paul Asadoorian on Fettle, a tool for checking a Linux system’s outdated packages, pending firmware updates and binary hardening flags, and on his InfraTrust Pulse report which tracks vendor advisories rather than raw CVE counts.</p>

<p><a href="https://fastwonderblog.com/2026/08/25/part-2-additional-sustainability-topics-from-the-chaoss-practitioner-guides/">Additional Sustainability Topics from the CHAOSS Practitioner Guides</a> (Dawn Foster) covers the Security, Diverse Leadership and Sunsetting guides; the Security guide recommends Libyears and release frequency as dependency-currency metrics.</p>

<p>The Sovereign Tech Agency is <a href="https://modal.cx/blog/announcing-flatpak-sta/">investing €508,640 in Flatpak</a> over two years, funding a contractor team organised by Modal Collective and Para-Real to build new portals for PipeWire audio, network isolation, VPN and spell-checking, plus an entitlements system for app permissions.</p>

<p><a href="https://github.com/decoderloop/rust_metadata_carver">Rust Metadata Carver</a> (Decoder Loop) is a Binary Ninja plugin that extracts panic metadata from Rust binaries, recovering which crates and Rust version a stripped binary was built with.</p>

<p><a href="https://www.jvt.me/posts/2026/08/17/renovate-pretty-log/">renovate-pretty-log-tui 0.6.0</a> (Jamie Tanna) adds a summary view and HTML export to the terminal UI for reading Renovate debug logs, and surfaces errors more clearly.</p>

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

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

<ul>
  <li><a href="https://github.com/git-pkgs/gcs/releases/tag/v0.1.0">gcs v0.1.0</a> (new), a Go library for Google Cloud Storage object operations via the JSON API with minimal dependencies</li>
  <li><a href="https://github.com/git-pkgs/archives/releases/tag/v0.6.0">archives v0.6.0</a></li>
  <li><a href="https://github.com/git-pkgs/artifacts/releases/tag/v0.2.0">artifacts v0.2.0</a></li>
  <li><a href="https://github.com/git-pkgs/brief/releases/tag/v0.12.1">brief v0.12.1</a></li>
  <li><a href="https://github.com/git-pkgs/clone/releases/tag/v0.7.1">clone v0.7.1</a></li>
  <li><a href="https://github.com/git-pkgs/manifests/releases/tag/v0.10.1">manifests v0.10.1</a></li>
  <li><a href="https://github.com/git-pkgs/purl/releases/tag/v0.1.19">purl v0.1.19</a></li>
  <li><a href="https://github.com/git-pkgs/registries/releases/tag/v0.9.0">registries v0.9.0</a></li>
  <li><a href="https://github.com/git-pkgs/sbom/releases/tag/v0.1.6">sbom v0.1.6</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">Now Hiring: Senior Open Source Maintainer</title><link href="https://nesbitt.io/2026/08/28/now-hiring-senior-open-source-maintainer.html" rel="alternate" type="text/html" title="Now Hiring: Senior Open Source Maintainer" /><published>2026-08-28T10:00:00+00:00</published><updated>2026-08-28T10:00:00+00:00</updated><id>https://nesbitt.io/2026/08/28/now-hiring-senior-open-source-maintainer</id><content type="html" xml:base="https://nesbitt.io/2026/08/28/now-hiring-senior-open-source-maintainer.html"><![CDATA[<p><strong>Location:</strong> Lincoln, Nebraska, USA<br />
<strong>Employment type:</strong> Volunteer<br />
<strong>Term:</strong> Permanent<br />
<strong>Compensation:</strong> $0<br />
<strong>Reports to:</strong> The community<br />
<strong>Direct reports:</strong> None<br />
<strong>Start date:</strong> Immediate</p>

<p>We’re looking for a passionate, self-directed engineer to take full ownership of one of the most widely deployed libraries in the ecosystem. This is a rare opportunity to make a real impact on software that millions of developers depend on every single day. If you thrive on autonomy, love working in the open, and want to wear many hats in a fast-paced environment, we’d love to hear from you.</p>

<p><strong>About the role</strong></p>

<p>As Senior Open Source Maintainer you will be the primary technical owner and public face of the project. You’ll drive the roadmap, ship releases, support a global user base, and champion the project across the wider industry. This is a highly visible individual contributor role with enormous scope. No two days are the same, and you’ll have full autonomy to decide how you spend your time.</p>

<p><strong>What you’ll do</strong></p>

<ul>
  <li>Own the technical roadmap and long-term architectural direction</li>
  <li>Review and merge contributions from a diverse, global community of open source developers</li>
  <li>Triage and prioritise the issue tracker and feature request backlog</li>
  <li>Maintain comprehensive documentation, examples, migration guides, and API references</li>
  <li>Ensure project websites and documentation meet current accessibility standards</li>
  <li>Cut releases across all supported major version lines and write the release notes</li>
  <li>Own the CI pipeline, release automation, and package publishing infrastructure</li>
  <li>Keep dependencies current and promptly action automated update pull requests</li>
  <li>Support users on GitHub, Discord, Slack, Stack Overflow, Mastodon, Bluesky, and email</li>
  <li>Ensure compatibility across all current runtimes, operating systems, and architectures</li>
  <li>Preserve backwards compatibility, including widely relied-upon undocumented behaviour</li>
  <li>Coordinate release timing with downstream distributions, registries, and redistributors</li>
</ul>

<p><strong>You’ll also</strong></p>

<ul>
  <li>Participate in the on-call rotation for incidents at downstream production deployments</li>
  <li>Support enterprise consumers where project issues block business-critical systems</li>
  <li>Define and communicate support, deprecation, and end-of-life policies for all release lines</li>
  <li>Provide migration guidance to adopters upgrading from end-of-life releases</li>
  <li>Coordinate advisories, CVE records, disclosure timelines, and researcher credits</li>
  <li>Ship embargoed CVE patches across all supported version lines within the disclosure window</li>
  <li>Produce SBOMs, VEX statements, and signed provenance attestations for every release</li>
  <li>Maintain reproducible builds and independently verifiable release artifacts</li>
  <li>Complete supplier security assessments and questionnaires for enterprise consumers</li>
  <li>Remediate findings from OpenSSF Scorecard, dependency scanners, and compliance platforms</li>
</ul>

<p><strong>As well as</strong></p>

<ul>
  <li>Review high volumes of contributions from AI coding agents and automated refactoring tools</li>
  <li>Triage vulnerability reports generated by commercial AI security scanners</li>
  <li>Champion the project at international conferences, on podcasts, and across social media</li>
  <li>Moderate all community spaces and enforce the Code of Conduct</li>
  <li>Engage constructively with feedback shared on social media and public forums</li>
  <li>Represent the project in working groups, standards bodies, and regulatory consultations</li>
  <li>Prepare grant applications and quarterly reports for current and prospective funders</li>
  <li>Administer domains, trademarks, signing keys, cloud accounts, and social media</li>
  <li>Advise enterprise legal teams on licensing, patent, warranty, and export control matters</li>
  <li>Own the succession plan: identify, recruit, and onboard your replacement</li>
</ul>

<p><strong>How we’ll measure success</strong></p>

<ul>
  <li>GitHub stars</li>
  <li>Time to first response on issues and pull requests</li>
  <li>Release cadence and mean time to patch for disclosed vulnerabilities</li>
  <li>Open issue and stale pull request backlog</li>
  <li>Dependency freshness</li>
  <li>OpenSSF Scorecard result and other automated project scoring</li>
  <li>Community sentiment across public channels</li>
  <li>Downstream adoption</li>
  <li>Bus factor</li>
</ul>

<p><strong>What we’re looking for</strong></p>

<ul>
  <li>8+ years of professional software engineering experience</li>
  <li>Deep expertise in one systems language and professional proficiency in at least four others</li>
  <li>Written communication pitched to audiences from first-time contributors to Fortune 500 CISOs</li>
  <li>Consistent release cadence with minimal dependency churn and no breaking changes</li>
  <li>Comfortable across engineering, support, security, community, marketing, finance, and legal</li>
  <li>Balances contributors, enterprises, security researchers, regulators, and automated systems</li>
  <li>Comfortable receiving direct public feedback and turning it into improvements</li>
  <li>Availability overlapping core business hours in AMER, EMEA, and APAC</li>
  <li>Able to respond to time-sensitive security matters outside normal working hours</li>
  <li>Provides own hardware, reliable internet, and test environments for all supported platforms</li>
</ul>

<p><strong>Nice to have</strong></p>

<ul>
  <li>Prior experience in technical writing, developer relations, or community management</li>
  <li>Experience migrating existing codebases to memory-safe languages</li>
  <li>Working knowledge of licence compatibility and international trademark registration</li>
  <li>Grant writing and nonprofit financial reporting experience</li>
  <li>Media training</li>
  <li>An established personal audience on at least one major social platform</li>
  <li>Conversational proficiency in a second and third language</li>
  <li>A second job</li>
</ul>

<p><strong>What we offer</strong></p>

<ul>
  <li>100% remote, work from anywhere in the world</li>
  <li>Complete flexibility over your working hours</li>
  <li>High-impact work depended on by startups and Fortune 500 companies alike</li>
  <li>Exceptional visibility and personal brand building opportunities</li>
  <li>Regular speaking slots at leading industry events</li>
  <li>The opportunity to develop close working relationships with several national CERTs</li>
  <li>A passionate, highly engaged global user community</li>
  <li>GitHub Sponsors enabled on the repository</li>
</ul>

<p><strong>How to apply</strong></p>

<p>Please submit a CV, a link to your GitHub profile, and a short cover letter telling us what ownership means to you. Our interview process consists of a recruiter screen, a technical interview, a system design round, a take-home exercise, and a final community panel.</p>

<p>If you don’t meet every requirement listed above, we’d still encourage you to apply.</p>

<p>We are committed to building a diverse and inclusive community and welcome applicants from all backgrounds. This position will remain open until filled. Due to the volume of applications received, we are unable to respond to every candidate individually.</p>

<p><em>This role is not eligible for relocation assistance or visa sponsorship.</em></p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="open-source" /><category term="maintainers" /><category term="satire" /><summary type="html"><![CDATA[A rare opportunity to make a real impact in a fast-paced, high-visibility role.]]></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">Bazel Module Versions Aren’t SemVer</title><link href="https://nesbitt.io/2026/08/27/bazel-module-versions-arent-semver.html" rel="alternate" type="text/html" title="Bazel Module Versions Aren’t SemVer" /><published>2026-08-27T10:00:00+00:00</published><updated>2026-08-27T10:00:00+00:00</updated><id>https://nesbitt.io/2026/08/27/bazel-module-versions-arent-semver</id><content type="html" xml:base="https://nesbitt.io/2026/08/27/bazel-module-versions-arent-semver.html"><![CDATA[<p>Aman Sharma <a href="https://github.com/git-pkgs/enrichment/issues/65">reported</a> this week that <a href="https://packages.ecosyste.ms">packages.ecosyste.ms</a> had the latest version of protobuf on the <a href="https://registry.bazel.build/">Bazel Central Registry</a> (BCR, the default package index for Bazel’s built-in dependency manager) as <code class="language-plaintext highlighter-rouge">3.19.6</code>. The <a href="https://registry.bazel.build/modules/protobuf">current release</a> is <code class="language-plaintext highlighter-rouge">36.0.bcr.1</code>. Protobuf <a href="https://protobuf.dev/support/version-support/">dropped the leading <code class="language-plaintext highlighter-rouge">3.</code> in 2022</a> and has shipped <code class="language-plaintext highlighter-rouge">21.x</code> through <code class="language-plaintext highlighter-rouge">36.x</code> since, so <code class="language-plaintext highlighter-rouge">3.19.6</code> is four years behind.</p>

<p>The <a href="https://github.com/ecosyste-ms/packages/issues/1810">root cause</a> is that the “latest release” query filters out anything the <a href="https://rubygems.org/gems/semantic"><code class="language-plaintext highlighter-rouge">semantic</code></a> gem fails to parse. That code predates the <a href="https://github.com/andrew/vers">vers</a> library and I’ve yet to swap it over, so it’s still applying one strict SemVer parser to every ecosystem. SemVer requires exactly three numeric segments before the hyphen. <code class="language-plaintext highlighter-rouge">36.0</code> has two, <code class="language-plaintext highlighter-rouge">0.7.1.bcr.1</code> has five, and <code class="language-plaintext highlighter-rouge">bcr</code> contains letters, so every protobuf release since <code class="language-plaintext highlighter-rouge">3.19.6</code> fails the parse and gets treated as unstable. The same filter reports <a href="https://registry.bazel.build/modules/glog">glog</a>’s latest as <code class="language-plaintext highlighter-rouge">0.7.1</code> when the registry has <code class="language-plaintext highlighter-rouge">0.7.1.bcr.1</code>.</p>

<p>Bazel is a build tool first, and its module system (<a href="https://bazel.build/external/overview#bzlmod">bzlmod</a>, the default since Bazel 7) is mostly used to pull in existing C++, Java and Go projects that already have their own release histories. Rather than force those projects to renumber, Bazel <a href="https://bazel.build/external/module#version-format">documents</a> its version format as a deliberate loosening of SemVer: any SemVer string is valid and sorts the same way, and beyond that the release part can have any number of segments and each segment can contain letters. Abseil’s date-based <code class="language-plaintext highlighter-rouge">20210324.2</code> and protobuf’s two-segment <code class="language-plaintext highlighter-rouge">36.0</code> are both accepted as-is. The grammar in <a href="https://github.com/bazelbuild/bazel/blob/a8a2c0c1fa063a0bba4e7222a1b1d2c8a4e0e632/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java#L33-L48"><code class="language-plaintext highlighter-rouge">Version.java</code></a> is <code class="language-plaintext highlighter-rouge">RELEASE[-PRERELEASE][+BUILD]</code>, where <code class="language-plaintext highlighter-rouge">RELEASE</code> is dot-separated identifiers of ASCII letters and digits only. Comparison applies SemVer’s <a href="https://semver.org/#spec-item-11">prerelease identifier rules</a> to the release segments as well:</p>

<ul>
  <li>numeric identifiers sort numerically, so <code class="language-plaintext highlighter-rouge">36.0</code> &gt; <code class="language-plaintext highlighter-rouge">4.0.0</code></li>
  <li>any identifier containing a letter sorts above every purely numeric one, then as an ASCII string</li>
  <li>a version with more release segments sorts above one that shares its prefix, so <code class="language-plaintext highlighter-rouge">0.7.1.bcr.1</code> &gt; <code class="language-plaintext highlighter-rouge">0.7.1</code></li>
  <li>a prerelease sorts below the same release part on its own, so <code class="language-plaintext highlighter-rouge">36.0-rc2</code> &lt; <code class="language-plaintext highlighter-rouge">36.0</code></li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">.bcr.N</code> suffix is a <a href="https://github.com/bazelbuild/bazel-central-registry/blob/main/docs/README.md#module-versions">registry convention</a> rather than part of the format. Registry entries are immutable once published, and the registry often carries small patches on top of the upstream tarball to make a project build cleanly under Bazel. When one of those patches needs updating and the upstream source stays as-is, the fixed entry is published under a new version with <code class="language-plaintext highlighter-rouge">.bcr.1</code> tacked onto the release part. That’s why <code class="language-plaintext highlighter-rouge">36.0.bcr.1</code> exists: <code class="language-plaintext highlighter-rouge">36.0</code> was <a href="https://github.com/bazelbuild/bazel-central-registry/blob/main/modules/protobuf/metadata.json">yanked</a> for a macOS toolchain integrity mismatch, and the fix went in as extra release segments rather than a <code class="language-plaintext highlighter-rouge">+build</code> tag so that it sorts strictly above <code class="language-plaintext highlighter-rouge">36.0</code>. That ordering matters because Bazel resolves dependencies with <a href="https://research.swtch.com/vgo-mvs">Minimal Version Selection</a>, the same algorithm as Go modules, which always picks the highest version any dependent asked for; a suffix that sorted equal or lower would leave everyone pinned to the broken build.</p>

<p><code class="language-plaintext highlighter-rouge">Version.java</code> also handles build metadata differently from SemVer §10: rather than being ignored for comparison and kept, the <code class="language-plaintext highlighter-rouge">+BUILD</code> part is <a href="https://github.com/bazelbuild/bazel/blob/a8a2c0c1fa063a0bba4e7222a1b1d2c8a4e0e632/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java#L69-L72">stripped at parse time</a> before anything is stored or sent to a registry, so <code class="language-plaintext highlighter-rouge">1.2.3+abc</code> and <code class="language-plaintext highlighter-rouge">1.2.3</code> are equal and interchangeable rather than equal-but-distinct. The empty string is a valid version and <a href="https://github.com/bazelbuild/bazel/blob/a8a2c0c1fa063a0bba4e7222a1b1d2c8a4e0e632/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java#L182-L186">sorts higher than everything else</a>; it marks a module that’s been overridden to point at a local directory or git commit instead of a registry release, and sorting it top means a local override always wins MVS. Since the release segments have no fixed meaning, breaking changes were originally signalled by a separate <code class="language-plaintext highlighter-rouge">compatibility_level</code> integer in the module file, roughly equivalent to a SemVer major version. Both active release lines <a href="https://github.com/bazelbuild/bazel/pull/28615">made that field a no-op</a> in February 2026 (9.1.0, backported to 8.6.0) after it caused resolution failures only the module authors could resolve, leaving breaking changes to be reported by the module’s own build-time errors instead.</p>

<p>External parsers get <code class="language-plaintext highlighter-rouge">.bcr.N</code> wrong one of two ways:</p>

<ul>
  <li>strict SemVer (the <code class="language-plaintext highlighter-rouge">semantic</code> gem): rejects <code class="language-plaintext highlighter-rouge">0.7.1.bcr.1</code> because the release part must be exactly three integers, so it drops from the stable set</li>
  <li>permissive generic (the vers fallback): accepts the extra segments, applies prerelease semantics to the alphabetic <code class="language-plaintext highlighter-rouge">bcr</code>, and sorts it below <code class="language-plaintext highlighter-rouge">0.7.1</code></li>
</ul>

<p>The <a href="https://github.com/package-url/vers-spec">vers spec</a>, the package-url project’s cross-ecosystem notation for version ranges, enumerates comparison schemes for <a href="https://github.com/package-url/vers-spec/blob/main/docs/types/vers-types.md">fifteen ecosystems</a> and has yet to cover Bazel, though <code class="language-plaintext highlighter-rouge">pkg:bazel</code> is a <a href="https://github.com/package-url/purl-spec/blob/main/types/bazel-definition.json">registered purl type</a> so there is already a slot for a <code class="language-plaintext highlighter-rouge">vers:bazel</code> scheme. Both vers libraries I maintain hit the second case. The fix in <a href="https://github.com/andrew/vers/pull/34">Ruby</a> and <a href="https://github.com/git-pkgs/vers/pull/43">Go</a> is a port of the <code class="language-plaintext highlighter-rouge">Version.java</code> sort as an implementation-defined <code class="language-plaintext highlighter-rouge">bazel</code> scheme ahead of the spec. The corresponding <a href="https://github.com/ecosyste-ms/packages/pull/1811">ecosyste.ms change</a> keeps <code class="language-plaintext highlighter-rouge">.bcr.N</code> releases eligible as stable, lets the yanked flag exclude <code class="language-plaintext highlighter-rouge">36.0</code>, and moves both protobuf and glog to the correct latest version.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="ecosyste.ms" /><category term="dependencies" /><summary type="html"><![CDATA[According to strict SemVer, protobuf's latest Bazel release is from 2022.]]></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">Hardening the Override Flag</title><link href="https://nesbitt.io/2026/08/25/hardening-the-override-flag.html" rel="alternate" type="text/html" title="Hardening the Override Flag" /><published>2026-08-25T10:00:00+00:00</published><updated>2026-08-25T10:00:00+00:00</updated><id>https://nesbitt.io/2026/08/25/hardening-the-override-flag</id><content type="html" xml:base="https://nesbitt.io/2026/08/25/hardening-the-override-flag.html"><![CDATA[<p>I’ve been reading through the recent run of incidents where package-manager infrastructure was the attack surface, as one does on a Sunday afternoon. One question I kept coming back to was whether the tools have any defences at the flag and config level. Every package manager has an override that turns off a check: <code class="language-plaintext highlighter-rouge">--allow-unauthenticated</code>, <code class="language-plaintext highlighter-rouge">--break-system-packages</code>, <code class="language-plaintext highlighter-rouge">--ignore-scripts</code>, an env var that points the resolver at a different registry. If a compromised install script can pass those as easily as a person at a keyboard can, the check does very little. So I went looking at how command-line tools more generally handle their dangerous overrides, <code class="language-plaintext highlighter-rouge">sudo</code> and <code class="language-plaintext highlighter-rouge">curl</code> and <code class="language-plaintext highlighter-rouge">rm</code> and the rest, and the catalogue got long enough to write up on its own.</p>

<h2 id="names-and-channels">Names and channels</h2>

<p>GNU rm goes back and re-reads <code class="language-plaintext highlighter-rouge">argv</code> after option parsing has finished. getopt_long accepts any unambiguous prefix of a long option, which would make <code class="language-plaintext highlighter-rouge">--no-p</code> a valid spelling of <code class="language-plaintext highlighter-rouge">--no-preserve-root</code>, so rm <a href="https://github.com/coreutils/coreutils/blob/master/src/rm.c">checks the literal string</a> and rejects anything shorter with “you may not abbreviate the –no-preserve-root option”. <code class="language-plaintext highlighter-rouge">--preserve-root</code> has been the default <a href="https://lists.gnu.org/archive/html/bug-coreutils/2006-08/msg00354.html">since 2006</a>, and the override requires the full flag every time, on top of whatever the argument parser does. It’s easy to miss when reimplementing: uutils, the Rust coreutils rewrite, <a href="https://github.com/uutils/coreutils/issues/10188">accepted <code class="language-plaintext highlighter-rouge">--n</code> until March 2026</a>.</p>

<p>A long name with no short form is the most common hardening on an override flag, and the one most package managers reach for. apt has <code class="language-plaintext highlighter-rouge">--allow-remove-essential</code>, <code class="language-plaintext highlighter-rouge">--allow-downgrades</code>, <code class="language-plaintext highlighter-rouge">--allow-change-held-packages</code> and <code class="language-plaintext highlighter-rouge">--allow-unauthenticated</code>, all long-only, <a href="https://manpages.debian.org/unstable/apt/apt-get.8.en.html">split out from a blanket <code class="language-plaintext highlighter-rouge">--force-yes</code> in apt 1.1</a>. pnpm 10 disables install scripts by default and calls the global re-enable <a href="https://pnpm.io/settings/build"><code class="language-plaintext highlighter-rouge">dangerouslyAllowAllBuilds</code></a>. hdparm gates its drive-destroying operations behind <a href="https://sourceforge.net/projects/hdparm/files/hdparm/"><code class="language-plaintext highlighter-rouge">--yes-i-know-what-i-am-doing</code>, and the worst of them behind <code class="language-plaintext highlighter-rouge">--please-destroy-my-drive</code> as well</a>. The name is the warning, and someone reviewing a script or a diff will read past <code class="language-plaintext highlighter-rouge">-f</code> but stop on <code class="language-plaintext highlighter-rouge">--allow-remove-essential</code>.</p>

<p>apt also used a typed confirmation phrase for essential-package removal until 2021: print the warning, require <code class="language-plaintext highlighter-rouge">Yes, do as I say!</code> character-for-character before proceeding. Typing the translated phrase in Chinese locales <a href="https://bugs.debian.org/234886">required an input method that was unavailable at a bare console</a>, so apt 0.5.23 stopped translating it for zh_*. The check <a href="https://lists.debian.org/deity/2012/10/msg00042.html">ran regardless of TTY</a>, so <code class="language-plaintext highlighter-rouge">echo 'Yes, do as I say!' | apt-get ...</code> worked. In November 2021 a Pop!_OS packaging conflict made <code class="language-plaintext highlighter-rouge">apt install steam</code> propose removing the desktop environment, the prompt appeared <a href="https://www.youtube.com/watch?v=0506yDSgU7M&amp;t=597s">in a Linus Tech Tips video</a>, and the phrase got typed anyway. <a href="https://salsa.debian.org/apt-team/apt/-/merge_requests/199">apt 2.3.12</a> replaced the prompt with a hard error two weeks later; the NEWS entry credits Linus Tech Tips and System76 by name.</p>

<p><a href="https://peps.python.org/pep-0668/">PEP 668</a> says the escape hatch for externally-managed environments “should not be something as simple as a <code class="language-plaintext highlighter-rouge">--force</code> flag”, and pip’s <code class="language-plaintext highlighter-rouge">--break-system-packages</code>, <a href="https://github.com/pypa/pip/pull/11780">added in 23.0.1</a>, is long-only with an error message that takes half a screen to steer users away. But pip maps every long option to both an environment variable and a config-file key automatically, with no per-option opt-out. <code class="language-plaintext highlighter-rouge">PIP_BREAK_SYSTEM_PACKAGES=1</code> in <code class="language-plaintext highlighter-rouge">.bashrc</code> or <code class="language-plaintext highlighter-rouge">break-system-packages = true</code> in <code class="language-plaintext highlighter-rouge">pip.conf</code> sets it permanently, and the <a href="https://github.com/pypa/pip/blob/main/tests/functional/test_pep668.py">pip test suite itself</a> has to clear the env var to test the un-overridden path.</p>

<p>Node.js tags each option individually in <a href="https://github.com/nodejs/node/blob/main/src/node_options.cc"><code class="language-plaintext highlighter-rouge">src/node_options.cc</code></a> as either <code class="language-plaintext highlighter-rouge">kAllowedInEnvvar</code> or <code class="language-plaintext highlighter-rouge">kDisallowedInEnvvar</code>, and exits with an error if a disallowed one arrives through <code class="language-plaintext highlighter-rouge">NODE_OPTIONS</code>. <code class="language-plaintext highlighter-rouge">--eval</code>, <code class="language-plaintext highlighter-rouge">--print</code>, <code class="language-plaintext highlighter-rouge">--interactive</code> and anything that names a script to run are on the <a href="https://nodejs.org/api/cli.html#node_optionsoptions">disallowed list</a>, and the <a href="https://github.com/nodejs/node/pull/12028">PR that introduced the mechanism</a> put <code class="language-plaintext highlighter-rouge">--tls-cipher-list</code> there too with the one-line rationale “Disallowed because of security concerns”. cargo’s config reference marks registry <code class="language-plaintext highlighter-rouge">[source]</code> replacement and <code class="language-plaintext highlighter-rouge">[patch]</code> tables the same way, <a href="https://doc.rust-lang.org/cargo/reference/config.html">“Environment: not supported”</a>, so pointing a build at a different crate source takes a file on disk rather than an exported variable. npm 12 restricts a different channel: passing <code class="language-plaintext highlighter-rouge">--allow-scripts</code> on the command line in a project-scoped install throws <a href="https://github.com/npm/cli/pull/9424"><code class="language-plaintext highlighter-rouge">EALLOWSCRIPTS</code></a>, forcing the policy into <code class="language-plaintext highlighter-rouge">package.json</code> where it’s checked in and reviewed.</p>

<p>Daniel Stenberg made the case against relying on names alone in 2017, when a curl user <a href="https://curl.se/mail/archive-2017-04/0002.html">proposed</a> deprecating <code class="language-plaintext highlighter-rouge">-k</code> and keeping only <code class="language-plaintext highlighter-rouge">--insecure</code>, on the grounds that a two-character flag is hard to spot in a script and easy to insert. Stenberg <a href="https://curl.se/mail/archive-2017-04/0004.html">declined</a>: the misuse comes from copy-paste, users transplant <code class="language-plaintext highlighter-rouge">-k</code> from a Stack Overflow answer without reading it, and they’d transplant <code class="language-plaintext highlighter-rouge">--insecure</code> just as readily. Adding a warning would produce fatigue rather than caution. curl 8.x still accepts <code class="language-plaintext highlighter-rouge">-k</code>, though the equivalent <code class="language-plaintext highlighter-rouge">CURL_INSECURE</code> environment variable is absent; every entry in the man page’s <a href="https://curl.se/docs/manpage.html#ENVIRONMENT">ENVIRONMENT section</a> leaves verification on.</p>

<h2 id="preconditions-and-scope">Preconditions and scope</h2>

<p><code class="language-plaintext highlighter-rouge">git push --force-with-lease</code> attaches a precondition rather than relying on spelling: override only if the remote ref matches what I last fetched, so a force-push fails if someone else pushed in the meantime. That check turned out to have a hole: editors with background auto-fetch update the tracking ref without the user seeing the new commits, so the lease matches even though the user’s mental model is stale. git 2.30 added <a href="https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-if-includes"><code class="language-plaintext highlighter-rouge">--force-if-includes</code></a>, which additionally requires the remote tip to appear in the local branch’s reflog, so a fetched-but-unread commit still blocks the push.</p>

<p><code class="language-plaintext highlighter-rouge">go get -insecure</code> applied to everything the command touched, and Go 1.17 <a href="https://go.dev/doc/go1.17">removed it</a> in favour of <a href="https://go.dev/ref/mod#environment-variables"><code class="language-plaintext highlighter-rouge">GOINSECURE</code></a>, which takes a comma-separated list of module path globs so unverified fetches only apply to matching paths. pacman made the same move: the boolean <code class="language-plaintext highlighter-rouge">--force</code> that overrode file-conflict checks was removed in 5.1 and replaced with <a href="https://man.archlinux.org/man/pacman.8"><code class="language-plaintext highlighter-rouge">--overwrite &lt;glob&gt;</code></a>, which has to name what it’s clobbering. Nix’s <a href="https://nixos.org/manual/nixpkgs/stable/#sec-allow-insecure"><code class="language-plaintext highlighter-rouge">permittedInsecurePackages</code></a> requires the versioned package name, <code class="language-plaintext highlighter-rouge">openssl-1.1.1w</code> rather than <code class="language-plaintext highlighter-rouge">openssl</code>, so when the version changes the exception stops matching and the build fails again until someone re-approves it. Composer 2.2’s <a href="https://getcomposer.org/doc/06-config.md#allow-plugins"><code class="language-plaintext highlighter-rouge">allow-plugins</code></a> is a per-plugin map in <code class="language-plaintext highlighter-rouge">composer.json</code> rather than a global switch. cargo’s <code class="language-plaintext highlighter-rouge">[patch]</code> table is per-crate.</p>

<p><code class="language-plaintext highlighter-rouge">systemctl reboot --force</code> skips the orderly shutdown of units, and passing <code class="language-plaintext highlighter-rouge">--force</code> twice <a href="https://www.freedesktop.org/software/systemd/man/latest/systemctl.html#-f">skips systemd itself</a>, issuing the <code class="language-plaintext highlighter-rouge">reboot(2)</code> syscall directly from the systemctl process so it works even when PID 1 has hung.</p>

<p>Ceph’s pool-deletion command stacks three mechanisms: <code class="language-plaintext highlighter-rouge">ceph osd pool delete NAME NAME --yes-i-really-really-mean-it</code>, with the pool name given twice, and the monitor on the server side still refuses unless <a href="https://docs.ceph.com/en/latest/rados/operations/pools/#deleting-a-pool"><code class="language-plaintext highlighter-rouge">mon_allow_pool_delete</code></a> is set to true in its configuration.</p>

<h2 id="out-of-band">Out of band</h2>

<p>Docker keeps <a href="https://docs.docker.com/reference/cli/dockerd/"><code class="language-plaintext highlighter-rouge">insecure-registries</code></a> in the daemon config only, so pulling from an unverified registry means reconfiguring the daemon. A git server with <a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-receivedenyNonFastForwards"><code class="language-plaintext highlighter-rouge">receive.denyNonFastForwards</code></a> or <code class="language-plaintext highlighter-rouge">receive.denyDeletes</code> set rejects a force-push regardless of what flags the client sent. Homebrew’s <code class="language-plaintext highlighter-rouge">HOMEBREW_FORBIDDEN_FORMULAE</code> and siblings let an admin block installs, and <code class="language-plaintext highlighter-rouge">HOMEBREW_FORBIDDEN_OWNER</code> <a href="https://docs.brew.sh/Manpage#environment">names who set the policy</a> so the error message tells the user who to ask rather than what to type. Set in <code class="language-plaintext highlighter-rouge">/etc/homebrew/brew.env</code> alongside <a href="https://github.com/Homebrew/brew/pull/15787"><code class="language-plaintext highlighter-rouge">HOMEBREW_SYSTEM_ENV_TAKES_PRIORITY</code></a>, the system file is applied after the user’s shell environment and overrides it, so a value exported in the shell is discarded.</p>

<p><code class="language-plaintext highlighter-rouge">csrutil disable</code> has always required booting to Recovery. <code class="language-plaintext highlighter-rouge">spctl --master-disable</code> used to turn off Gatekeeper from a normal terminal, but on macOS 15 it <a href="https://derflounder.wordpress.com/2024/09/23/spctl-command-line-tool-no-longer-able-to-manage-gatekeeper-on-macos-sequoia/">prints “This operation is no longer supported”</a> and directs the user to System Settings; a persistent global disable now needs an MDM configuration profile or an interactive System Settings change rather than a scriptable command.</p>

<p>sudo’s credential cache expires instead of requiring a separate channel: <a href="https://www.sudo.ws/docs/man/sudoers.man/">five minutes by default</a> per terminal. <code class="language-plaintext highlighter-rouge">Set-ExecutionPolicy -Scope Process</code> in PowerShell lasts for the shell session, and GitHub’s sudo mode for sensitive account settings re-prompts after a couple of hours. Ceph’s <code class="language-plaintext highlighter-rouge">injectargs</code>, which is how you set <code class="language-plaintext highlighter-rouge">mon_allow_pool_delete</code> without a monitor restart, is cleared when the monitor restarts. Putting the examples against the same six properties:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: center">Long name, no short form</th>
      <th style="text-align: center">No env-var route</th>
      <th style="text-align: center">Must name target</th>
      <th style="text-align: center">Lapses</th>
      <th style="text-align: center">Checks state</th>
      <th style="text-align: center">Out of band</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rm --no-preserve-root</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>pip <code class="language-plaintext highlighter-rouge">--break-system-packages</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>apt <code class="language-plaintext highlighter-rouge">--allow-remove-essential</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>pnpm <code class="language-plaintext highlighter-rouge">dangerouslyAllowAllBuilds</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>curl <code class="language-plaintext highlighter-rouge">-k</code> / <code class="language-plaintext highlighter-rouge">--insecure</code></td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>cargo <code class="language-plaintext highlighter-rouge">[source]</code> replacement</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>git <code class="language-plaintext highlighter-rouge">--force-with-lease</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>pacman <code class="language-plaintext highlighter-rouge">--overwrite &lt;glob&gt;</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>Go <code class="language-plaintext highlighter-rouge">GOINSECURE</code></td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>Composer <code class="language-plaintext highlighter-rouge">allow-plugins</code></td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>Nix <code class="language-plaintext highlighter-rouge">permittedInsecurePackages</code></td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>sudo credential cache</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td>Docker <code class="language-plaintext highlighter-rouge">insecure-registries</code></td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
    </tr>
    <tr>
      <td>git <code class="language-plaintext highlighter-rouge">receive.denyNonFastForwards</code></td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
    </tr>
    <tr>
      <td>Homebrew <code class="language-plaintext highlighter-rouge">FORBIDDEN_*</code> + system priority</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
    </tr>
    <tr>
      <td>macOS <code class="language-plaintext highlighter-rouge">csrutil disable</code></td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
    </tr>
    <tr>
      <td>Ceph pool delete</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center">✓</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">✓</td>
    </tr>
  </tbody>
</table>

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

<p>A long unabbreviatable flag catches a fat-fingered <code class="language-plaintext highlighter-rouge">-f</code> and stands out to a reviewer skimming a diff. A TTY check blocks <code class="language-plaintext highlighter-rouge">yes | tool</code>, server-side config ignores whatever flags the client sent, and a typed phrase, on apt’s evidence, stops very little. Refusing an env-var route stops a poisoned parent process, and for package managers specifically that parent is often something the tool itself just installed.</p>

<p>An npm postinstall script, a <code class="language-plaintext highlighter-rouge">setup.py</code>, a <code class="language-plaintext highlighter-rouge">build.rs</code>, a Homebrew formula’s <code class="language-plaintext highlighter-rouge">install</code> block: all of these run with the package manager’s environment and all of them can invoke the package manager again, or export variables that the next invocation will read. When <a href="https://socket.dev/blog/mastra-npm-packages-compromised">140 <code class="language-plaintext highlighter-rouge">@mastra/*</code> npm packages were compromised in June 2026</a> the injected payload set <code class="language-plaintext highlighter-rouge">NODE_TLS_REJECT_UNAUTHORIZED=0</code> in its own process before phoning home, because Node reads that from the environment unconditionally. In <a href="https://www.qualys.com/2024/11/19/needrestart/needrestart.txt">CVE-2024-48990</a> <code class="language-plaintext highlighter-rouge">needrestart</code>, running as root, inherited <code class="language-plaintext highlighter-rouge">PYTHONPATH</code> from the unprivileged processes it was inspecting and executed attacker code with it. Homebrew’s <code class="language-plaintext highlighter-rouge">bin/brew</code> <a href="https://github.com/Homebrew/brew/pull/1753">re-executes itself through <code class="language-plaintext highlighter-rouge">env -i</code></a> with a fixed allowlist before any formula code runs, and separately <a href="https://github.com/Homebrew/brew/blob/master/Library/Homebrew/extend/ENV/sensitive.rb">strips anything matching <code class="language-plaintext highlighter-rouge">token</code>, <code class="language-plaintext highlighter-rouge">key</code>, <code class="language-plaintext highlighter-rouge">password</code>, <code class="language-plaintext highlighter-rouge">cookie</code> or <code class="language-plaintext highlighter-rouge">auth</code></a> from the environment before evaluating tap Ruby. Node’s <code class="language-plaintext highlighter-rouge">kDisallowedInEnvvar</code> list, cargo’s env-unsupported <code class="language-plaintext highlighter-rouge">[source]</code> table, and npm’s <code class="language-plaintext highlighter-rouge">EALLOWSCRIPTS</code> all guard against the case where the package manager’s caller is itself a package.</p>

<p>A global <code class="language-plaintext highlighter-rouge">NIXPKGS_ALLOW_INSECURE=1</code> or <code class="language-plaintext highlighter-rouge">GOINSECURE=*</code> applies to every dependency resolved from that point on, including transitive ones pulled in by other packages. Nix’s version-pinned entries mean an exception granted for <code class="language-plaintext highlighter-rouge">openssl-1.1.1w</code> stops applying when a dependency bumps to a newer vulnerable build, and an entry in Composer’s <code class="language-plaintext highlighter-rouge">allow-plugins</code> map for <code class="language-plaintext highlighter-rouge">phpstan/extension-installer</code> applies to that plugin alone. A boolean override reaches everything downstream of it; a named one reaches what it names.</p>

<p>pip’s PEP 668 error message ends with “You can override this, at the risk of breaking your Python installation or OS, by passing –break-system-packages.” apt’s error, when <code class="language-plaintext highlighter-rouge">-y</code> is passed, reads “Essential packages were removed and -y was used without –allow-remove-essential”, while the interactive error a human at a terminal sees omits the flag, so of apt’s two code paths the automated one is the one told how to bypass. pip’s message and apt’s <code class="language-plaintext highlighter-rouge">-y</code> message both spell out the next command for anything that parses error output and retries: a CI wrapper, a <code class="language-plaintext highlighter-rouge">build.rs</code> shelling out to the system package manager, a provisioning script, or increasingly an agent that reads stderr as instructions.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="security" /><category term="cli" /><summary type="html"><![CDATA[export PIP_BREAK_SYSTEM_PACKAGES=1]]></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: 22 August 2026</title><link href="https://nesbitt.io/2026/08/22/this-week-in-package-management.html" rel="alternate" type="text/html" title="This Week in Package Management: 22 August 2026" /><published>2026-08-22T10:00:00+00:00</published><updated>2026-08-22T10:00:00+00:00</updated><id>https://nesbitt.io/2026/08/22/this-week-in-package-management</id><content type="html" xml:base="https://nesbitt.io/2026/08/22/this-week-in-package-management.html"><![CDATA[<p>Week fourteen 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://go.dev/doc/go1.27">Go 1.27</a> is out. On the module side, <code class="language-plaintext highlighter-rouge">go mod tidy</code> now consolidates duplicate <code class="language-plaintext highlighter-rouge">require</code> blocks into the canonical direct/indirect pair for modules declaring <code class="language-plaintext highlighter-rouge">go 1.27</code> or later, <code class="language-plaintext highlighter-rouge">go doc</code> accepts <code class="language-plaintext highlighter-rouge">package@version</code> to fetch documentation for a specific module version, and the <code class="language-plaintext highlighter-rouge">go</code> command drops support for fetching modules from Bazaar repositories. A <a href="https://go.dev/doc/go1.27#compressflatepkgcompressflate"><code class="language-plaintext highlighter-rouge">compress/flate</code> encoder change</a> also means <code class="language-plaintext highlighter-rouge">archive/zip</code> and <code class="language-plaintext highlighter-rouge">compress/gzip</code> produce different bytes than under Go 1.26, which is worth checking anywhere a Go-built tool’s archive output is hash-pinned downstream (forge tarball endpoints, module proxies, release pipelines).</p>

<p><a href="https://blog.rust-lang.org/2026/08/20/Rust-1.98.0/">Rust 1.98</a> ships Cargo with an unstable <a href="https://github.com/rust-lang/cargo/pull/17012"><code class="language-plaintext highlighter-rouge">-Zmin-publish-age</code></a> flag implementing <a href="https://github.com/rust-lang/rfcs/pull/3923">RFC 3923</a>: resolution filters out crate versions published more recently than a configured threshold. It also fixes a 1.96 Windows regression in <code class="language-plaintext highlighter-rouge">cargo:token-from-stdout</code> credential providers.</p>

<p><a href="https://bun.com/blog/bun-v1.4">Bun 1.4</a> is the first release of the <a href="https://bun.com/blog/bun-in-rust">Rust rewrite</a>, the bulk of which Jarred Sumner produced from the Zig codebase with Claude Code workflows. It fills out the package manager subcommands: <code class="language-plaintext highlighter-rouge">bun pm diff</code> shows un-minified source diffs between package versions and flags new install scripts, <code class="language-plaintext highlighter-rouge">bun audit fix</code> upgrades vulnerable dependencies, <code class="language-plaintext highlighter-rouge">bun dedupe</code> and <code class="language-plaintext highlighter-rouge">bun prune</code> clean up the lockfile and <code class="language-plaintext highlighter-rouge">node_modules</code>, GitHub and tarball dependencies now record SHA-512 hashes in the lockfile, and only packages from the npm registry are auto-trusted to run install scripts by default.</p>

<p><a href="https://github.com/pnpm/pnpm/releases/tag/v11.22.0">pnpm 11.22</a> shipped alongside a <a href="https://pnpm.io/blog/releases/11.21-11.22">11.21–11.22 recap</a>: <code class="language-plaintext highlighter-rouge">pnpm install</code> now edits the lockfile in place for most everyday manifest changes without re-resolving the whole dependency graph, global installs switch over atomically, <code class="language-plaintext highlighter-rouge">pnpm cache path</code> prints the store location, and a project’s <code class="language-plaintext highlighter-rouge">pnpm-workspace.yaml</code> can no longer relocate machine-level state directories. pnpm 12 reached <a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0-rc.8">RC 8</a>: the <code class="language-plaintext highlighter-rouge">registries</code> setting is now keyed by URL with scopes and tarball layout declared per entry, and <code class="language-plaintext highlighter-rouge">packageImportMethod: auto</code> tries hardlinks before reflinks on Linux, roughly halving the time to materialise <code class="language-plaintext highlighter-rouge">node_modules</code> from a warm store on btrfs.</p>

<p><a href="https://hex.pm/blog/hex-v25-released">Hex 2.5</a> surfaces security advisories during <code class="language-plaintext highlighter-rouge">mix deps.get</code> and <code class="language-plaintext highlighter-rouge">mix deps.update</code>, printing a summary of vulnerable packages at the end of the run. A <code class="language-plaintext highlighter-rouge">cooldown</code> setting withholds versions younger than a configured age from resolution, lifted automatically when the currently locked version is itself retired or has an advisory, and organisations can publish signed dependency policies that opted-in projects apply centrally.</p>

<p><a href="https://github.com/renovatebot/renovate/releases/tag/44.39.1">Renovate 44.33.0–44.39.1</a> adds <a href="https://github.com/renovatebot/renovate/pull/44222">PEP 691 JSON simple index</a> support to the PyPI datasource and passes <a href="https://github.com/renovatebot/renovate/pull/43429"><code class="language-plaintext highlighter-rouge">POETRY_SOLVER_MIN_RELEASE_AGE</code></a> through to Poetry when <code class="language-plaintext highlighter-rouge">minimumReleaseAge</code> is configured.</p>

<p>Also out:</p>

<ul>
  <li><a href="https://github.com/gradle/gradle/releases/tag/v9.7.1">Gradle 9.7.1</a></li>
  <li><a href="https://github.com/conda/conda/releases/tag/26.7.1">conda 26.7.1</a></li>
  <li><a href="https://github.com/pdm-project/pdm/releases/tag/2.28.2">PDM 2.28.2</a></li>
  <li><a href="https://github.com/Homebrew/brew/releases/tag/6.0.18">Homebrew 6.0.18</a></li>
  <li><a href="https://github.com/chocolatey/choco/releases/tag/2.7.4">Chocolatey 2.7.4</a></li>
  <li><a href="https://github.com/verdaccio/verdaccio/releases/tag/v6.9.3">Verdaccio 6.9.3</a></li>
  <li><a href="https://github.com/dependabot/dependabot-core/releases/tag/v0.392.0">Dependabot Core 0.392.0</a></li>
  <li><a href="https://github.com/jdx/mise/releases/tag/v2026.8.10">mise 2026.8.10</a></li>
  <li><a href="https://github.com/goharbor/harbor/releases/tag/v2.15.2">Harbor 2.15.2</a></li>
  <li><a href="https://rpm.org/releases/6.1.0">RPM 6.1.0</a></li>
  <li><a href="https://blog.rubygems.org/2026/08/20/4.0.19-released.html">RubyGems 4.0.19</a></li>
  <li><a href="https://github.com/canonical/snapd/releases/tag/2.76.3">snapd 2.76.3</a></li>
  <li><a href="https://github.com/rpm-software-management/dnf5/releases/tag/5.4.4.0">DNF5 5.4.4.0</a></li>
  <li><a href="https://github.com/ocaml/opam/releases/tag/2.6.0-beta1">opam 2.6.0-beta1</a></li>
  <li><a href="https://diffoscope.org/news/diffoscope-329-released/">diffoscope 329</a></li>
</ul>

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

<p>crates.io <a href="https://blog.rust-lang.org/2026/08/20/supply-chain-attack-on-arrayref/">removed</a> malicious versions of <code class="language-plaintext highlighter-rouge">arrayref</code>, <code class="language-plaintext highlighter-rouge">internment</code>, <code class="language-plaintext highlighter-rouge">append-only-vec</code> and several typosquat crates published from a compromised maintainer account. The malicious versions carried a build script that downloaded a remote payload and were live for under two hours before deletion; the post gives a command to check the local Cargo cache for the affected versions.</p>

<p><a href="https://github.com/sbt/sbt/releases/tag/v2.0.6">sbt 2.0.6</a> and <a href="https://github.com/sbt/sbt/releases/tag/v1.12.15">1.12.15</a> fix <a href="https://github.com/sbt/sbt/security/advisories/GHSA-m2pw-22cj-jq4v">GHSA-m2pw-22cj-jq4v</a>, a remote code execution via the sbt server when <code class="language-plaintext highlighter-rouge">serverConnectionType</code> is set to <code class="language-plaintext highlighter-rouge">Tcp</code>, and <a href="https://github.com/sbt/sbt/releases/tag/v2.0.7">2.0.7</a>/<a href="https://github.com/sbt/sbt/releases/tag/v1.13.0">1.13.0</a> fix the same class of bug in the BSP handler (<a href="https://github.com/sbt/sbt/security/advisories/GHSA-943m-f264-54p4">GHSA-943m-f264-54p4</a>). Builds using the default Unix domain socket connection type are unaffected.</p>

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

<p><a href="https://snarky.ca/whats-missing-to-have-reproducible-builds-on-pypi/">What’s missing to have reproducible builds on PyPI</a> (Brett Cannon) sets out three gaps: distributions don’t record the source location they were built from, sdists have no standard place to store build-environment SBOM data the way wheels do, and there’s no channel for independent verifiers to attest to PyPI that they reproduced a distribution.</p>

<p><a href="https://predr.ag/blog/protecting-the-rust-stdlib-from-breakage/">Protecting the Rust standard library from breakage</a> (Predrag Gruevski): rust-lang/rust CI now runs cargo-semver-checks against <code class="language-plaintext highlighter-rouge">core</code>, <code class="language-plaintext highlighter-rouge">alloc</code> and <code class="language-plaintext highlighter-rouge">std</code>, treating <code class="language-plaintext highlighter-rouge">#[stable]</code> and <code class="language-plaintext highlighter-rouge">#[unstable]</code> attributes as the public-API boundary. Getting there meant new stability fields in rustdoc’s JSON output and threading them through the linter’s query layer without rewriting existing lints.</p>

<p><a href="https://spinel.coop/blog/spinel-dev-log-july-2026/">Spinel dev log, July 2026</a>: the Ruby tooling cooperative’s monthly update on <code class="language-plaintext highlighter-rouge">rv</code> (a Ruby version manager with its own <a href="https://github.com/spinel-coop/rv-ruby">precompiled Ruby builds</a>), the Dyad shell helper, and brut-pack for bundling scripts.</p>

<p><a href="https://www.foo.be/2026/08/Open-Source-Metrics.html">How mature is this repository?</a> (Alexandre Dulaunoy) introduces <a href="https://github.com/adulau/osstrl">OSSTRL</a>, which computes a 1–9 Technology Readiness Level for a GitHub repository from automatically gathered evidence across community, governance, development, support and security dimensions, with maturity gates so a single strong signal can’t inflate the overall level.</p>

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

<p><a href="https://arxiv.org/abs/2608.16262">Implicit, Yet Impactful: Understanding Hidden Dependencies in Java Projects</a> (Zhang et al., arXiv) measures transitive Maven dependencies whose classes are referenced directly by a project’s own code without being declared: across 972 GitHub modules, 34% contain at least one, 48% of those introduce breaking API changes, and 36 CVEs in the sample expose vulnerable methods that the root project calls directly.</p>

<p><a href="https://arxiv.org/abs/2608.15886">SMTpip: Interpreter-Aware SMT-Based Dependency Conflict Resolution</a> (Sakib et al., arXiv) encodes both package version constraints and Python interpreter compatibility as SMT formulas so a solver can decide up front whether a satisfying environment exists, reporting a 6.9× speedup over pip’s backtracking resolver on their benchmark.</p>

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

<p><a href="https://opensourcesecurity.io/2026/2026-08-commonhaus-herodevs/">Commonhaus and HeroDevs launch OSSI</a> (Josh Bressers, Open Source Security): an interview with Erin Schnabel and Rob Nalen on a partnership funding CVE remediation and extended support for end-of-life releases of Commonhaus-hosted projects, starting with Hibernate, Jackson and Quarkus.</p>

<p><a href="https://blog.pypi.org/posts/2026-08-14-how-aws-powers-pypi-and-the-psf/">How AWS powers PyPI and the PSF</a> (PyPI blog): AWS’s open source credits programme covers PyPI’s infrastructure bill and its security sponsorship funds engineering time, but PyPI is currently maintained by roughly one and a half full-time engineers plus one on support. A line describing PyPI as a supply chain risk given that staffing was later <a href="https://github.com/pypi/warehouse/commit/bc867eed">reworded</a>.</p>

<p><a href="https://podcast.thoughtbot.com/619">Inside Modern Software Engineering with Homebrew’s Mike McQuaid</a> (Giant Robots podcast): an interview covering Homebrew’s history and open source maintainership.</p>

<p><a href="https://podcast.sustainoss.org/293">Sustain #293</a> has Daniel Roe and Matias Capeletto on <a href="https://npmx.dev">npmx</a>, the community-built npm registry browser, covering how the project reached hundreds of contributors since January and its governance and funding.</p>

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

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

<ul>
  <li><a href="https://github.com/git-pkgs/artifacts/releases/tag/v0.1.1">artifacts v0.1.1</a> (new), a Go library that describes a completed package file as a canonical package URL, an OCI-form content digest and a byte count, with optional filename and media type carried alongside</li>
  <li><a href="https://github.com/git-pkgs/integrity/releases/tag/v0.1.1">integrity v0.1.1</a> (new), a Go library that parses Subresource Integrity metadata (SHA-256/384/512, standard or URL-safe base64) and verifies package byte streams against it as they are read</li>
  <li><a href="https://github.com/git-pkgs/brief/releases/tag/v0.11.0">brief v0.11.0</a></li>
  <li><a href="https://github.com/git-pkgs/clone/releases/tag/v0.5.0">clone v0.5.0</a></li>
  <li><a href="https://github.com/git-pkgs/cooldown/releases/tag/v0.2.0">cooldown v0.2.0</a></li>
  <li><a href="https://github.com/git-pkgs/enrichment/releases/tag/v0.7.0">enrichment v0.7.0</a></li>
  <li><a href="https://github.com/git-pkgs/forge/releases/tag/v0.9.0">forge v0.9.0</a></li>
  <li><a href="https://github.com/git-pkgs/licenses/releases/tag/v0.6.0">licenses v0.6.0</a></li>
  <li><a href="https://github.com/git-pkgs/managers/releases/tag/v0.11.0">managers v0.11.0</a></li>
  <li><a href="https://github.com/git-pkgs/manifests/releases/tag/v0.10.0">manifests v0.10.0</a></li>
  <li><a href="https://github.com/git-pkgs/pin/releases/tag/v0.2.0">pin v0.2.0</a></li>
  <li><a href="https://github.com/git-pkgs/pom/releases/tag/v0.1.7">pom v0.1.7</a></li>
  <li><a href="https://github.com/git-pkgs/purl/releases/tag/v0.1.17">purl v0.1.17</a></li>
  <li><a href="https://github.com/git-pkgs/registries/releases/tag/v0.8.1">registries v0.8.1</a></li>
  <li><a href="https://github.com/git-pkgs/sigstore/releases/tag/v0.2.0">sigstore v0.2.0</a></li>
  <li><a href="https://github.com/git-pkgs/vers/releases/tag/v0.6.0">vers v0.6.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">Issues in the Repo</title><link href="https://nesbitt.io/2026/08/20/issues-in-the-repo.html" rel="alternate" type="text/html" title="Issues in the Repo" /><published>2026-08-20T09:00:00+00:00</published><updated>2026-08-20T09:00:00+00:00</updated><id>https://nesbitt.io/2026/08/20/issues-in-the-repo</id><content type="html" xml:base="https://nesbitt.io/2026/08/20/issues-in-the-repo.html"><![CDATA[<p>GitHub had a rough Monday this week, with git operations, Actions, and the issue tracker all unavailable for <a href="https://www.githubstatus.com/incidents/zkxwbgr0cnmx">several hours</a>. Code being unreachable during a forge outage is annoying but survivable because every contributor already has a full clone. Issues and pull request threads going dark is a different matter, since for most projects those exist only in GitHub’s database and nowhere else. I <a href="https://mastodon.social/@andrewnez/117111291448638063">asked on Mastodon</a> about tools that keep issue and review data inside the repository so it clones and pushes with the code, and got pointed at more projects than I expected, spanning about twenty years of people having a go at this.</p>

<p>The projects sort by where they physically put the data, and each storage location brings its own answers to the same handful of questions: whether a plain <code class="language-plaintext highlighter-rouge">git clone</code> fetches it, what happens when two people edit the same issue offline and then both push, whether you can read it without the tool installed, whether it survives being pushed to a bare git host that has no support for the tool, and who counts as the author of an issue when there are no forge accounts. I <a href="/2025/11/26/extending-git-functionality.html">wrote about git’s extension points</a> last year and several of these tools use the custom-ref mechanism from that post, but here I’m looking at the data model rather than the plumbing.</p>

<h3 id="files-in-the-working-tree">Files in the working tree</h3>

<p>The oldest approach is to make each issue a text file, or a directory of files, checked in next to the source. <a href="https://bugs-everywhere.readthedocs.io/">Bugs Everywhere</a> did this in 2005 for Bazaar and later added git and Mercurial backends, and <a href="https://rubygems.org/gems/ditz">ditz</a> stored issues as YAML from 2008. More recently <a href="https://gitroot.dev">GitRoot</a> builds an entire self-hosted forge on the same idea, keeping issues as <code class="language-plaintext highlighter-rouge">issues/&lt;id&gt;-&lt;slug&gt;.md</code> with front-matter status fields, users and permissions as YAML under <code class="language-plaintext highlighter-rouge">.gitroot/</code>, and a small server that renders the lot as a web UI. <a href="https://mastodon.social/@mistersql/117111351359071242">Matthew Martin</a> pointed at his own <a href="https://github.com/matthewdeanmartin/keepachangelog-manager/tree/main/tickets">ticket directory</a> that reuses keep-a-changelog fragments as lightweight work items with no tooling at all, which is about as minimal as this pattern gets.</p>

<p>In Diomidis Spinellis’s <a href="https://github.com/dspinellis/git-issue">git-issue</a> the <code class="language-plaintext highlighter-rouge">.issues/</code> directory is itself a nested git repository with its own <code class="language-plaintext highlighter-rouge">.git</code>, sha-bucketed as <code class="language-plaintext highlighter-rouge">issues/ef/1a04f.../description</code>, <code class="language-plaintext highlighter-rouge">.../tags</code>, <code class="language-plaintext highlighter-rouge">.../assignee</code> and so on. That keeps the outer project’s history untouched, though the outer clone then doesn’t carry the issues at all.</p>

<p>Because the issues are ordinary tracked files, everything git already does works with no extra configuration: <code class="language-plaintext highlighter-rouge">git clone</code> fetches them, <code class="language-plaintext highlighter-rouge">grep -r</code> searches them, <code class="language-plaintext highlighter-rouge">cat</code> reads them, <code class="language-plaintext highlighter-rouge">git bundle</code> packs them, any dumb HTTP host or cgit instance serves them, and <code class="language-plaintext highlighter-rouge">git log -p .issues/</code> shows who changed what and when. Concurrent edits to the same issue become a normal three-way text merge. That works cleanly when two people append separate comments to the bottom of a file, and produces a real conflict when they both change the title line, which you resolve the same way you’d resolve a conflict in code. The author of an issue is whoever <code class="language-plaintext highlighter-rouge">git blame</code> says created the file, backed by whatever commit signing the project already uses, so identity comes from git’s existing model rather than needing its own.</p>

<p>The drawback is that issue history and code history share one commit graph. On a busy project <code class="language-plaintext highlighter-rouge">git log</code> fills with “reword issue #34” interleaved with real changes, <code class="language-plaintext highlighter-rouge">git bisect</code> steps through issue-edit commits, and creating a feature branch forks the issue state along with the code, so an issue closed on the branch shows as open again the moment you switch back to main. Most of these tools work around the log noise by making each issue operation its own commit with a conventional prefix that’s easy to <code class="language-plaintext highlighter-rouge">--invert-grep</code> away, but the branching problem is unavoidable while mutable metadata is in the same tree as the code it describes.</p>

<h3 id="orphan-branch">Orphan branch</h3>

<p>An orphan branch is a ref under <code class="language-plaintext highlighter-rouge">refs/heads/</code> whose root commit has no parent, so it shares no history with <code class="language-plaintext highlighter-rouge">main</code> and its files never appear in a normal checkout. Scott Chacon’s <a href="https://github.com/schacon/ticgit">ticgit</a> from 2008 and the later <a href="https://github.com/jeffWelling/ticgit">ticgit-ng</a> fork keep issues on a branch literally called <code class="language-plaintext highlighter-rouge">ticgit</code>, with each issue as a directory in that branch’s tree, and GitHub’s old <code class="language-plaintext highlighter-rouge">gh-pages</code> convention used the same mechanism for documentation. The very new <a href="https://github.com/xit-vcs/haxy">haxy</a> forge from the xit project puts an event log on <code class="language-plaintext highlighter-rouge">refs/heads/haxy/events</code> where each commit has an empty tree and the commit message is a JSON event describing an issue creation, comment, or status change; the server replays the log to build its state.</p>

<p>The tool reads and writes the branch through plumbing or a second worktree, so <code class="language-plaintext highlighter-rouge">git log</code> on <code class="language-plaintext highlighter-rouge">main</code> stays free of issue noise, merging concurrent edits works the same as for any files on a branch, and the issue author is whoever authored the commit on the orphan branch. Because the ref is under <code class="language-plaintext highlighter-rouge">refs/heads/</code> a default <code class="language-plaintext highlighter-rouge">git clone</code> fetches it automatically, which is the one clear advantage this has over the notes and custom-ref approaches below.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git ls-tree ticgit
<span class="go">040000 tree 9b721cc...  1206206148_add-attachment-to-ticket_138
040000 tree 5d14214...  1206206166_download-attached-file_112

</span><span class="gp">$</span><span class="w"> </span>git ls-tree ticgit:1206206148_add-attachment-to-ticket_138
<span class="go">100644 blob cfdb466...  ASSIGNED_schacon@gmail.com
100644 blob 3074cf0...  COMMENT_1206206148_schacon@gmail.com
100644 blob f510327...  STATE_open
100644 blob 6d44874...  TAG_attach
100644 blob 9ebd07e...  TICKET_ID
</span></code></pre></div></div>

<p>Ticgit encodes almost everything in filenames (<code class="language-plaintext highlighter-rouge">STATE_open</code>, <code class="language-plaintext highlighter-rouge">TAG_attach</code>, <code class="language-plaintext highlighter-rouge">ASSIGNED_&lt;email&gt;</code>) so <code class="language-plaintext highlighter-rouge">git ls-tree</code> alone shows the state of an issue without reading any blobs. Haxy puts everything in the commit message instead, so <code class="language-plaintext highlighter-rouge">git log haxy/events</code> is the entire database, and in both cases the data is legible with stock git and no extra tool installed.</p>

<p>The trouble with an orphan branch is that it shows up in <code class="language-plaintext highlighter-rouge">git branch -a</code> looking like any other branch, so on a project with enough contributors someone will eventually delete the “stale” <code class="language-plaintext highlighter-rouge">ticgit</code> branch while tidying up. A <code class="language-plaintext highlighter-rouge">git push --prune</code> or <code class="language-plaintext highlighter-rouge">git push --mirror</code> from a clone made with <code class="language-plaintext highlighter-rouge">--single-branch</code>, or from a CI job that only fetched <code class="language-plaintext highlighter-rouge">main</code>, deletes it on the remote because the local side has no matching ref. Branch protection on most forges is pattern-based (<code class="language-plaintext highlighter-rouge">main</code>, <code class="language-plaintext highlighter-rouge">release/*</code>) and won’t cover a branch called <code class="language-plaintext highlighter-rouge">ticgit</code> unless someone remembers to add it.</p>

<p>Checking the orphan out in your only working tree swaps every file for the issue files, which is why the tools read it through <code class="language-plaintext highlighter-rouge">git show ticgit:path</code> rather than <code class="language-plaintext highlighter-rouge">git checkout</code>. The objects are safe from <code class="language-plaintext highlighter-rouge">git gc</code> for as long as the ref exists, but once the ref is deleted the entire disconnected graph becomes unreachable and the next <code class="language-plaintext highlighter-rouge">gc --prune</code> removes it, and on a stock bare host there is no server-side reflog to recover from.</p>

<h3 id="git-notes">git notes</h3>

<p>Git ships with a <a href="https://git-scm.com/docs/git-notes">notes facility</a> that attaches arbitrary blobs to existing commits without rewriting them, stored under <code class="language-plaintext highlighter-rouge">refs/notes/&lt;namespace&gt;</code>. The notes ref points at an ordinary commit whose tree maps each annotated commit’s SHA to a blob, so adding a note is itself a commit and the full edit history is kept. Google’s now-archived <a href="https://github.com/google/git-appraise">git-appraise</a> built distributed code review entirely on this: review requests are line-delimited JSON under <code class="language-plaintext highlighter-rouge">refs/notes/devtools/reviews</code>, threaded comments under <code class="language-plaintext highlighter-rouge">refs/notes/devtools/discuss</code>, and CI status under <code class="language-plaintext highlighter-rouge">refs/notes/devtools/ci</code>, all attached to the commit being reviewed.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git <span class="k">for</span><span class="nt">-each-ref</span> refs/notes/
<span class="go">e70fe480... commit  refs/notes/review

</span><span class="gp">$</span><span class="w"> </span>git cat-file <span class="nt">-p</span> refs/notes/review^<span class="o">{</span>tree<span class="o">}</span>
<span class="go">100644 blob 677b652c...  0a72c46f9a191c6097f708ccdabcd6af13eebede

</span><span class="gp">$</span><span class="w"> </span>git cat-file <span class="nt">-p</span> 677b652c
<span class="go">{"timestamp":"2026-08-18T00:00:00Z","reviewRef":"refs/heads/main",
 "targetRef":"refs/heads/master","description":"Demo review request",
 "requester":"andrew@example.com"}
</span></code></pre></div></div>

<p>Notes are not fetched by a default clone; you add <code class="language-plaintext highlighter-rouge">fetch = +refs/notes/*:refs/notes/*</code> to the remote config once and then they travel with every pull and push. Merging has first-class support in git itself via <code class="language-plaintext highlighter-rouge">git notes merge</code>, and the <code class="language-plaintext highlighter-rouge">cat_sort_uniq</code> strategy concatenates and dedupes conflicting notes line by line, which is why git-appraise stores one JSON object per line rather than a single pretty-printed document. Any git host accepts the ref, <code class="language-plaintext highlighter-rouge">git log --notes='*'</code> shows notes inline under each commit, and <code class="language-plaintext highlighter-rouge">git bundle create backup.bundle --all</code> packs them. Identity is the author of the notes commit, plus whatever the payload records; git-appraise puts the requester’s email in the JSON. The structural limitation is that a note has to hang off an existing commit, which suits code review and CI results very well and suits a bug report that isn’t about any particular commit rather less.</p>

<h3 id="custom-ref-namespaces">Custom ref namespaces</h3>

<p>The approach most of the currently-active projects have settled on is to store data under a ref namespace outside <code class="language-plaintext highlighter-rouge">refs/heads/</code>, so nothing shows up in <code class="language-plaintext highlighter-rouge">git branch</code>, nothing touches the working tree, and the tool controls the object layout completely. <a href="https://github.com/git-bug/git-bug">git-bug</a> is the most developed example: each bug is a chain of commits under <code class="language-plaintext highlighter-rouge">refs/bugs/&lt;id&gt;</code>, each commit’s tree holds an <code class="language-plaintext highlighter-rouge">ops</code> blob of JSON operations plus zero-byte marker files whose names encode Lamport clock values, and user identities are separate chains under <code class="language-plaintext highlighter-rouge">refs/identities/&lt;id&gt;</code>.</p>

<p>Gerrit’s <a href="https://gerrit-review.googlesource.com/Documentation/note-db.html">NoteDb</a> keeps every change’s review metadata as a commit graph at <code class="language-plaintext highlighter-rouge">refs/changes/NN/NNNN/meta</code> and has run Google’s own code review without a SQL database since Gerrit 3.0. <a href="https://radicle.xyz">Radicle</a> stores issues and patches as collaborative objects under per-peer <code class="language-plaintext highlighter-rouge">refs/namespaces/&lt;nodeid&gt;/refs/cobs/</code> and merges them over a gossip protocol rather than push. <a href="https://github.com/remenoscodes/git-native-issue">git-native-issue</a> keeps each issue as a commit chain under <code class="language-plaintext highlighter-rouge">refs/issues/&lt;uuid&gt;</code> where the tree is empty and the metadata is carried in commit-message trailers (<code class="language-plaintext highlighter-rouge">State: open</code>, <code class="language-plaintext highlighter-rouge">Labels: bug</code>) rather than JSON, with a published <a href="https://github.com/remenoscodes/git-native-issue/blob/main/ISSUE-FORMAT.md">format spec</a> intended to be readable by any tool. Even the centralised forges use this layer for the code half of a pull request, exposing <code class="language-plaintext highlighter-rouge">refs/pull/N/head</code> on GitHub and <code class="language-plaintext highlighter-rouge">refs/merge-requests/N/head</code> on GitLab, though those are written by the forge and read-only to clients.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git fetch origin <span class="s1">'refs/bugs/*:refs/bugs/*'</span> <span class="s1">'refs/identities/*:refs/identities/*'</span>
<span class="gp">$</span><span class="w"> </span>git <span class="k">for</span><span class="nt">-each-ref</span> refs/bugs/ | <span class="nb">head</span> <span class="nt">-1</span>
<span class="go">aecc49e1... commit  refs/bugs/0066216d1e0cc97ad1c9eaa553459ca85a0d57e133e...

</span><span class="gp">$</span><span class="w"> </span>git cat-file <span class="nt">-p</span> aecc49e1^<span class="o">{</span>tree<span class="o">}</span>
<span class="go">100644 blob e69de29b...  create-clock-354
100644 blob e69de29b...  edit-clock-2314
100644 blob 588a2a80...  ops
100644 blob e69de29b...  version-4

</span><span class="gp">$</span><span class="w"> </span>git cat-file <span class="nt">-p</span> 588a2a80
<span class="go">{"author":{"id":"193531e4590156..."},
 "ops":[{"type":1,"timestamp":1722433238,
         "title":"Issue running the example_test.go",
         "message":"Hi y'all, I'm really excited...","files":null}]}
</span></code></pre></div></div>

<p>A default clone skips these refs entirely, so the first thing every tool does is add its refspec to <code class="language-plaintext highlighter-rouge">.git/config</code>. After that the data pushes to and pulls from any git host that accepts arbitrary ref names, which today is all of the major ones. Every clone that adds the refspec carries the full issue history, which is 454 refs for git-bug’s own repo but would be considerably more on a project with the issue volume of rails or kubernetes.</p>

<p>Because each edit is appended as a new commit rather than modifying an existing blob, two people editing the same bug offline produce two divergent tips that the tool merges by replaying both operation logs against the Lamport clocks, and there is never a text-level conflict to resolve by hand. That also means that when two people set the same field offline the replay picks a winner by clock order without surfacing a conflict. The raw objects are inspectable with <code class="language-plaintext highlighter-rouge">git cat-file</code> as above but not really readable without the tool. Identity is handled inside the data model: git-bug stores name, avatar and login in a versioned identity object, and Radicle derives it from the node’s Ed25519 key.</p>

<p>git-bug also ships <a href="https://github.com/git-bug/git-bug/blob/master/doc/usage/third-party.md">bridges</a> that import from and export to GitHub, GitLab and Jira, so an existing tracker can be mirrored into <code class="language-plaintext highlighter-rouge">refs/bugs/</code> and kept usable through exactly the kind of outage that started this post. Forgejo has an <a href="https://codeberg.org/forgejo/forgejo/issues/2629">open feature request</a> for storing its own issues this way, and one proposal in that discussion is to treat <code class="language-plaintext highlighter-rouge">refs/issues/*</code> as the canonical store with the forge’s SQL tables rebuilt from it as a query index.</p>

<h3 id="built-into-the-vcs">Built into the VCS</h3>

<p><a href="https://fossil-scm.org">Fossil</a> was designed from the start with tickets, wiki pages, forum threads and technotes as artifact types stored alongside check-ins in the same content-addressed store, all packed into a single SQLite database file per repository. A ticket exists as a set of immutable artifacts that each declare changes to named fields, and the current state of a ticket is computed by applying every artifact with a matching ticket id in timestamp order. SourceGear’s <a href="https://web.archive.org/web/20160305220839/http://veracity-scm.com/">Veracity</a> took a similar line around 2011 with a distributed bug database synced alongside the source DAG, though the project has since been discontinued.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>fossil artifact 94cd0c6265
<span class="go">D 2026-08-18T06:37:37.865
J comment Something\sis\sbroken
J priority Medium
J status Open
J title Demo\sbug
J type Code_Defect
K 821e9e51da937dc2c37227ef8ed57c757e7530f0
U andrew
Z 1c440428fff5e32c7c5304e9f6cd5bec
</span></code></pre></div></div>

<p>The <a href="https://fossil-scm.org/home/doc/trunk/www/fileformat.wiki#tktchng">card format</a> is one letter per line type: <code class="language-plaintext highlighter-rouge">D</code> is the timestamp, each <code class="language-plaintext highlighter-rouge">J</code> is a field assignment with spaces escaped as <code class="language-plaintext highlighter-rouge">\s</code>, <code class="language-plaintext highlighter-rouge">K</code> is the id of the ticket this artifact modifies, <code class="language-plaintext highlighter-rouge">U</code> is the user, and <code class="language-plaintext highlighter-rouge">Z</code> is an MD5 of the preceding lines. Syncing exchanges any artifacts the other side lacks, concurrent edits to the same ticket both survive as separate artifacts, and because each <code class="language-plaintext highlighter-rouge">J</code> card sets one field the effective merge policy is last-writer-wins per field with no conflict markers.</p>

<p>Everything travels with <code class="language-plaintext highlighter-rouge">fossil sync</code> or <code class="language-plaintext highlighter-rouge">fossil clone</code> because there is only one file. Identity is the <code class="language-plaintext highlighter-rouge">U</code> card backed by Fossil’s own login system, and the whole thing is readable with <code class="language-plaintext highlighter-rouge">fossil ticket show</code> or by opening the SQLite file directly. The obvious drawback is that none of this is git, so it composes with git-based tooling only through <a href="https://fossil-scm.org/home/doc/trunk/www/inout.wiki">import and export</a>.</p>

<h3 id="git-bundle">git bundle</h3>

<p><a href="https://mastodon.social/@aslakr/117111324600445481">Aslak Raanes</a> asked in the thread whether these approaches survive <a href="https://git-scm.com/docs/git-bundle"><code class="language-plaintext highlighter-rouge">git bundle</code></a>, which is the closest thing git has to Fossil’s single-file repository and the easiest way to get an offline backup onto a USB stick. I tried it: <code class="language-plaintext highlighter-rouge">git bundle create backup.bundle --all</code> on a clone of the git-bug repo with <code class="language-plaintext highlighter-rouge">refs/bugs/*</code> and <code class="language-plaintext highlighter-rouge">refs/identities/*</code> fetched packs all 454 bug refs and every identity into the bundle, and the same on a repo with <code class="language-plaintext highlighter-rouge">refs/notes/review</code> includes the notes ref. That works because <code class="language-plaintext highlighter-rouge">--all</code> to <code class="language-plaintext highlighter-rouge">git rev-list</code> means everything under <code class="language-plaintext highlighter-rouge">refs/</code>, and a bundle is just a packfile with a ref list on the front. So for every git-based approach here, files in tree, orphan branches, notes, and custom refs alike, one bundle file holds the code and the issue tracker together.</p>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="git" /><category term="tools" /><category term="reference" /><summary type="html"><![CDATA[refs/bugs, refs/notes, refs/heads/ticgit, or a directory full of YAML.]]></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">Two-Factor Authentication Across Package Registries</title><link href="https://nesbitt.io/2026/08/18/two-factor-authentication-across-package-registries.html" rel="alternate" type="text/html" title="Two-Factor Authentication Across Package Registries" /><published>2026-08-18T10:00:00+00:00</published><updated>2026-08-18T10:00:00+00:00</updated><id>https://nesbitt.io/2026/08/18/two-factor-authentication-across-package-registries</id><content type="html" xml:base="https://nesbitt.io/2026/08/18/two-factor-authentication-across-package-registries.html"><![CDATA[<p>This month npm <a href="https://docs.npmjs.com/about-two-factor-authentication/">stopped accepting</a> bypass-2FA tokens for account-governance actions, one of the steps in <a href="https://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain/">the plan GitHub set out last September</a> to close the remaining routes that let a reusable credential bypass 2FA on npm. I went through the other <a href="https://packages.ecosyste.ms/registries">registries ecosyste.ms tracks</a> with more than ten thousand packages to see where each is on the same question, and the answer mostly follows from whose accounts a registry uses, an axis I’d left out when <a href="/2025/12/29/categorizing-package-registries.html">categorising registries</a> last year: its own, an OAuth provider’s, a git forge’s, or none.</p>

<h2 id="registries-with-their-own-accounts">Registries with their own accounts</h2>

<p>npm, PyPI, RubyGems, Packagist, Docker Hub, Hex.pm, Clojars, Hackage, and CPAN’s <a href="https://pause.perl.org/">PAUSE</a> each have a username-and-password identity system of their own, so 2FA on each has been implemented separately with its own enforcement policy. PyPI is the only one where enforcement is complete: 2FA has been <a href="https://blog.pypi.org/posts/2024-01-01-2fa-enforced/">mandatory for every account</a> since 1 January 2024, with TOTP and WebAuthn both accepted, after a run-up that included <a href="https://pypi.org/security-key-giveaway/">distributing 4,000 hardware keys</a> to maintainers of top projects in 2022. Publishing to npm requires either 2FA or a granular access token carrying a bypass flag. This month’s change closes the bypass for account-governance actions, and the September 2025 plan proposes removing it for local publishing as well, though that step has not yet landed. Mandatory enrolment on npm has run in cohorts since 2022: <a href="https://github.blog/security/supply-chain-security/top-100-npm-package-maintainers-require-2fa-additional-security/">top 100 by dependents</a> in February, <a href="https://github.blog/changelog/2022-05-31-top-500-npm-package-maintainers-now-require-2fa/">top 500</a> in May, then <a href="https://github.blog/open-source/enrolling-npm-publishers-enhanced-login-verification-two-factor-authentication-enforcement/">high-impact packages</a> at a million weekly downloads or 500 dependents.</p>

<p><a href="https://guides.rubygems.org/setting-up-multifactor-authentication/">RubyGems</a> supports WebAuthn and TOTP and has <a href="https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html">required it since August 2022</a> for owners of any gem with more than 180 million total downloads, with a per-gem <code class="language-plaintext highlighter-rouge">rubygems_mfa_required</code> metadata flag for maintainers who want to opt the rest of their co-owners in. TOTP has been <a href="https://github.com/composer/packagist/pull/1031">available on Packagist</a> since 2019, and the Packagist team’s <a href="https://blog.packagist.com/an-update-on-composer-packagist-supply-chain-security/">May 2026 post</a> sets out mandatory MFA across packagist.org as the direction, with organisation-level enforcement and a requirement on maintainers of larger packages as the concrete first steps. <a href="https://hex.pm/blog/announcing-two-factor-auth">Hex.pm</a> supports TOTP and offers GitHub login alongside its native accounts. TOTP is already required when publishing over the OAuth device flow, which is the default in the mix and Gleam CLIs, and the <a href="https://hex.pm/blog/deprecating-basic-auth">July 2026 basic-auth deprecation</a> sets 1 November 2026 as the date from which 2FA applies to every write operation on the API.<sup id="fnref:hex"><a href="#fn:hex" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p><a href="https://docs.docker.com/security/2fa/">Docker Hub</a> and <a href="https://github.com/clojars/clojars-web/wiki/Two-Factor-Auth">Clojars</a> each support TOTP as an account option with no mandate, WebAuthn is not available on either, and SMS is not a supported second factor anywhere in this group. Hackage has an <a href="https://github.com/haskell/hackage-server/issues/1265">open issue</a> from November 2023 with a volunteer to implement it, and PAUSE authenticates uploads with a password and sends a notification email on every upload as the check on unexpected activity.</p>

<h2 id="registries-that-delegate-identity-to-a-platform">Registries that delegate identity to a platform</h2>

<p><a href="https://crates.io/">crates.io</a> authenticates exclusively through GitHub OAuth, so a crates.io login carries whatever protections the GitHub account behind it has, and the same is true of <a href="https://dart.dev/tools/pub/publishing">pub.dev</a> against Google accounts and the public <a href="https://developer.hashicorp.com/terraform/registry/modules/publish">Terraform Registry</a> against GitHub. There has been <a href="https://github.com/rust-lang/crates.io/discussions/4200">discussion</a> of having crates.io refuse logins from GitHub accounts without 2FA enabled, which the GitHub API exposes, but it has not been implemented. <a href="https://github.com/rust-lang/rfcs/pull/3946">RFC 3946</a>, accepted in May, introduces a crates.io-native username so that an account’s identity is no longer its GitHub username, and the <a href="https://blog.rust-lang.org/2026/07/13/crates-io-development-update/">July 2026 development update</a> reports the implementation as started, which opens the way for other identity providers and would move crates.io towards the first group.</p>

<p><a href="https://learn.microsoft.com/en-us/nuget/nuget-org/individual-accounts">NuGet.org</a> delegates authentication to Microsoft and Azure AD accounts and has <a href="https://devblogs.microsoft.com/dotnet/requiring-two-factor-authentication-on-nuget-org/">required 2FA on the linked identity</a> for new accounts since 8 March 2022, with existing accounts phased in over the following two months: sign-in to NuGet.org redirects to the Microsoft 2FA enrolment screen for any account without it, without changing that account’s global 2FA setting. That makes NuGet the earlier of the two registries in this survey with a universal requirement, and the requirement is NuGet.org’s own policy applied through an identity system it does not run.</p>

<p>Sonatype’s <a href="https://central.sonatype.org/register/central-portal/">Central Publisher Portal</a> for Maven Central, which replaced the old OSSRH JIRA-based signup, supports GitHub and Google social login alongside a portal-native username and password. There is no second factor on the portal’s own login, so a Central account has 2FA only to the extent that a linked GitHub or Google account does, and Sonatype’s 2FA support elsewhere in its product line does not extend to Central. Maven Central separately requires every published artifact to be <a href="https://central.sonatype.org/publish/requirements/gpg/">PGP-signed</a>, which is a control on the artifact rather than the account.</p>

<h2 id="registries-that-are-a-git-repository">Registries that are a git repository</h2>

<p>Homebrew, conda-forge, Spack, nixpkgs, Guix, and Julia’s <a href="https://github.com/JuliaRegistries/General">General registry</a> keep their package definitions <a href="/2025/12/24/package-managers-keep-using-git-as-a-database.html">in a git repository</a> and accept new packages and version bumps as pull requests, so there is no separate registry publisher account and any second factor is on the forge account instead. GitHub Actions, where an action is <a href="/2025/12/06/github-actions-package-manager.html">any repository with an <code class="language-plaintext highlighter-rouge">action.yml</code></a>, works the same way with the marketplace listing on top. For the ones hosted on GitHub the effective policy is GitHub’s contributor 2FA programme, on which GitHub reported in <a href="https://github.blog/2024-04-24-securing-millions-of-developers-through-2fa/">April 2024</a> that 95% of the users targeted through 2023 had enrolled, with the requirement applied to groups selected by “the impact of their user privileges or specific actions they took” rather than to every account, so whether a homebrew-core or conda-forge committer is subject to mandatory 2FA is determined by GitHub’s targeting rather than by anything the registry sets. A committer to one of these repositories necessarily contributes code on GitHub and so falls within the programme’s selection criteria, unlike a crates.io or Terraform Registry publisher who only needs a GitHub login and may hold an account the programme has not reached. Guix has been <a href="https://guix.gnu.org/blog/2025/migrating-to-codeberg/">hosted on Codeberg</a> since May 2025, where <a href="https://docs.codeberg.org/security/2fa/">TOTP and WebAuthn</a> are available with no mandate.</p>

<h2 id="email-gated-and-account-free">Email-gated and account-free</h2>

<p><a href="https://cran.r-project.org/submit.html">CRAN</a> accepts submissions through a web form and email confirmation with human review of every package, CocoaPods trunk authenticates with an <a href="https://guides.cocoapods.org/making/getting-setup-with-trunk.html">emailed session token</a>, and neither has an account database that a second factor would attach to. Trunk is <a href="https://blog.cocoapods.org/CocoaPods-Specs-Repo/">scheduled</a> to stop accepting new podspecs on 2 December 2026 after a read-only trial in the first week of November, which will make the question moot for that registry. The Go module proxy and the Swift Package Index have no publisher accounts at all: <a href="https://proxy.golang.org/">proxy.golang.org</a> is a caching mirror of module versions fetched from their VCS hosts, and the <a href="https://swiftpackageindex.com/">Swift Package Index</a> is a discovery layer over repositories it does not host.</p>

<h2 id="publishing-from-ci">Publishing from CI</h2>

<p>A CI job cannot tap a security key, so publishing from CI on any registry that enforces 2FA at publish time requires a path other than an interactive login. Long-lived API tokens with the 2FA check waived have been that path historically, as with npm’s bypass flag and RubyGems’ automation-scoped keys, and the current round of changes on both registries is narrowing it. Trusted publishing replaces the stored token with a short-lived OIDC token that the registry verifies against a pre-registered CI workflow, so the credential is bound to the workflow that built the artifact and no maintainer credential is involved in the publish. <a href="https://docs.pypi.org/trusted-publishers/">PyPI</a> added it in April 2023, followed by <a href="https://blog.rubygems.org/2023/12/14/trusted-publishing.html">RubyGems</a> in December 2023, <a href="https://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain/">npm</a> and <a href="https://blog.rust-lang.org/2025/07/11/crates-io-development-update-2025-07/">crates.io</a> in 2025, and <a href="https://dart.dev/tools/pub/automated-publishing">pub.dev</a> for GitHub Actions and Google Cloud.</p>

<p>npm’s <a href="https://github.blog/changelog/2026-05-22-staged-publishing-and-new-install-time-controls-for-npm/">staged publishing</a>, generally available since npm CLI 11.15.0 in May 2026, instead keeps the second factor with a human by splitting the operation in two: <a href="https://docs.npmjs.com/staged-publishing/"><code class="language-plaintext highlighter-rouge">npm stage publish</code></a> uploads the tarball to a holding area with any token and no 2FA prompt, and the version becomes installable only after a maintainer approves it through a separate 2FA challenge on the CLI or the website. The approval step applies regardless of which token staged the upload, including one issued over OIDC, so a human 2FA challenge sits between CI and the public index even when the pipeline authenticated with trusted publishing. Packagist’s planned FIDO2-confirmed release step applies the same design to git tags, and a <a href="https://discuss.python.org/t/pre-pep-staged-releases-separated-from-pep-694/107804">pre-PEP discussion</a> opened in June proposes splitting staged releases out of <a href="https://peps.python.org/pep-0694/">PEP 694</a> as a standalone feature for PyPI.</p>

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

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: right">packages</th>
      <th>account model</th>
      <th>2FA methods</th>
      <th>required</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>npm</td>
      <td style="text-align: right">5,745,146</td>
      <td>native</td>
      <td>WebAuthn, TOTP</td>
      <td>publish; cohorts since 2022</td>
    </tr>
    <tr>
      <td>Go proxy</td>
      <td style="text-align: right">2,281,772</td>
      <td>none</td>
      <td>n/a</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>Docker Hub</td>
      <td style="text-align: right">1,002,426</td>
      <td>native</td>
      <td>TOTP</td>
      <td>no</td>
    </tr>
    <tr>
      <td>PyPI</td>
      <td style="text-align: right">928,948</td>
      <td>native</td>
      <td>WebAuthn, TOTP</td>
      <td>all users since Jan 2024</td>
    </tr>
    <tr>
      <td>NuGet</td>
      <td style="text-align: right">840,288</td>
      <td>Microsoft account</td>
      <td>via MSA/AAD</td>
      <td>all users since Mar 2022</td>
    </tr>
    <tr>
      <td>Maven Central</td>
      <td style="text-align: right">615,793</td>
      <td>native / GitHub / Google</td>
      <td>none native</td>
      <td>no</td>
    </tr>
    <tr>
      <td>Packagist</td>
      <td style="text-align: right">510,123</td>
      <td>native</td>
      <td>TOTP</td>
      <td>direction set May 2026</td>
    </tr>
    <tr>
      <td>crates.io</td>
      <td style="text-align: right">325,536</td>
      <td>GitHub</td>
      <td>via GitHub</td>
      <td>no</td>
    </tr>
    <tr>
      <td>RubyGems</td>
      <td style="text-align: right">211,106</td>
      <td>native</td>
      <td>WebAuthn, TOTP</td>
      <td>&gt;180M downloads</td>
    </tr>
    <tr>
      <td>nixpkgs</td>
      <td style="text-align: right">153,297</td>
      <td>GitHub PRs</td>
      <td>via GitHub</td>
      <td>contributor cohorts</td>
    </tr>
    <tr>
      <td>CocoaPods</td>
      <td style="text-align: right">102,418</td>
      <td>email token</td>
      <td>none</td>
      <td>no</td>
    </tr>
    <tr>
      <td>pub.dev</td>
      <td style="text-align: right">88,115</td>
      <td>Google account</td>
      <td>via Google</td>
      <td>no</td>
    </tr>
    <tr>
      <td>CPAN</td>
      <td style="text-align: right">41,333</td>
      <td>native</td>
      <td>none</td>
      <td>no</td>
    </tr>
    <tr>
      <td>GitHub Actions</td>
      <td style="text-align: right">32,727</td>
      <td>GitHub</td>
      <td>via GitHub</td>
      <td>contributor cohorts</td>
    </tr>
    <tr>
      <td>Guix</td>
      <td style="text-align: right">32,281</td>
      <td>Codeberg PRs</td>
      <td>via Codeberg</td>
      <td>no</td>
    </tr>
    <tr>
      <td>CRAN</td>
      <td style="text-align: right">29,558</td>
      <td>email form</td>
      <td>none</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>Terraform Registry</td>
      <td style="text-align: right">23,429</td>
      <td>GitHub</td>
      <td>via GitHub</td>
      <td>no</td>
    </tr>
    <tr>
      <td>Hex.pm</td>
      <td style="text-align: right">22,730</td>
      <td>native / GitHub</td>
      <td>TOTP</td>
      <td>partial<sup id="fnref:hexreq"><a href="#fn:hexreq" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></td>
    </tr>
    <tr>
      <td>Clojars</td>
      <td style="text-align: right">22,298</td>
      <td>native</td>
      <td>TOTP</td>
      <td>no</td>
    </tr>
    <tr>
      <td>conda-forge</td>
      <td style="text-align: right">20,636</td>
      <td>GitHub PRs</td>
      <td>via GitHub</td>
      <td>contributor cohorts</td>
    </tr>
    <tr>
      <td>Hackage</td>
      <td style="text-align: right">19,403</td>
      <td>native</td>
      <td>none</td>
      <td>no</td>
    </tr>
    <tr>
      <td>Swift Package Index</td>
      <td style="text-align: right">14,112</td>
      <td>none</td>
      <td>n/a</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>Julia General</td>
      <td style="text-align: right">13,730</td>
      <td>GitHub PRs</td>
      <td>via GitHub</td>
      <td>contributor cohorts</td>
    </tr>
    <tr>
      <td>Homebrew</td>
      <td style="text-align: right">9,483</td>
      <td>GitHub PRs</td>
      <td>via GitHub</td>
      <td>contributor cohorts</td>
    </tr>
    <tr>
      <td>Spack</td>
      <td style="text-align: right">9,300</td>
      <td>GitHub PRs</td>
      <td>via GitHub</td>
      <td>contributor cohorts</td>
    </tr>
  </tbody>
</table>

<p>Package counts from <a href="https://packages.ecosyste.ms/registries">ecosyste.ms</a>, August 2026. Distribution repositories (Debian, Ubuntu, Alpine, and similar) are omitted since publication goes through distribution maintainers rather than upstream authors.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:hex">
      <p>An earlier version of this post grouped Hex.pm with the registries that have TOTP as an unenforced option. Thanks to Jonatan Männchen for the correction. <a href="#fnref:hex" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:hexreq">
      <p>Required when publishing over the OAuth device flow; required for all API write operations from 1 November 2026. <a href="#fnref:hexreq" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Andrew Nesbitt</name><email>andrew@ecosyste.ms</email></author><category term="package-managers" /><category term="security" /><category term="registries" /><summary type="html"><![CDATA[Something you have, something you know, and someone else's OAuth.]]></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>