<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rust-Tool-Base on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/categories/rust-tool-base/</link><description>Recent content in Rust-Tool-Base on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Wed, 13 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/categories/rust-tool-base/index.xml" rel="self" type="application/rss+xml"/><item><title>Pure-Rust Git, no git binary</title><link>https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/</guid><description>&lt;img src="https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/cover-pure-rust-git-no-git-binary.png" alt="Featured image of post Pure-Rust Git, no git binary" /&gt;&lt;p&gt;go-tool-base&amp;rsquo;s VCS support has two halves that get confused for one. One half talks to forge APIs (GitHub, GitLab) for releases and pull requests. The other talks to the &lt;code&gt;.git&lt;/code&gt; directory on disk: clone, history, diff, status. This post is mostly about the second half, and specifically about a question that turns out to have three answers in Rust, only one of which I&amp;rsquo;d recommend: how do you actually &lt;em&gt;do&lt;/em&gt; Git from inside a program?&lt;/p&gt;
&lt;h2 id="a-vcs-subsystem-with-two-halves"&gt;A VCS subsystem with two halves
&lt;/h2&gt;&lt;p&gt;go-tool-base has a VCS subsystem, and it does two distinct jobs.&lt;/p&gt;
&lt;p&gt;The first is forge APIs. GitHub and GitLab, Enterprise and nested group paths included. It authenticates, lists releases, fetches release assets, manages pull requests. The self-update machinery sits on this half, and it&amp;rsquo;s what a tool uses to ask &amp;ldquo;what&amp;rsquo;s the latest release?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The second is local Git. go-tool-base also carries a &lt;code&gt;RepoLike&lt;/code&gt; object, an abstraction over an actual Git repository on disk: clone it, read its commit history, diff two trees, check its status. This half doesn&amp;rsquo;t talk to a hosting service at all. It talks to the &lt;code&gt;.git&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;It would be easy to assume the second half grew out of the first. It didn&amp;rsquo;t, and where it actually came from is the part worth telling.&lt;/p&gt;
&lt;h2 id="a-capability-ahead-of-its-consumer"&gt;A capability ahead of its consumer
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;RepoLike&lt;/code&gt; object wasn&amp;rsquo;t built for go-tool-base. It came from another project, where it had already proved itself, and it was pulled into go-tool-base on purpose, with a specific future consumer in mind: the code generator.&lt;/p&gt;
&lt;p&gt;The plan is for the generator to use Git directly. When it scaffolds a new tool, that tool should start life as a Git repository, with a &lt;code&gt;git init&lt;/code&gt; and an initial commit. When you later regenerate, the generator should diff the regenerated template output against your working tree to detect drift, the same idea as &lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;respecting your edits&lt;/a&gt;. Both of those are local Git operations, not API calls, so the generator needs a repository abstraction to call into.&lt;/p&gt;
&lt;p&gt;That wiring isn&amp;rsquo;t finished yet. The generator doesn&amp;rsquo;t drive &lt;code&gt;RepoLike&lt;/code&gt; today. But the capability is in place, deliberately, ahead of the consumer that will use it, because the alternative is bolting Git support on later under deadline pressure, and that&amp;rsquo;s how you end up with the wrong abstraction.&lt;/p&gt;
&lt;p&gt;So when rust-tool-base was built, a repository abstraction was never in question. The Rust port carries the same capability for the same reason: a &lt;code&gt;Repo&lt;/code&gt; type with &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;clone&lt;/code&gt;, &lt;code&gt;walk&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;, &lt;code&gt;blame&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;checkout&lt;/code&gt;, present and ready for the generator to wire into. The open question was never &lt;em&gt;whether&lt;/em&gt; to have it. It was how to &lt;em&gt;do&lt;/em&gt; Git from inside a Rust program, and there are three answers, only one of which is any good.&lt;/p&gt;
&lt;h2 id="three-ways-to-do-git-and-the-one-worth-picking"&gt;Three ways to do Git, and the one worth picking
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Shell out to &lt;code&gt;git&lt;/code&gt;.&lt;/strong&gt; Run the &lt;code&gt;git&lt;/code&gt; binary as a subprocess and parse its output. It works until it doesn&amp;rsquo;t. The binary might not be installed. It might be a different version with different output. Its output is formatted for humans and changes between releases, so parsing it is a standing liability. You&amp;rsquo;ve made an undeclared dependency on a program you don&amp;rsquo;t ship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Link libgit2.&lt;/strong&gt; libgit2 is the C library that reimplements Git as something you can call from code, and &lt;code&gt;git2&lt;/code&gt; is the Rust binding to it. It&amp;rsquo;s solid and widely used. But it&amp;rsquo;s a C dependency, which means a C toolchain in the build, and it&amp;rsquo;s consistently the single biggest source of cross-compilation pain in the Rust Git ecosystem. The musl builds, the Windows builds, the static linking: libgit2 is where they tend to break.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;gix&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;gix&lt;/code&gt; is a reimplementation of Git in pure Rust. No C library, no subprocess. It&amp;rsquo;s just Rust code, and it compiles and cross-compiles like any other crate, because that&amp;rsquo;s all it is. It&amp;rsquo;s also generally faster, and being pure Rust it fits the &lt;a class="link" href="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/" &gt;no-&lt;code&gt;unsafe&lt;/code&gt;-in-first-party-code&lt;/a&gt; story far more comfortably than dragging a C library along.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-vcs&lt;/code&gt; is &lt;code&gt;gix&lt;/code&gt;-first. The &lt;code&gt;Repo&lt;/code&gt; type is built on it. There&amp;rsquo;s no &lt;code&gt;git&lt;/code&gt; binary dependency, and there&amp;rsquo;s no libgit2 in a default build.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gix&lt;/code&gt; is still maturing, and a few write paths, &lt;code&gt;push&lt;/code&gt; in particular, aren&amp;rsquo;t ready in it yet. For those, &lt;code&gt;git2&lt;/code&gt; stays available as an &lt;em&gt;opt-in&lt;/em&gt; fallback behind a Cargo feature. Off by default, so the libgit2 C dependency and its cross-compile cost only land in builds that explicitly ask for push support. The common case, a tool that clones, reads history, diffs and commits, pays none of it. (Which is &lt;a class="link" href="https://blog-570662.gitlab.io/two-kinds-of-feature-flag/" &gt;exactly the feature-flag story&lt;/a&gt; from a couple of weeks back, doing real work.)&lt;/p&gt;
&lt;h2 id="repo-is-a-foundation-not-a-façade"&gt;&lt;code&gt;Repo&lt;/code&gt; is a foundation, not a façade
&lt;/h2&gt;&lt;p&gt;One design decision is worth calling out, because it came straight from a go-tool-base lesson.&lt;/p&gt;
&lt;p&gt;It would have been easy to build &lt;code&gt;Repo&lt;/code&gt; as a narrow façade exposing exactly what the scaffolder and the release-notes feature need today, and nothing else. That was rejected on purpose. go-tool-base&amp;rsquo;s &lt;code&gt;RepoLike&lt;/code&gt; is itself the cautionary tale: it arrived from another project, settled into a sensible abstraction, and is already lined up to carry a consumer, the generator, that wasn&amp;rsquo;t driving its design when it was first written. A repository abstraction gets used by code that doesn&amp;rsquo;t exist yet. Build one as a narrow façade around today&amp;rsquo;s needs and you&amp;rsquo;ve guaranteed a rewrite the first time a downstream tool wants something slightly different.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;rtb-vcs&lt;/code&gt;&amp;rsquo;s &lt;code&gt;Repo&lt;/code&gt; is built as a foundation: a sensible, reasonably complete vocabulary of Git operations that a tool author can compose richer behaviour on, without re-importing &lt;code&gt;gix&lt;/code&gt; directly and re-deriving the framework&amp;rsquo;s auth and concurrency conventions. The errors back this up. &lt;code&gt;gix&lt;/code&gt;&amp;rsquo;s error types aren&amp;rsquo;t leaked through the public API; they&amp;rsquo;re wrapped in semantic &lt;code&gt;RepoError&lt;/code&gt; variants, so the backend could be swapped, &lt;code&gt;gix&lt;/code&gt; to &lt;code&gt;git2&lt;/code&gt;, or to something else entirely, without breaking a single downstream caller.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s VCS support has two halves: forge-API calls for releases and pull requests, and a &lt;code&gt;RepoLike&lt;/code&gt; object for local Git operations. The repo half arrived from another project and is wired in ahead of its intended consumer, the code generator, which will use it to initialise repositories for scaffolded tools and to diff regenerated output for drift.&lt;/p&gt;
&lt;p&gt;rust-tool-base carries the same capability on purpose. Its &lt;code&gt;Repo&lt;/code&gt; type is built on &lt;code&gt;gix&lt;/code&gt;, a pure-Rust Git implementation, so there&amp;rsquo;s no dependency on an installed &lt;code&gt;git&lt;/code&gt; binary and no libgit2 C library in a default build, which keeps cross-compilation clean. &lt;code&gt;git2&lt;/code&gt; stays an opt-in fallback for the few write paths &lt;code&gt;gix&lt;/code&gt; can&amp;rsquo;t do yet. And &lt;code&gt;Repo&lt;/code&gt; is built as a foundation for downstream tools, with the backend wrapped behind its own error type so it can be replaced without breaking callers.&lt;/p&gt;</description></item><item><title>Secrets that scrub themselves from RAM</title><link>https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/</guid><description>&lt;img src="https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/cover-secrets-that-scrub-themselves-from-ram.png" alt="Featured image of post Secrets that scrub themselves from RAM" /&gt;&lt;p&gt;A while ago I worked out &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;where a CLI should keep your API key&lt;/a&gt;: env var, OS keychain, or, grudgingly, a literal in the config file. That answers where the secret &lt;em&gt;lives&lt;/em&gt;. It says nothing about what happens to it once it&amp;rsquo;s loaded and sitting in your process memory, which is the half where secrets actually tend to leak. Rust, it turns out, can do something about that half that Go simply can&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-already-settled"&gt;What go-tool-base already settled
&lt;/h2&gt;&lt;p&gt;A while back I wrote about where a CLI should keep your API keys. The answer go-tool-base settled on was three storage modes, in a fixed precedence: an environment variable reference (the recommended default), the OS keychain (opt-in), or a literal value in the config file (legacy, and refused outright when &lt;code&gt;CI=true&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;rust-tool-base keeps that design unchanged. Same three modes, same precedence, same refusal of literal secrets in CI. A tool embeds a &lt;code&gt;CredentialRef&lt;/code&gt; in its typed config, and a &lt;code&gt;Resolver&lt;/code&gt; walks env, then keychain, then literal, then a well-known fallback variable, first hit wins. That part is a straight carry-over, because &lt;em&gt;where&lt;/em&gt; to keep the secret was design, and design survives the port.&lt;/p&gt;
&lt;p&gt;But storage is only half the life of a secret. The other half is what happens to it once it&amp;rsquo;s resolved and sitting in your process memory. That&amp;rsquo;s where Rust can do something Go can&amp;rsquo;t, and rust-tool-base takes the opening.&lt;/p&gt;
&lt;h2 id="the-two-ways-a-secret-leaks-after-youve-loaded-it"&gt;The two ways a secret leaks after you&amp;rsquo;ve loaded it
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve resolved the API key. It&amp;rsquo;s a value in memory now. Two very ordinary things can leak it from there, and neither involves your storage being wrong.&lt;/p&gt;
&lt;p&gt;The first is &lt;strong&gt;the log line&lt;/strong&gt;. Somewhere a developer writes a debug print of a config struct, or an error includes the struct that holds the key, or a panic dumps it. The secret is a string like any other string, so it renders like any other string, straight into a log aggregator that a lot of people can read.&lt;/p&gt;
&lt;p&gt;The second is &lt;strong&gt;the leftover bytes&lt;/strong&gt;. The key sat in a heap allocation. The variable goes out of scope, the allocation is freed, and on most runtimes &amp;ldquo;freed&amp;rdquo; just means &amp;ldquo;returned to the allocator&amp;rdquo;. The bytes are still there until something else writes over them. A core dump taken in that window contains your key. So does the next allocation that happens to land on that memory and gets logged before it&amp;rsquo;s overwritten.&lt;/p&gt;
&lt;p&gt;A Go string can&amp;rsquo;t really defend against either. Go strings are immutable, so you can&amp;rsquo;t zero one in place; the runtime copies them freely, so you can&amp;rsquo;t even track every copy; and there&amp;rsquo;s no compile-time barrier stopping anyone printing one. You can be disciplined, but discipline is all you&amp;rsquo;ve got.&lt;/p&gt;
&lt;h2 id="secretstring-closes-both"&gt;&lt;code&gt;SecretString&lt;/code&gt; closes both
&lt;/h2&gt;&lt;p&gt;rust-tool-base routes every secret through &lt;code&gt;secrecy::SecretString&lt;/code&gt;, and the crate is explicit that taking a plain &lt;code&gt;&amp;amp;str&lt;/code&gt; or &lt;code&gt;String&lt;/code&gt; for a secret is a &lt;em&gt;type error&lt;/em&gt;, not a style preference.&lt;/p&gt;
&lt;p&gt;For the log line, &lt;code&gt;SecretString&lt;/code&gt; has its own &lt;code&gt;Debug&lt;/code&gt; implementation, and it prints &lt;code&gt;[REDACTED]&lt;/code&gt;. Always. A config struct holding a &lt;code&gt;SecretString&lt;/code&gt; can be debug-printed, put in an error, caught in a panic, and the secret field shows up as &lt;code&gt;[REDACTED]&lt;/code&gt; every single time. You don&amp;rsquo;t have to remember not to log it. The type already won&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;For the leftover bytes, &lt;code&gt;SecretString&lt;/code&gt; zeroes its memory when it&amp;rsquo;s dropped. When the value goes out of scope, before the allocation is handed back, the bytes are overwritten. The window where a freed allocation still holds your key is closed. A core dump taken afterwards finds zeroes.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a third leak &lt;code&gt;SecretString&lt;/code&gt; blocks that&amp;rsquo;s easy to miss. It deliberately doesn&amp;rsquo;t implement &lt;code&gt;Serialize&lt;/code&gt;. You cannot serialise a &lt;code&gt;SecretString&lt;/code&gt;. That sounds like an inconvenience until you see what it prevents: a tool that loads config, changes one setting, and writes the whole struct back would, with an ordinary string, faithfully write the resolved secret to disk in plain text. Because &lt;code&gt;SecretString&lt;/code&gt; can&amp;rsquo;t be serialised, &lt;code&gt;CredentialRef&lt;/code&gt; can&amp;rsquo;t be either, and that accident is structurally impossible. Writing a secret back is a deliberate, separate path, never a side effect of saving config.&lt;/p&gt;
&lt;p&gt;When code genuinely needs the raw value, to drop it into an &lt;code&gt;Authorization&lt;/code&gt; header, it calls &lt;code&gt;expose_secret()&lt;/code&gt;. The name is the point. Getting at the plaintext is one explicit, greppable, reviewable call, and everywhere else the secret stays wrapped.&lt;/p&gt;
&lt;h2 id="discipline-versus-the-type-system"&gt;Discipline versus the type system
&lt;/h2&gt;&lt;p&gt;The honest framing is this. None of these leaks are exotic. Logging a struct, a core dump after a free, re-saving a config file: they&amp;rsquo;re all routine, and they&amp;rsquo;re all how real credentials end up somewhere they shouldn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s storage design is good, and rust-tool-base kept it. But in Go, &lt;em&gt;not leaking the secret once it&amp;rsquo;s in memory&lt;/em&gt; comes down to every developer being careful every time. In Rust, &lt;code&gt;SecretString&lt;/code&gt; makes the type system carry it. The redaction, the zeroing, the un-serialisability aren&amp;rsquo;t things you remember to do. They&amp;rsquo;re things the secret does to itself because of what it is. That&amp;rsquo;s the part Go structurally can&amp;rsquo;t match, and it&amp;rsquo;s why the port didn&amp;rsquo;t just copy the storage modes across, it tightened the handling underneath them.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;go-tool-base settled where a CLI keeps a secret: env var, keychain, or literal, in a fixed precedence. rust-tool-base keeps that design and hardens what happens once the secret is loaded.&lt;/p&gt;
&lt;p&gt;Every secret is a &lt;code&gt;secrecy::SecretString&lt;/code&gt;. It debug-prints as &lt;code&gt;[REDACTED]&lt;/code&gt;, so it can&amp;rsquo;t fall into a log by accident. Its memory is zeroed on drop, so it doesn&amp;rsquo;t survive in freed heap. It isn&amp;rsquo;t serialisable, so it can&amp;rsquo;t be written back to config by a blanket save. Getting the plaintext is one explicit &lt;code&gt;expose_secret()&lt;/code&gt; call. Go can only ask developers to be careful with a secret in memory; Rust lets the type be careful for them.&lt;/p&gt;</description></item><item><title>Errors without an error handler</title><link>https://blog-570662.gitlab.io/errors-without-an-error-handler/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-without-an-error-handler/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-without-an-error-handler/cover-errors-without-an-error-handler.png" alt="Featured image of post Errors without an error handler" /&gt;&lt;p&gt;In &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt; I said go-tool-base&amp;rsquo;s error handler was one of the bits that &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; survive the move to Rust, and promised to come back to it. Here&amp;rsquo;s the come-back. The short version is that Rust hands you, for free, the single consistent error exit that go-tool-base had to build a whole component to get.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-built"&gt;What go-tool-base built
&lt;/h2&gt;&lt;p&gt;A while ago I &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;wrote about error handling in go-tool-base&lt;/a&gt;. The core of it: an error should carry a &lt;em&gt;hint&lt;/em&gt;, a separate field of human guidance telling the user what to do next, kept apart from the error&amp;rsquo;s identity so code can still match on it.&lt;/p&gt;
&lt;p&gt;The other half of that post was about consistency. Every go-tool-base command returns its errors the idiomatic Cobra way, and they all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. Presentation decided in exactly one place, so no command can render a failure differently from its neighbour.&lt;/p&gt;
&lt;p&gt;That handler is a real object. It exists, it&amp;rsquo;s wired in, it&amp;rsquo;s the thing every error passes through. Building it was a deliberate piece of work, and it was the right call for Go.&lt;/p&gt;
&lt;p&gt;When I rebuilt this in Rust, the handler didn&amp;rsquo;t survive the move. Not because consistency stopped mattering. Because Rust gives you the single exit for free, and an object to enforce it would just be re-implementing something the language already does for you.&lt;/p&gt;
&lt;h2 id="the-shape-of-a-rust-error"&gt;The shape of a Rust error
&lt;/h2&gt;&lt;p&gt;Start with the type. In rust-tool-base every crate defines its own error enum, and every one of them derives two traits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[derive(Debug, thiserror::Error, miette::Diagnostic)]&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="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;ConfigError&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="cp"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;config file not found at {path}&amp;#34;&lt;/span&gt;&lt;span class="cp"&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="cp"&gt;#[diagnostic(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; code(rtb::config::not_found),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; help(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;run `mytool init` to create one, or set MYTOOL_CONFIG&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&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="n"&gt;NotFound&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 class="n"&gt;path&lt;/span&gt;: &lt;span class="nc"&gt;PathBuf&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="c1"&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;&lt;code&gt;thiserror::Error&lt;/code&gt; makes it a proper error type. &lt;code&gt;miette::Diagnostic&lt;/code&gt; is the interesting one. A &lt;code&gt;Diagnostic&lt;/code&gt; is an error that also carries the things you&amp;rsquo;d want when &lt;em&gt;presenting&lt;/em&gt; it: a stable &lt;code&gt;code&lt;/code&gt;, a severity, a &lt;code&gt;help&lt;/code&gt; string, and optionally source labels pointing at spans of input. The &lt;code&gt;help&lt;/code&gt; line is the same idea as go-tool-base&amp;rsquo;s hint, the recovery step, except here it&amp;rsquo;s an attribute on the variant rather than a field threaded through a wrapper.&lt;/p&gt;
&lt;p&gt;So the guidance lives on the error, structured, from the moment the error is created.&lt;/p&gt;
&lt;h2 id="there-is-no-handler-theres-a-convention"&gt;There is no handler, there&amp;rsquo;s a convention
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where Rust does the work go-tool-base&amp;rsquo;s handler was built to do.&lt;/p&gt;
&lt;p&gt;A rust-tool-base &lt;code&gt;main&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[tokio::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="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;miette&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;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="n"&gt;rtb&lt;/span&gt;::&lt;span class="n"&gt;cli&lt;/span&gt;::&lt;span class="n"&gt;Application&lt;/span&gt;::&lt;span class="n"&gt;builder&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VersionInfo&lt;/span&gt;::&lt;span class="n"&gt;from_env&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&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="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;await&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;&lt;code&gt;main&lt;/code&gt; returns &lt;code&gt;miette::Result&amp;lt;()&amp;gt;&lt;/code&gt;. Every command&amp;rsquo;s &lt;code&gt;run&lt;/code&gt; returns a &lt;code&gt;Result&lt;/code&gt; too. In between, errors propagate with the &lt;code&gt;?&lt;/code&gt; operator: a function that hits an error returns it upward, immediately, and the caller does the same, all the way to &lt;code&gt;main&lt;/code&gt;. Nobody writes a &amp;ldquo;check this error&amp;rdquo; call. &lt;code&gt;?&lt;/code&gt; is the propagation.&lt;/p&gt;
&lt;p&gt;And when an error reaches &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; returns it, &lt;em&gt;something&lt;/em&gt; has to render it for the user. That something is a report hook. rust-tool-base installs one at startup, and from then on any &lt;code&gt;Diagnostic&lt;/code&gt; that exits &lt;code&gt;main&lt;/code&gt; is rendered through it: the code, the severity, the help text, the source labels, with colour. One renderer, installed once.&lt;/p&gt;
&lt;p&gt;Look at what that adds up to. Every error in the program flows to one place, &lt;code&gt;main&lt;/code&gt;. It&amp;rsquo;s rendered by one thing, the hook. Presentation is decided in exactly one location and no command can deviate from it. That&amp;rsquo;s precisely the property go-tool-base&amp;rsquo;s &lt;code&gt;ErrorHandler&lt;/code&gt; was built to guarantee. The difference is that nobody built it. The single exit is just where &lt;code&gt;?&lt;/code&gt; propagation ends, and the single renderer is one hook. The language&amp;rsquo;s own convention for returning errors from &lt;code&gt;main&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the funnel.&lt;/p&gt;
&lt;h2 id="errors-are-values-all-the-way"&gt;Errors are values, all the way
&lt;/h2&gt;&lt;p&gt;The thing that took me a moment to fully trust is that there&amp;rsquo;s no funnel to maintain, because there&amp;rsquo;s no funnel as an object. go-tool-base&amp;rsquo;s handler is a component: it can drift, it has to be kept in the path, a command could in principle be wired to bypass it. The Rust version cannot be bypassed, because bypassing it would mean a command not returning its error, and an error you don&amp;rsquo;t return is a compile-time warning at best and dead-obvious wrong code at worst.&lt;/p&gt;
&lt;p&gt;So the model is just: errors are values, you return them, &lt;code&gt;?&lt;/code&gt; carries them up, &lt;code&gt;main&lt;/code&gt; hands the last one to the hook. The consistency isn&amp;rsquo;t enforced by a guard. It&amp;rsquo;s the only thing the shape of the language really lets you do.&lt;/p&gt;
&lt;p&gt;go-tool-base reaches a single, consistent error exit by building one and routing everything through it. rust-tool-base reaches the same exit by having errors be ordinary return values and letting them fall out of &lt;code&gt;main&lt;/code&gt;. Same outcome. One of them is a component you own; the other is a convention you inherit.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;go-tool-base funnels every error through one &lt;code&gt;ErrorHandler&lt;/code&gt; so presentation stays consistent. That handler is a deliberately built component, and it&amp;rsquo;s the right design in Go.&lt;/p&gt;
&lt;p&gt;rust-tool-base has no handler. Every crate&amp;rsquo;s error type derives &lt;code&gt;miette::Diagnostic&lt;/code&gt;, carrying its code, severity and help text. Errors propagate with &lt;code&gt;?&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;, which returns &lt;code&gt;miette::Result&lt;/code&gt;, and a framework-installed hook renders whatever comes out. The single consistent exit is the end of &lt;code&gt;?&lt;/code&gt; propagation, and the single renderer is one hook. The funnel go-tool-base built by hand is, in Rust, just the language&amp;rsquo;s return-from-&lt;code&gt;main&lt;/code&gt; convention.&lt;/p&gt;</description></item><item><title>A framework that contains no unsafe</title><link>https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/cover-a-framework-that-contains-no-unsafe.png" alt="Featured image of post A framework that contains no unsafe" /&gt;&lt;p&gt;&amp;ldquo;It&amp;rsquo;s written in Rust&amp;rdquo; gets thrown around as if it were a memory-safety guarantee. It mostly isn&amp;rsquo;t. Rust is memory-safe by &lt;em&gt;default&lt;/em&gt;, which is a wonderful thing, but the &lt;code&gt;unsafe&lt;/code&gt; keyword exists precisely so any crate, any module, can step outside that default when it needs to. So &amp;ldquo;written in Rust&amp;rdquo; really means &amp;ldquo;mostly safe, probably&amp;rdquo;. rust-tool-base makes the stronger claim about its own code, and gets the compiler to enforce it.&lt;/p&gt;
&lt;h2 id="safe-by-default-is-not-the-same-as-safe"&gt;Safe by default is not the same as safe
&lt;/h2&gt;&lt;p&gt;People reach for Rust because of memory safety, and the reputation is earned. Write ordinary Rust and the compiler will not let you have a use-after-free, a data race, or a buffer overrun. That&amp;rsquo;s the default, and it&amp;rsquo;s a very good default.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a default, and defaults can be turned off. Rust has an &lt;code&gt;unsafe&lt;/code&gt; keyword precisely so that, when you genuinely need to, you can dereference a raw pointer, call into C, or tell the compiler you&amp;rsquo;ve upheld an invariant it can&amp;rsquo;t check itself. Inside an &lt;code&gt;unsafe&lt;/code&gt; block, the guarantees are yours to maintain, not the compiler&amp;rsquo;s to enforce.&lt;/p&gt;
&lt;p&gt;That keyword has to exist. Some of the most foundational crates in the ecosystem are built on it, carefully. But it means a fact worth being precise about: a project being &amp;ldquo;written in Rust&amp;rdquo; tells you its code is &lt;em&gt;mostly&lt;/em&gt; safe. It does not tell you the project&amp;rsquo;s own code contains &lt;em&gt;no&lt;/em&gt; &lt;code&gt;unsafe&lt;/code&gt;. Those are different claims, and only the second one is a guarantee.&lt;/p&gt;
&lt;p&gt;rust-tool-base makes the second claim about its own code, and has the compiler back it up.&lt;/p&gt;
&lt;h2 id="forbid-not-just-deny"&gt;&lt;code&gt;forbid&lt;/code&gt;, not just &lt;code&gt;deny&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The mechanism is one line at the top of every crate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#![forbid(unsafe_code)]&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;&lt;code&gt;unsafe_code&lt;/code&gt; is a lint, and Rust lints have levels. The interesting choice is &lt;code&gt;forbid&lt;/code&gt; rather than &lt;code&gt;deny&lt;/code&gt;, because the two are not the same strength.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;deny&lt;/code&gt; makes the lint an error. But it&amp;rsquo;s an error a &lt;em&gt;downstream module can locally override&lt;/em&gt;. Anyone can write &lt;code&gt;#[allow(unsafe_code)]&lt;/code&gt; on a function or a block and the &lt;code&gt;deny&lt;/code&gt; is lifted right there. As a policy, &lt;code&gt;deny&lt;/code&gt; is &amp;ldquo;don&amp;rsquo;t do this unless you really mean to&amp;rdquo;, and &amp;ldquo;unless you really mean to&amp;rdquo; is a door.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;forbid&lt;/code&gt; is the strict one. It makes the lint an error &lt;em&gt;and&lt;/em&gt; it makes that error impossible to override from inside the crate. A module cannot &lt;code&gt;#[allow]&lt;/code&gt; its way back out. Once a crate root says &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, there&amp;rsquo;s no &lt;code&gt;unsafe&lt;/code&gt; anywhere in that crate, and no local exception can be carved out. The compiler simply refuses.&lt;/p&gt;
&lt;p&gt;So every rust-tool-base crate that ships in a built tool forbids &lt;code&gt;unsafe&lt;/code&gt; at its root. Not &amp;ldquo;discourages&amp;rdquo;. Cannot contain it.&lt;/p&gt;
&lt;h2 id="the-one-honest-subtlety"&gt;The one honest subtlety
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a wrinkle, and it&amp;rsquo;s worth showing rather than hiding, because it&amp;rsquo;s where the design got specific.&lt;/p&gt;
&lt;p&gt;The workspace sets &lt;code&gt;unsafe_code = &amp;quot;deny&amp;quot;&lt;/code&gt; as the baseline for &lt;em&gt;everything&lt;/em&gt;, including test files. But test code occasionally has a real need for &lt;code&gt;unsafe&lt;/code&gt;. In the 2024 edition, &lt;code&gt;std::env::set_var&lt;/code&gt; became &lt;code&gt;unsafe&lt;/code&gt;, because mutating the process environment isn&amp;rsquo;t thread-safe, and a test that exercises environment-driven configuration has to call it.&lt;/p&gt;
&lt;p&gt;So the split is deliberate. The workspace-wide level is &lt;code&gt;deny&lt;/code&gt;, which a test file can locally &lt;code&gt;#[allow]&lt;/code&gt; when it genuinely needs that one environment call. But every production &lt;code&gt;lib.rs&lt;/code&gt; and &lt;code&gt;main.rs&lt;/code&gt; additionally carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt; cannot be relaxed. Test scaffolding gets a controlled, visible exception for a specific standard-library call. Shipping code gets none. The guarantee that matters, &amp;ldquo;the code in the binary contains no &lt;code&gt;unsafe&lt;/code&gt;&amp;rdquo;, holds, and the place it&amp;rsquo;s slightly loosened is exactly the place that never reaches a user.&lt;/p&gt;
&lt;h2 id="what-the-guarantee-is-actually-worth"&gt;What the guarantee is actually worth
&lt;/h2&gt;&lt;p&gt;Two things, one for users and one for reviewers.&lt;/p&gt;
&lt;p&gt;For users: an entire family of bug is ruled out of first-party code mechanically. Use-after-free, double-free, data races on shared memory, reading off the end of a buffer. These are the classic memory-safety vulnerabilities, and in a crate that forbids &lt;code&gt;unsafe&lt;/code&gt; they cannot originate, because the constructs that produce them cannot be written. That&amp;rsquo;s not careful coding. It&amp;rsquo;s the compiler refusing to build anything else.&lt;/p&gt;
&lt;p&gt;For reviewers: the cost of an &lt;code&gt;unsafe&lt;/code&gt; block is mostly the review burden it carries. Every one is a spot where a human has to check, by hand, that an invariant holds, and has to re-check it whenever nearby code changes. A crate that forbids &lt;code&gt;unsafe&lt;/code&gt; has zero of those. There&amp;rsquo;s no &lt;code&gt;unsafe&lt;/code&gt; block to audit, ever, because the compiler guarantees there isn&amp;rsquo;t one.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be straight about the boundary: this is a promise about rust-tool-base&amp;rsquo;s &lt;em&gt;own&lt;/em&gt; code. Its dependencies are another matter, and some of them do contain &lt;code&gt;unsafe&lt;/code&gt;, correctly. Keeping that side honest is a different job, done by &lt;a class="link" href="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/" &gt;vetting the dependency tree and gating it in CI&lt;/a&gt;. Within first-party code, though, the guarantee is real, and there&amp;rsquo;s no Go equivalent to it. Go has an &lt;code&gt;unsafe&lt;/code&gt; package, but nothing that lets a codebase prove, to the compiler, that it never touches it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Rust is memory-safe by default, but the &lt;code&gt;unsafe&lt;/code&gt; keyword exists so that default can be set aside. &amp;ldquo;Written in Rust&amp;rdquo; therefore does not by itself mean a project&amp;rsquo;s own code contains no &lt;code&gt;unsafe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base makes that the stronger claim. Every crate root carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt;, unlike &lt;code&gt;deny&lt;/code&gt;, cannot be overridden from inside the crate. Test files get a narrow, visible &lt;code&gt;deny&lt;/code&gt;-level exception for the one standard-library call that needs it; shipping code gets none. The payoff is a whole class of memory-safety bug ruled out of first-party code by construction, and not one &lt;code&gt;unsafe&lt;/code&gt; block left for a reviewer to audit.&lt;/p&gt;</description></item><item><title>Waivers with an expiry date</title><link>https://blog-570662.gitlab.io/waivers-with-an-expiry-date/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/waivers-with-an-expiry-date/</guid><description>&lt;img src="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/cover-waivers-with-an-expiry-date.png" alt="Featured image of post Waivers with an expiry date" /&gt;&lt;p&gt;A vulnerability scanner gives you a yes or a no. Is there a known advisory on a path you actually use? Yes, or no. That&amp;rsquo;s genuinely useful, and you should run one. But it&amp;rsquo;s a snapshot, taken on the day you ask, and supply-chain risk in a framework is a bigger and more ongoing thing than a single yes-or-no can capture.&lt;/p&gt;
&lt;p&gt;So rust-tool-base treats its whole dependency tree as something to have a &lt;em&gt;policy&lt;/em&gt; about, not something to scan and forget.&lt;/p&gt;
&lt;h2 id="a-scanner-answers-one-question"&gt;A scanner answers one question
&lt;/h2&gt;&lt;p&gt;When I &lt;a class="link" href="https://blog-570662.gitlab.io/every-finding-was-the-same-shape/" &gt;had go-tool-base security-audited&lt;/a&gt;, part of the routine was running a vulnerability scanner over the dependencies. Go has a good one. It looks at your dependency graph, cross-references known advisories, and tells you whether any of them reach code you actually call.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s useful and you should do it. But notice the shape of what it gives back: essentially a yes or a no. Either there&amp;rsquo;s a known vulnerability on a reachable path or there isn&amp;rsquo;t. It answers one question, on the day you ask it.&lt;/p&gt;
&lt;p&gt;Supply-chain risk in a framework is broader than that one question, because a framework drags its entire dependency tree into every tool built on it. rust-tool-base treats the whole tree as something to have a &lt;em&gt;policy&lt;/em&gt; about, and the tool for that is &lt;code&gt;cargo-deny&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-gate-not-a-scan"&gt;A gate, not a scan
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cargo-deny&lt;/code&gt; reads a &lt;code&gt;deny.toml&lt;/code&gt; and checks the dependency graph against four kinds of rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Licences.&lt;/strong&gt; There&amp;rsquo;s an allowlist: MIT, Apache-2.0, the BSD variants, ISC, a handful of others. Every transitive crate&amp;rsquo;s licence has to be on it. A dependency that pulls in something copyleft, or something with no licence at all, fails the build. You find out the first time it enters the tree, not during a release scramble when someone finally reads the legal implications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advisories.&lt;/strong&gt; It checks the RustSec advisory database, and yanked crates are set to &lt;code&gt;deny&lt;/code&gt;, so a dependency that&amp;rsquo;s been pulled from the registry stops CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bans.&lt;/strong&gt; Wildcard version requirements (&lt;code&gt;version = &amp;quot;*&amp;quot;&lt;/code&gt;) are denied outright, because a dependency that floats to whatever&amp;rsquo;s newest is a supply-chain hole by construction. Duplicate versions of the same crate get surfaced too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Crates may only come from the official registry. An unknown registry or a stray git dependency is denied. Nothing sneaks in from a URL.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a gate. It encodes, as rules in a file, what the project will and won&amp;rsquo;t accept into its dependency tree, and it enforces them on every build instead of once an audit.&lt;/p&gt;
&lt;h2 id="the-honest-part-is-the-waiver-list"&gt;The honest part is the waiver list
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing every real project runs into. Sooner or later there&amp;rsquo;s an advisory you genuinely can&amp;rsquo;t fix this week. It&amp;rsquo;s against a crate three levels down your tree. The fix needs an upstream release that hasn&amp;rsquo;t happened. The crate is scheduled to be reworked two milestones from now anyway. The gate is going to fail, and the work to satisfy it honestly isn&amp;rsquo;t available to you yet.&lt;/p&gt;
&lt;p&gt;The lazy response is a blanket ignore: silence the advisory, move on, forget. Now your gate has a hole in it that nobody remembers opening.&lt;/p&gt;
&lt;p&gt;rust-tool-base&amp;rsquo;s &lt;code&gt;deny.toml&lt;/code&gt; does something better. Every waiver in the &lt;code&gt;ignore&lt;/code&gt; list is a documented record. Each one carries a comment that names the crate, traces the &lt;em&gt;exact dependency path&lt;/em&gt; that reaches it, gives the reason, and names the condition that lifts it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# `instant` - reached via async-openai -&amp;gt; backoff -&amp;gt; rtb-ai (v0.3).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2024-0384&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# `paste` - reached via ratatui -&amp;gt; rtb-docs (v0.2) / rtb-tui (v0.4).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2024-0436&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The file states the policy out loud: &amp;ldquo;Every waiver points at a deferred stub crate that will be reworked before its ship milestone. Lift each waiver when the owning crate lands its v0.1.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Some waivers go further and carry a structured reason field, so the &lt;em&gt;why&lt;/em&gt; travels with the entry rather than living only in a comment above it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2025-0140&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gix-date via gix is a stub dependency; rtb-vcs v0.5 will upgrade&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read that list and you don&amp;rsquo;t see a project that quietly stopped caring about seven advisories. You see seven advisories the project knows about, can trace, and has tied to a specific milestone. The waiver has an expiry condition. When &lt;code&gt;rtb-vcs&lt;/code&gt; reaches v0.5, that &lt;code&gt;gix&lt;/code&gt; entry is meant to come out, and the comment is the reminder that it should.&lt;/p&gt;
&lt;h2 id="why-this-is-the-bit-to-copy"&gt;Why this is the bit to copy
&lt;/h2&gt;&lt;p&gt;A gate that can&amp;rsquo;t be relaxed is a gate people route around. They&amp;rsquo;ll find the broadest possible ignore and use it, because the alternative is being blocked on someone else&amp;rsquo;s release. The pressure to do that is real, and it&amp;rsquo;s not unreasonable.&lt;/p&gt;
&lt;p&gt;So the design that actually holds up isn&amp;rsquo;t a stricter gate. It&amp;rsquo;s a gate with an honest, structured escape hatch: you &lt;em&gt;can&lt;/em&gt; waive an advisory, but a waiver costs you a documented record with a dependency path and an expiry condition. That price is small enough that nobody routes around it, and high enough that waivers don&amp;rsquo;t accumulate silently. The &lt;code&gt;ignore&lt;/code&gt; list stays readable, and every line in it is something you could defend out loud.&lt;/p&gt;
&lt;p&gt;Supply-chain hygiene framed this way isn&amp;rsquo;t an audit you survive once a year. It&amp;rsquo;s bookkeeping: a ledger of what you accepted, why, and when each exception is due to close. Which, now I write it down, is just the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;Boy Scout rule&lt;/a&gt; again, pointed at a dependency tree. Leave it tidier than you found it, and write down the bits you couldn&amp;rsquo;t tidy yet.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;A vulnerability scanner answers one question on one day. &lt;code&gt;cargo-deny&lt;/code&gt; is a standing policy gate: licences against an allowlist, advisories and yanked crates denied, wildcard versions banned, sources restricted to the official registry, enforced on every build.&lt;/p&gt;
&lt;p&gt;The part of rust-tool-base&amp;rsquo;s setup worth copying is the waiver list. Every advisory that can&amp;rsquo;t be fixed yet is recorded with its crate, its dependency path, its reason and the milestone that removes it. A waiver is a dated note, not a shrug, and that&amp;rsquo;s what keeps the gate honest enough that nobody actually wants to bypass it.&lt;/p&gt;</description></item><item><title>A builder that won't compile if you forget a field</title><link>https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/cover-a-builder-that-wont-compile-if-you-forget-a-field.png" alt="Featured image of post A builder that won't compile if you forget a field" /&gt;&lt;p&gt;go-tool-base configures things with functional options, and if you forget a required one, the best case is a runtime failure and the worst case is an empty value sailing silently into everything downstream. Most builder patterns share the same hole. rust-tool-base closes it in a way I find genuinely delightful: the &lt;code&gt;.build()&lt;/code&gt; method simply doesn&amp;rsquo;t &lt;em&gt;exist&lt;/em&gt; until you&amp;rsquo;ve set every required field.&lt;/p&gt;
&lt;h2 id="when-is-a-required-field-actually-required"&gt;When is a required field actually required
&lt;/h2&gt;&lt;p&gt;Every framework has constructors with a mix of required and optional inputs. An &lt;code&gt;Application&lt;/code&gt; in rust-tool-base needs tool metadata and a version. It optionally takes a custom config type, extra commands, feature toggles. The metadata needs a name and a summary; a description and a help channel are optional.&lt;/p&gt;
&lt;p&gt;The interesting question is &lt;em&gt;when&lt;/em&gt; &amp;ldquo;required&amp;rdquo; gets enforced. There are really only two moments available: when the program runs, or when it compiles. Most APIs pick the first without ever framing it as a choice.&lt;/p&gt;
&lt;h2 id="how-go-tool-base-does-it"&gt;How go-tool-base does it
&lt;/h2&gt;&lt;p&gt;go-tool-base uses functional options, the standard Go pattern:&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="nx"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool&amp;#34;&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&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;&lt;code&gt;New&lt;/code&gt; takes a variadic list of options and applies them. It&amp;rsquo;s flexible and it reads well. But look at what the &lt;em&gt;type&lt;/em&gt; actually says. &lt;code&gt;New&lt;/code&gt; accepts zero or more options. The signature is satisfied by passing nothing at all. If &lt;code&gt;WithName&lt;/code&gt; is required, nothing in the type system knows that. Forget it and the code compiles cleanly, and you find out when the program runs, or worse, when it doesn&amp;rsquo;t visibly fail but quietly carries an empty name into everything downstream.&lt;/p&gt;
&lt;p&gt;A plain builder is no better here. &lt;code&gt;builder.name(&amp;quot;mytool&amp;quot;).build()&lt;/code&gt; and &lt;code&gt;builder.build()&lt;/code&gt; are both perfectly valid calls as far as the compiler is concerned. The builder &lt;em&gt;hopes&lt;/em&gt; you set the name. It can check at the end and return an error, but that check still happens at runtime.&lt;/p&gt;
&lt;p&gt;In every one of these the required-ness of a field is a fact that lives in documentation and in the author&amp;rsquo;s head, not in the code.&lt;/p&gt;
&lt;h2 id="typestate-putting-required-in-the-type"&gt;Typestate: putting &amp;ldquo;required&amp;rdquo; in the type
&lt;/h2&gt;&lt;p&gt;rust-tool-base builds these with &lt;code&gt;bon&lt;/code&gt;, and the pattern it generates is a &lt;em&gt;typestate&lt;/em&gt; builder. The idea is that the builder&amp;rsquo;s type changes as you call it, and that type tracks which required fields you&amp;rsquo;ve set so far.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolMetadata&lt;/span&gt;::&lt;span class="n"&gt;builder&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool&amp;#34;&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my CLI tool&amp;#34;&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ToolMetadata::builder()&lt;/code&gt; returns a builder in a state that records &amp;ldquo;name not set, summary not set&amp;rdquo;. Calling &lt;code&gt;.name(...)&lt;/code&gt; consumes that builder and returns a &lt;em&gt;different type&lt;/em&gt;, one whose state records &amp;ldquo;name set&amp;rdquo;. Calling &lt;code&gt;.summary(...)&lt;/code&gt; does the same for the summary.&lt;/p&gt;
&lt;p&gt;The part that matters is &lt;code&gt;.build()&lt;/code&gt;. It isn&amp;rsquo;t a method on the builder in general. It only exists on the builder type that represents &amp;ldquo;every required field has been set&amp;rdquo;. So this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolMetadata&lt;/span&gt;::&lt;span class="n"&gt;builder&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my CLI tool&amp;#34;&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;doesn&amp;rsquo;t compile. Not because a runtime check fired, but because in the state &amp;ldquo;name not set&amp;rdquo; there&amp;rsquo;s no &lt;code&gt;.build()&lt;/code&gt; method to call in the first place. The compiler stops you, and the error points straight at the missing &lt;code&gt;.name(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Optional fields stay optional. You can call &lt;code&gt;.description(...)&lt;/code&gt; or skip it, and &lt;code&gt;.build()&lt;/code&gt; is reachable either way, because the description was never part of the state that gates it. The required and the optional are genuinely different in the type, which is exactly the distinction the functional-options version could only keep in a comment.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Application::builder()&lt;/code&gt; works the same way. It won&amp;rsquo;t produce an &lt;code&gt;Application&lt;/code&gt; until it has metadata and a version, and &amp;ldquo;won&amp;rsquo;t&amp;rdquo; there means the method is absent, not that a check returns &lt;code&gt;Err&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="why-the-moment-matters"&gt;Why the moment matters
&lt;/h2&gt;&lt;p&gt;Moving the check from run time to compile time changes who finds the mistake, and when.&lt;/p&gt;
&lt;p&gt;A runtime check finds it when that code path executes, which might be in a test, might be in CI, might be on a user&amp;rsquo;s machine at the worst possible moment. A compile-time check finds it the instant you write it, in the editor, before anything has run at all. The same mistake, caught at the cheapest possible point instead of one of the expensive ones.&lt;/p&gt;
&lt;p&gt;It also changes what the API &lt;em&gt;documents about itself&lt;/em&gt;. A functional-options constructor can&amp;rsquo;t tell you, from its signature alone, which options you must pass. A typestate builder can, because the set of methods available to you at each step &lt;em&gt;is&lt;/em&gt; the documentation. You literally cannot reach &lt;code&gt;.build()&lt;/code&gt; without having been walked past every required field on the way.&lt;/p&gt;
&lt;p&gt;This is one of those places where Rust&amp;rsquo;s type system earns its reputation. The builder isn&amp;rsquo;t more careful than the Go version. It&amp;rsquo;s that &amp;ldquo;this field is required&amp;rdquo; stopped being a convention and became something the compiler enforces. (Another entry, if you&amp;rsquo;re keeping score from &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt;, in the column of outcomes that survived while the Go mechanism got left behind.)&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;Required fields have to be enforced somewhere. Functional options and ordinary builders enforce them at runtime, if at all, because &lt;code&gt;.build()&lt;/code&gt; is always callable and the type system never learns which inputs were mandatory.&lt;/p&gt;
&lt;p&gt;rust-tool-base uses typestate builders generated by &lt;code&gt;bon&lt;/code&gt;. The builder&amp;rsquo;s type changes as you set fields, and &lt;code&gt;.build()&lt;/code&gt; only exists once every required field is present. Forgetting one is a compile error that names the missing call, not a runtime surprise. The required-versus-optional distinction stops being a comment and becomes part of the type.&lt;/p&gt;</description></item><item><title>Registering commands without life before main</title><link>https://blog-570662.gitlab.io/registering-commands-without-life-before-main/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/registering-commands-without-life-before-main/</guid><description>&lt;img src="https://blog-570662.gitlab.io/registering-commands-without-life-before-main/cover-registering-commands-without-life-before-main.png" alt="Featured image of post Registering commands without life before main" /&gt;&lt;p&gt;I ended the &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;last post&lt;/a&gt; promising to show how a Rust command registers itself when the language flatly refuses to run any of your code before &lt;code&gt;main()&lt;/code&gt;. This is that post, and it&amp;rsquo;s a lovely example of reaching the same outcome by a completely different road.&lt;/p&gt;
&lt;p&gt;The outcome I wanted to keep is self-registration.&lt;/p&gt;
&lt;h2 id="what-self-registration-buys"&gt;What self-registration buys
&lt;/h2&gt;&lt;p&gt;A command in go-tool-base lives in its own file, and that file puts the command into the framework itself. There&amp;rsquo;s no central list of commands to keep in sync. You add a file, the command appears. You delete the file, it&amp;rsquo;s gone. Nothing else changes.&lt;/p&gt;
&lt;p&gt;That property is worth protecting. The alternative, a hand-maintained registry that every new command has to be threaded into, is exactly the sort of central file that turns into a merge-conflict magnet and quietly falls out of date. So when go-tool-base moved to Rust, self-registration was firmly in the column of things that had to survive.&lt;/p&gt;
&lt;p&gt;The way Go &lt;em&gt;did&lt;/em&gt; it was not.&lt;/p&gt;
&lt;h2 id="how-go-does-it"&gt;How Go does it
&lt;/h2&gt;&lt;p&gt;A Go package can declare an &lt;code&gt;init()&lt;/code&gt; function, and the runtime guarantees every &lt;code&gt;init()&lt;/code&gt; runs before &lt;code&gt;main()&lt;/code&gt; starts. A go-tool-base command file uses this to append itself to a package-level slice:&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="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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;DeployCommand&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;By the time &lt;code&gt;main()&lt;/code&gt; runs, every command file&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt; has already fired and the registry slice is populated. It&amp;rsquo;s a tidy trick, and it leans entirely on a Go feature: code that executes before &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="rust-doesnt-have-that"&gt;Rust doesn&amp;rsquo;t have that
&lt;/h2&gt;&lt;p&gt;Rust has no &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s no language-blessed phase that runs your code before &lt;code&gt;main()&lt;/code&gt;. This is a deliberate decision, not an oversight. Code running before &lt;code&gt;main()&lt;/code&gt; across many files has no well-defined order, and a startup phase whose ordering you can&amp;rsquo;t see is a classic source of subtle, miserable bugs. Rust closed that door on purpose.&lt;/p&gt;
&lt;p&gt;Which leaves a real question. If nothing runs before &lt;code&gt;main()&lt;/code&gt;, how does a command file insert itself into a registry without a central list editing it in?&lt;/p&gt;
&lt;h2 id="distributed-slices"&gt;Distributed slices
&lt;/h2&gt;&lt;p&gt;The answer is a crate called &lt;code&gt;linkme&lt;/code&gt;, and the mechanism is the &lt;em&gt;linker&lt;/em&gt; rather than a runtime phase.&lt;/p&gt;
&lt;p&gt;You declare a slice the framework will collect into:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice]&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="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;BUILTIN_COMMANDS&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A command file then contributes one entry to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Greet&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Greet&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 class="cm"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice(BUILTIN_COMMANDS)]&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="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;register_greet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;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="nb"&gt;Box&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Greet&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;Here&amp;rsquo;s the part that makes it work. The &lt;code&gt;#[distributed_slice]&lt;/code&gt; attribute doesn&amp;rsquo;t generate any code that runs at startup. It places each entry into a dedicated section of the compiled object file. When the linker builds the final binary, it gathers everything in that section and lays it out as one contiguous array. &lt;code&gt;BUILTIN_COMMANDS&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; that array.&lt;/p&gt;
&lt;p&gt;So by the time the program exists as a binary on disk, the registry is already assembled. &lt;code&gt;main()&lt;/code&gt; doesn&amp;rsquo;t build it. No &lt;code&gt;init()&lt;/code&gt; builds it. The linker built it, statically, as part of producing the executable. At runtime the framework iterates a slice that was complete before the process ever started.&lt;/p&gt;
&lt;h2 id="what-you-get-from-it"&gt;What you get from it
&lt;/h2&gt;&lt;p&gt;The outcome is the one Go&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt; gave, and then a bit more.&lt;/p&gt;
&lt;p&gt;A command still lives in one file and still self-registers. Adding a command is still adding a file. There&amp;rsquo;s still no central list.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s no startup phase to reason about, because there isn&amp;rsquo;t one. There&amp;rsquo;s no global mutable slice being appended to as &lt;code&gt;init()&lt;/code&gt;s fire, because nothing is appended at runtime; the slice is immutable and finished. There&amp;rsquo;s no ordering question, because the linker isn&amp;rsquo;t running your code, it&amp;rsquo;s collecting data. And it costs nothing at runtime: assembling the registry happened at link time, so program start just reads it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same idea go-tool-base had, expressed by the tool Rust actually gives you. Go reaches the registry through a controlled phase before &lt;code&gt;main()&lt;/code&gt;. Rust reaches it without any phase at all, because the linker did the assembly while the binary was still being built. Two roads, one destination&amp;hellip; which, if you&amp;rsquo;ve been following along, is becoming the whole theme of the Rust side of this project.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;Self-registration, where a command file inserts itself into the framework with no central list, is a property worth keeping. go-tool-base achieves it with a package-level &lt;code&gt;init()&lt;/code&gt;, leaning on Go&amp;rsquo;s guarantee that such functions run before &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rust has no equivalent and wants none, because code running before &lt;code&gt;main()&lt;/code&gt; has no clear ordering. rust-tool-base uses &lt;code&gt;linkme&lt;/code&gt; distributed slices instead: each command is placed into a dedicated linker section, and the linker assembles them into one contiguous, immutable slice as it builds the binary. The registry is complete before the program runs. Same outcome as Go&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt;, with no life before &lt;code&gt;main&lt;/code&gt; required.&lt;/p&gt;</description></item><item><title>rust-tool-base: the same idea, in a language that argues back</title><link>https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/</guid><description>&lt;img src="https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/cover-rust-tool-base-the-same-idea.png" alt="Featured image of post rust-tool-base: the same idea, in a language that argues back" /&gt;&lt;p&gt;I built &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;go-tool-base&lt;/a&gt; because I was sick of rebuilding the same CLI scaffolding every time I started a new Go tool. You&amp;rsquo;d think that would have taught me a lesson about doing things more than once. Apparently not, because I&amp;rsquo;ve now started building rust-tool-base: the same idea, the same itch, for Rust.&lt;/p&gt;
&lt;p&gt;In my defence, there&amp;rsquo;s method in it.&lt;/p&gt;
&lt;h2 id="the-same-itch-a-different-language"&gt;The same itch, a different language
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I kept writing the same couple of hundred lines of wiring every time I started a new Go CLI. Config loading, logging setup, an update check, an error path, a help system. None of it was the tool. All of it had to be there before the tool could be.&lt;/p&gt;
&lt;p&gt;Lately I&amp;rsquo;ve been learning Rust, and two things collided. The first is how I tend to learn a language. I&amp;rsquo;ve always picked them up reasonably quickly, and the way I do it isn&amp;rsquo;t with a tutorial that builds a toy, it&amp;rsquo;s by rebuilding something whose shape I already know cold, so that every decision is about &lt;em&gt;the language&lt;/em&gt; rather than &lt;em&gt;the problem&lt;/em&gt;. The second is that every time I started a Rust CLI of any size, I hit the very same gap I&amp;rsquo;d already filled once in Go.&lt;/p&gt;
&lt;p&gt;So rather than learn Rust on a throwaway, I decided to learn it by building rust-tool-base: the same idea, the same niche, for Rust.&lt;/p&gt;
&lt;h2 id="the-gap-in-rust"&gt;The gap in Rust
&lt;/h2&gt;&lt;p&gt;The Rust ecosystem has a well-earned reputation for sharp, focused crates and a deliberate shortage of big opinionated frameworks. &lt;code&gt;clap&lt;/code&gt; for argument parsing, &lt;code&gt;figment&lt;/code&gt; for layered config, &lt;code&gt;tracing&lt;/code&gt; for logging, &lt;code&gt;miette&lt;/code&gt; for errors, &lt;code&gt;ratatui&lt;/code&gt; for terminal UI, &lt;code&gt;reqwest&lt;/code&gt; and &lt;code&gt;tokio&lt;/code&gt; underneath. Each of them is genuinely best-in-class.&lt;/p&gt;
&lt;p&gt;What nobody hands you is the assembly. Wiring those into one coherent product, and then adding self-update, AI integration, an MCP server, embedded documentation, credential handling, telemetry and a scaffolder, is real work, and it&amp;rsquo;s the same work on every project.&lt;/p&gt;
&lt;p&gt;The closest existing neighbours stop short of it. &lt;code&gt;cli-batteries&lt;/code&gt; is a thin preamble: argument parsing plus a logging subscriber plus panic and signal handling. &lt;code&gt;starbase&lt;/code&gt; has a proper session and lifecycle model but is CLI-agnostic and shaped around the moonrepo tooling it came from. &lt;code&gt;cargo-dist&lt;/code&gt; and &lt;code&gt;cargo-release&lt;/code&gt; are about release packaging, not the runtime. Good tools, all of them, but none is the opinionated, full-lifecycle, scaffolded base that go-tool-base is in the Go world. That space is empty, and rust-tool-base is built to fill it.&lt;/p&gt;
&lt;h2 id="why-it-is-not-a-port"&gt;Why it is not a port
&lt;/h2&gt;&lt;p&gt;The obvious way to build this would be to open go-tool-base and translate it file by file. I&amp;rsquo;m not doing that, and the reason matters enough that it&amp;rsquo;s the rule the whole project is built around.&lt;/p&gt;
&lt;p&gt;go-tool-base is full of Go. It leans on a &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;&lt;code&gt;Props&lt;/code&gt; struct&lt;/a&gt; that carries the framework&amp;rsquo;s services in loosely-typed fields. It configures things with functional options. It registers commands using package-level &lt;code&gt;init()&lt;/code&gt;. It threads a &lt;code&gt;context.Context&lt;/code&gt; through every call. Those are all good, idiomatic Go. Transliterated into Rust they&amp;rsquo;d become code that argues with the compiler on every single line, because Rust has its own answers to every one of those problems and they are emphatically not the Go answers.&lt;/p&gt;
&lt;p&gt;So rust-tool-base reaches the &lt;em&gt;same outcomes&lt;/em&gt; by Rust&amp;rsquo;s means. Commands still self-register, but through link-time machinery instead of &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s still one context object per command, but it&amp;rsquo;s strongly typed rather than a loosely-keyed bag. Configuration is still layered, but it lands in your own typed struct instead of a string-keyed lookup. Same philosophy, same shape of product, an entirely different ecosystem underneath. The README says it plainly: it&amp;rsquo;s a sibling, not a port.&lt;/p&gt;
&lt;h2 id="why-do-it-twice-at-all"&gt;Why do it twice at all
&lt;/h2&gt;&lt;p&gt;Three reasons, and they reinforce each other.&lt;/p&gt;
&lt;p&gt;The first is plain usefulness. The next time I want a Rust CLI tool, I want the same head start go-tool-base already gives me in Go.&lt;/p&gt;
&lt;p&gt;The second is the learning. Rebuilding a system I understand forces me to meet Rust&amp;rsquo;s idioms where they actually bite, not where a tutorial gently stages them. You learn ownership properly when a real design is pushing back at you.&lt;/p&gt;
&lt;p&gt;The third is the one I didn&amp;rsquo;t expect, and it&amp;rsquo;s the subject of the next post. Building the same framework twice, in two languages, turns out to be the cleanest way to find out which of your original decisions were genuine &lt;em&gt;design&lt;/em&gt; and which were merely &lt;em&gt;idiom&lt;/em&gt;. The design survives the move. The idiom does not. Sorting one from the other has been the most interesting part so far.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;rust-tool-base is the Rust sibling of go-tool-base: the same batteries-included, scaffolded, opinionated CLI framework, aimed at the same gap, which in Rust is the gap between a pile of excellent crates and a coherent product.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a port. Transliterating Go idioms into Rust produces code that fights the language, so RTB reaches the same outcomes through Rust&amp;rsquo;s own mechanisms instead. The posts after this one walk through the specific cases: how commands register, how the builder works, how errors are reported, and a few things RTB can do that the Go version structurally can&amp;rsquo;t. First, though, the thing the exercise taught me about my own design.&lt;/p&gt;</description></item></channel></rss>