<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Dependencies on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/dependencies/</link><description>Recent content in Dependencies on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Fri, 29 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/dependencies/index.xml" rel="self" type="application/rss+xml"/><item><title>Nobody's coming to clean your supply chain</title><link>https://phpboyscout.uk/nobody-is-coming-to-clean-your-supply-chain/</link><pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/nobody-is-coming-to-clean-your-supply-chain/</guid><description>&lt;img src="https://phpboyscout.uk/nobody-is-coming-to-clean-your-supply-chain/cover-nobody-is-coming-to-clean-your-supply-chain.png" alt="Featured image of post Nobody's coming to clean your supply chain" /&gt;&lt;p&gt;Pick a week in May 2026 and there&amp;rsquo;s a supply-chain attack in it. On the 11th
someone owned TanStack&amp;rsquo;s CI and pushed
&lt;a class="link" href="https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised" target="_blank" rel="noopener"
 &gt;84 poisoned package versions in six minutes&lt;/a&gt;.
On the 14th, three malicious versions of
&lt;a class="link" href="https://www.stepsecurity.io/blog/node-ipc-npm-supply-chain-attack" target="_blank" rel="noopener"
 &gt;node-ipc&lt;/a&gt;,
a library with ten million weekly downloads, shipped an identical
credential-stealer. Days later it was
&lt;a class="link" href="https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/" target="_blank" rel="noopener"
 &gt;@antv&lt;/a&gt;,
cascading down into a charting library a million projects depend on. Each one
runs its payload the moment you install it, then quietly tries to publish
itself from your machine.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ve heard this story so many times the outrage has worn smooth. So let me
point at the one detail that should still make you sit up: the TanStack
packages carried &lt;em&gt;valid signing provenance&lt;/em&gt;. Real attestation, pointing at the
real pipeline. The seal was genuine. The contents were poison.&lt;/p&gt;
&lt;h2 id="a-signature-proves-the-sender-not-the-contents"&gt;A signature proves the sender, not the contents
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve spent a fair while &lt;a class="link" href="https://phpboyscout.uk/verifying-your-own-downloads/" &gt;building integrity and signing into my own
tools&lt;/a&gt;, so this one
stings a little. Signing is a trust mechanism, and a good one. It&amp;rsquo;s how I prove
a binary you downloaded was built and published by me and nobody else, and in a
world with this many ways to be impersonated, that matters more than ever.&lt;/p&gt;
&lt;p&gt;But TanStack shows the limit in neon. If the pipeline doing the signing is
itself compromised, the signature is still perfectly valid. It just now
certifies a lie. Provenance answers &amp;ldquo;did this come from where it claims?&amp;rdquo; It
does not answer &amp;ldquo;is what&amp;rsquo;s inside safe?&amp;rdquo;, and we have spent a few years quietly
letting people treat those as the same question.&lt;/p&gt;
&lt;p&gt;They aren&amp;rsquo;t. A signature is a promise about the &lt;em&gt;sender&lt;/em&gt;. The thing we actually
need is a promise about the &lt;em&gt;contents&lt;/em&gt;: that whoever signed has done the
diligence, the testing, the vetting, to vouch for what&amp;rsquo;s in the tin. A
signature without that behind it isn&amp;rsquo;t a safety certificate. It&amp;rsquo;s a
tamper-proof seal on a poisoned jar.&lt;/p&gt;
&lt;h2 id="it-was-never-just-npm"&gt;It was never just npm
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s tempting to file all this under &amp;ldquo;npm being npm&amp;rdquo;. Resist it, because it&amp;rsquo;s a
category error. The thing that makes these attacks work, a stranger&amp;rsquo;s code
running on your machine as a side effect of installing or building, is not an
npm bug. It&amp;rsquo;s a near-universal design choice.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Ecosystem&lt;/th&gt;
 &lt;th&gt;Untrusted code on install/build?&lt;/th&gt;
 &lt;th&gt;Mechanism&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;npm&lt;/strong&gt; (JS)&lt;/td&gt;
 &lt;td&gt;Yes, at install (dependencies too)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;pre&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; scripts&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;PyPI&lt;/strong&gt; (Python)&lt;/td&gt;
 &lt;td&gt;sdist yes, wheel no&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;setup.py&lt;/code&gt;; wheels forbid hooks&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;RubyGems&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Yes, at install&lt;/td&gt;
 &lt;td&gt;native-extension build (&lt;code&gt;extconf.rb&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;cargo&lt;/strong&gt; (Rust)&lt;/td&gt;
 &lt;td&gt;Yes, at build&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;build.rs&lt;/code&gt; and proc-macros&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Composer&lt;/strong&gt; (PHP)&lt;/td&gt;
 &lt;td&gt;Dependencies: no&lt;/td&gt;
 &lt;td&gt;only the &lt;em&gt;root&lt;/em&gt; project&amp;rsquo;s scripts run, by design&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Maven/Gradle&lt;/strong&gt; (JVM)&lt;/td&gt;
 &lt;td&gt;Yes, at build&lt;/td&gt;
 &lt;td&gt;build scripts and plugins&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;NuGet&lt;/strong&gt; (.NET)&lt;/td&gt;
 &lt;td&gt;Modern: no&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;install.ps1&lt;/code&gt;, legacy format only&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt; (modules)&lt;/td&gt;
 &lt;td&gt;No&lt;/td&gt;
 &lt;td&gt;no install or build hooks&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(Lifecycle hooks across ecosystems are catalogued at
&lt;a class="link" href="https://github.com/ecosyste-ms/package-manager-hooks" target="_blank" rel="noopener"
 &gt;ecosyste.ms&lt;/a&gt; if you want
the receipts.)&lt;/p&gt;
&lt;p&gt;Read that and the lesson isn&amp;rsquo;t &amp;ldquo;npm is uniquely bad&amp;rdquo;, it&amp;rsquo;s &amp;ldquo;this was a choice,
and several ecosystems chose differently&amp;rdquo;. Go runs no install or build hooks at
all. PHP&amp;rsquo;s Composer flatly refuses to run a dependency&amp;rsquo;s scripts, only your own
project&amp;rsquo;s. Python&amp;rsquo;s wheel format forbids install hooks. The hook was never
inevitable.&lt;/p&gt;
&lt;p&gt;And yes, that includes my own back yard. cargo&amp;rsquo;s &lt;code&gt;build.rs&lt;/code&gt; is the same gun
fired at build time instead of install time, and the
&lt;a class="link" href="https://socket.dev/blog/trapdoor-crypto-stealer-npm-pypi-crates" target="_blank" rel="noopener"
 &gt;TrapDoor campaign&lt;/a&gt;
used exactly that to rifle through keystores on crates.io this year. Rust isn&amp;rsquo;t
safe here. It&amp;rsquo;s a smaller, better-policed target, which is a different thing,
and I&amp;rsquo;d rather say so than pretend one of my favourite languages is above it.&lt;/p&gt;
&lt;h2 id="no-registry-can-hand-you-a-clean-package"&gt;No registry can hand you a clean package
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the uncomfortable core. Not one of these registries can guarantee the
package you pull is clean. They can sign it, scan it, attest its origin and
mandate 2FA on maintainers, and they should do all of that. But none of it is a
guarantee, because the failure modes are endless and attackers keep finding new
ones. A maintainer account gets phished. A CI token leaks. A trusted
contributor turns. A dependency four levels down quietly changes hands.&lt;/p&gt;
&lt;p&gt;So the onus lands, and will keep landing for a good while yet, on the consuming
engineer. That isn&amp;rsquo;t a comfortable answer or a clever one. It&amp;rsquo;s the true one.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s a genuinely rotten spot to stand in, because the advice contradicts
itself. Patch slowly and you&amp;rsquo;re scolded for running known-vulnerable
dependencies. Patch the instant a release drops and you&amp;rsquo;ve skipped the
bedding-in that might have caught a poisoned one. There&amp;rsquo;s no setting on that
dial that&amp;rsquo;s safe, only trade-offs you have to actually think about. Add CI that
leaks credentials it never needed, and a dependency tree thousands of strangers
deep, and you can see why there&amp;rsquo;s no single villain to blame and no single
switch to flip.&lt;/p&gt;
&lt;h2 id="the-boring-discipline-that-actually-helps"&gt;The boring discipline that actually helps
&lt;/h2&gt;&lt;p&gt;What&amp;rsquo;s left isn&amp;rsquo;t heroic, it&amp;rsquo;s hygiene, and it&amp;rsquo;s the boring, necessary stuff I keep
banging on about.
&lt;a class="link" href="https://phpboyscout.uk/openssf-scorecard-graded-my-supply-chain/" &gt;Pin your CI actions to commit SHAs&lt;/a&gt;
so a moved tag can&amp;rsquo;t swap code under you. Commit your lockfiles. Run the
auditors, &lt;code&gt;cargo-audit&lt;/code&gt;, &lt;code&gt;pip-audit&lt;/code&gt;, &lt;code&gt;govulncheck&lt;/code&gt;, &lt;code&gt;npm audit&lt;/code&gt;, or Google&amp;rsquo;s
cross-ecosystem &lt;a class="link" href="https://github.com/google/osv-scanner" target="_blank" rel="noopener"
 &gt;OSV-Scanner&lt;/a&gt;, on every
build. Gate the dependency tree and
&lt;a class="link" href="https://phpboyscout.uk/waivers-with-an-expiry-date/" &gt;give every exception an expiry date&lt;/a&gt;
so &amp;ldquo;we&amp;rsquo;ll deal with it later&amp;rdquo; can&amp;rsquo;t quietly become &amp;ldquo;never&amp;rdquo;. Keep the tree
small: every crate you don&amp;rsquo;t add is a stranger you don&amp;rsquo;t have to trust.&lt;/p&gt;
&lt;p&gt;None of that is a solution. All of it is diligence, and diligence is the only
thing that was ever going to stand behind the signature. When I sign a release,
the cryptography is the easy part. The promise underneath it, that I pinned,
locked, audited, vetted and tested before I put my name on it, is the part
worth anything. That&amp;rsquo;s the contract. The signature is just how I countersign
it.&lt;/p&gt;
&lt;p&gt;The encouraging note is that the structural defences exist and they work. Go&amp;rsquo;s
checksum database and its refusal to run hooks. Composer declining to trust a
dependency&amp;rsquo;s scripts. Python&amp;rsquo;s wheels. &lt;code&gt;cargo-vet&lt;/code&gt; and &lt;code&gt;cargo-deny&lt;/code&gt; giving you
somewhere to record human judgement at scale. More ecosystems should steal
these shamelessly, because a registry that makes the safe path the &lt;em&gt;default&lt;/em&gt;
does the working engineer a far bigger favour than one that leaves it all to
discipline.&lt;/p&gt;
&lt;h2 id="the-same-shape-a-third-time"&gt;The same shape, a third time
&lt;/h2&gt;&lt;p&gt;If this feels familiar, it should. I wrote recently about
&lt;a class="link" href="https://phpboyscout.uk/ai-didnt-kill-curls-bug-bounty/" &gt;a bug bounty that collapsed because the cost of slop was deferred&lt;/a&gt;,
and about &lt;a class="link" href="https://phpboyscout.uk/the-greybeards-edge-was-never-typing/" &gt;a junior pipeline being cut because the bill lands years
later&lt;/a&gt;.
Supply-chain security is the same shape a third time. The convenience is now,
the catastrophe is later, and the only thing standing in the gap is an engineer
paying attention, doing the dull work, refusing to be rushed into trusting
something they haven&amp;rsquo;t checked.&lt;/p&gt;
&lt;p&gt;There is no clean package waiting to be found, no registry about to solve this
for us, no signature that means &amp;ldquo;safe&amp;rdquo; all on its own. There&amp;rsquo;s the diligence
you do before you put your name to something, and the judgement to know when an
install is asking you to trust more than you should. For a good while yet, that
is the whole job. Boring, unfashionable, and the only thing that works.&lt;/p&gt;</description></item><item><title>The blank import that keeps a dependency out of your binary</title><link>https://phpboyscout.uk/the-blank-import-that-keeps-a-dependency-out-of-your-binary/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/the-blank-import-that-keeps-a-dependency-out-of-your-binary/</guid><description>&lt;img src="https://phpboyscout.uk/the-blank-import-that-keeps-a-dependency-out-of-your-binary/cover-the-blank-import-that-keeps-a-dependency-out-of-your-binary.png" alt="Featured image of post The blank import that keeps a dependency out of your binary" /&gt;&lt;p&gt;go-tool-base can stash your credentials in the OS keychain, which most people building on it are perfectly happy about. But some of them ship into regulated and air-gapped environments where the binary isn&amp;rsquo;t &lt;em&gt;permitted&lt;/em&gt; to contain keychain or session-bus code at all&amp;hellip; not dormant, not unused, simply not there.&lt;/p&gt;
&lt;p&gt;So I had a feature most users want and a minority must be able to provably not have. The way I ended up solving it is one of my favourite little bits of honest Go.&lt;/p&gt;
&lt;h2 id="a-feature-some-users-have-to-be-able-to-not-have"&gt;A feature some users have to be able to &lt;em&gt;not have&lt;/em&gt;
&lt;/h2&gt;&lt;p&gt;go-tool-base needs somewhere to keep secrets: AI provider keys, VCS tokens, the occasional app password. The best home for those on a developer&amp;rsquo;s machine is the operating system&amp;rsquo;s own keychain. macOS Keychain, GNOME Keyring or KWallet on Linux via the Secret Service, Windows Credential Manager. So I wanted go-tool-base to support all three. (This is the keychain mode I mentioned back in the &lt;a class="link" href="https://phpboyscout.uk/where-should-a-cli-keep-your-api-keys/" &gt;credentials post&lt;/a&gt;, finally getting the explanation I promised it.)&lt;/p&gt;
&lt;p&gt;The Go library for that is &lt;a class="link" href="https://github.com/zalando/go-keyring" target="_blank" rel="noopener"
 &gt;&lt;code&gt;go-keyring&lt;/code&gt;&lt;/a&gt;, and it&amp;rsquo;s good. The catch is what it drags in behind it. On Linux it talks to the Secret Service over D-Bus, which means &lt;code&gt;godbus&lt;/code&gt;. On Windows it pulls &lt;code&gt;wincred&lt;/code&gt;. Perfectly reasonable dependencies for a desktop tool.&lt;/p&gt;
&lt;p&gt;Now here&amp;rsquo;s the constraint that made this interesting. Some of the people building tools on go-tool-base don&amp;rsquo;t ship to developer laptops. They ship into regulated sectors and air-gapped deployments where a security review will scan the binary, enumerate every dependency, and ask pointed questions about anything that does inter-process communication. For those builds, &amp;ldquo;the keychain code is there but we never call it&amp;rdquo; is not an acceptable answer. The reviewer&amp;rsquo;s position, and it&amp;rsquo;s a fair one, is that code which isn&amp;rsquo;t in the binary cannot be a finding.&lt;/p&gt;
&lt;p&gt;So I had a feature that most users want, and a minority of users must be able to provably &lt;em&gt;not have&lt;/em&gt;. Same framework, same release.&lt;/p&gt;
&lt;h2 id="why-i-didnt-reach-for-a-build-tag"&gt;Why I didn&amp;rsquo;t reach for a build tag
&lt;/h2&gt;&lt;p&gt;The obvious Go answer is a build tag. Compile with &lt;code&gt;-tags keychain&lt;/code&gt; to get it, leave the tag off to not. I started down that road. I even spent a while on an inverted version, a &lt;code&gt;nokeychain&lt;/code&gt; tag, on the theory that the regulated build should be the one that has to ask, so a forgotten flag fails safe.&lt;/p&gt;
&lt;p&gt;It works. It also isn&amp;rsquo;t very nice. Build tags are invisible at the call site. Nothing in the source tells you that a file only exists in some builds. The two worlds drift, because the tagged-out path isn&amp;rsquo;t compiled in your normal editor session and quietly rots. And the ergonomics for a &lt;em&gt;downstream consumer&lt;/em&gt; are poor: every tool built on go-tool-base would have to know the right magic incantation and thread it through their own release pipeline correctly, forever.&lt;/p&gt;
&lt;p&gt;I tried a second approach too: pull the keychain backend out into a completely separate Go module. That genuinely solves the dependency question (a module you don&amp;rsquo;t require can&amp;rsquo;t contribute to your &lt;code&gt;go.sum&lt;/code&gt;). But a separate module for one backend is clunky. Separate versioning, separate release, separate repo, all for a single file&amp;rsquo;s worth of behaviour. It felt like using a shipping container to post a letter.&lt;/p&gt;
&lt;h2 id="the-shape-that-actually-fits-a-registry-and-an-init"&gt;The shape that actually fits: a registry and an &lt;code&gt;init()&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The version I&amp;rsquo;m happy with leans on two boring, well-worn Go mechanisms and lets them do something quietly clever together.&lt;/p&gt;
&lt;p&gt;First, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/credentials/backend.go#L27" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/credentials&lt;/code&gt;&lt;/a&gt; defines a &lt;code&gt;Backend&lt;/code&gt; interface and a registry. By default the registry holds a stub backend that politely returns &amp;ldquo;unsupported&amp;rdquo; for everything. The framework only ever talks to &lt;em&gt;the registered backend&lt;/em&gt;, whatever that happens to be.&lt;/p&gt;
&lt;p&gt;Second, the keychain implementation lives in its own package, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/credentials/keychain/keychain.go#L97" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/credentials/keychain&lt;/code&gt;&lt;/a&gt;, still inside the same module, no separate release to manage. That package has an &lt;code&gt;init()&lt;/code&gt; that registers its &lt;code&gt;go-keyring&lt;/code&gt;-backed backend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//nolint:gochecknoinits // registration via import is the whole point&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And &lt;code&gt;go-keyring&lt;/code&gt;, &lt;code&gt;godbus&lt;/code&gt;, &lt;code&gt;wincred&lt;/code&gt;, the whole IPC dependency chain, are only imported by &lt;em&gt;that&lt;/em&gt; package.&lt;/p&gt;
&lt;p&gt;Now the trick. To switch keychain support on, you import the package. You don&amp;rsquo;t have to &lt;em&gt;use&lt;/em&gt; anything from it. A blank import is enough, because a blank import still runs the package&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// cmd/gtb/keychain.go - the entire file.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/credentials/keychain&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That single line is the on/off switch for the shipped &lt;code&gt;gtb&lt;/code&gt; binary. The blank import means &lt;code&gt;init()&lt;/code&gt; runs, the keychain backend registers itself, and credential operations start routing through the OS keychain. No flag, no tag, no config.&lt;/p&gt;
&lt;h2 id="the-part-that-makes-it-provable"&gt;The part that makes it provable
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s why this beats the build tag, and it comes down to one guarantee in the Go toolchain: &lt;strong&gt;the linker only includes packages that are actually imported.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;cmd/gtb/keychain.go&lt;/code&gt; exists, the &lt;code&gt;keychain&lt;/code&gt; package is in the import graph, so &lt;code&gt;go-keyring&lt;/code&gt;, &lt;code&gt;godbus&lt;/code&gt; and &lt;code&gt;wincred&lt;/code&gt; are linked in. Delete that one file and rebuild, and the &lt;code&gt;keychain&lt;/code&gt; package is no longer reachable from &lt;code&gt;main&lt;/code&gt;. The linker performs dead-code elimination, and the entire &lt;code&gt;go-keyring&lt;/code&gt; chain is &lt;em&gt;gone&lt;/em&gt;. Not dormant. Not present-but-unused. Absent from the binary.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the bit a regulated build needs. It isn&amp;rsquo;t a promise that the code won&amp;rsquo;t run. It&amp;rsquo;s a structural fact that the code isn&amp;rsquo;t there, and you can hand a security reviewer an SBOM that proves it. &lt;code&gt;go-keyring&lt;/code&gt; won&amp;rsquo;t appear, because it genuinely isn&amp;rsquo;t linked.&lt;/p&gt;
&lt;p&gt;For a downstream tool built on go-tool-base the story is the same, and just as cheap. Want keychain support? Add the one-line blank import to your own &lt;code&gt;cmd&lt;/code&gt; package. Must ship keychain-free? Don&amp;rsquo;t. Your binary&amp;rsquo;s dependency graph follows your import graph, exactly as Go always promised it would. The default (no import) is the locked-down one, which is the right way round for a safety property.&lt;/p&gt;
&lt;h2 id="why-i-like-this-more-than-i-expected-to"&gt;Why I like this more than I expected to
&lt;/h2&gt;&lt;p&gt;Build tags hide a decision in the compiler invocation. This pattern puts the decision in the source, as an import, where it&amp;rsquo;s greppable, obvious in code review, and impossible to get subtly wrong. There&amp;rsquo;s a real file called &lt;code&gt;keychain.go&lt;/code&gt; whose entire content is one import, and it reads as exactly what it is: a switch.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also just &lt;em&gt;honest&lt;/em&gt; Go. No reflection, no plugin loader, no clever runtime. A registry, an &lt;code&gt;init()&lt;/code&gt;, and the linker doing the one job it&amp;rsquo;s always done. The cleverness, such as it is, is in the arrangement, not in any individual piece.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base needed OS keychain support for the many, and a way to provably exclude it for the few. Build tags could express the toggle but hid it in the build invocation and rotted in the dark. A separate module solved the dependency question but was far too much machinery for one backend.&lt;/p&gt;
&lt;p&gt;Putting the keychain backend in its own package, activated by a blank &lt;code&gt;import _&lt;/code&gt; that fires its &lt;code&gt;init()&lt;/code&gt;, gets you both: a one-line, in-source, code-reviewable switch, and, because the linker only links what&amp;rsquo;s imported, a build with the import omitted that contains &lt;em&gt;none&lt;/em&gt; of the keychain dependency chain. Provable absence, not promised disuse.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re carrying an optional dependency that some of your users need gone rather than merely idle, this is the pattern. Let the import graph be the feature flag.&lt;/p&gt;</description></item></channel></rss>