TL;DR: go-tool-base has runtime feature flags that decide which built-in commands are active in a given run. rust-tool-base has those too, but it also has a second, completely separate kind: Cargo features. A runtime flag decides what a command does. A Cargo feature decides what is in the binary at all. Switch off the vcs feature and the entire rtb-vcs crate, with its whole dependency subtree, is never compiled and never linked. The two systems are deliberately kept apart.
A workspace of crates
Before the flags, the shape that makes them possible. go-tool-base is one Go module with packages under pkg/. rust-tool-base is a Cargo workspace of seventeen crates: rtb-app, rtb-config, rtb-cli, rtb-vcs, rtb-ai, rtb-mcp, rtb-docs, rtb-telemetry, and so on, with an umbrella crate called rtb that re-exports the public surface.
That isn’t tidiness for its own sake. Each subsystem being a separately compilable crate is what gives you a unit you can include or exclude wholesale. Hold onto that, because it’s the hinge for everything below.
The flag go-tool-base already has
go-tool-base has feature flags, and I’d describe them as runtime flags. A tool built on it can enable or disable built-in commands:
props.SetFeatures(
props.Disable(props.InitCmd),
props.Enable(props.AiCmd),
)
At startup the framework resolves that set and decides which commands are reachable for this run. The init command might be present in the binary but switched off; the ai command might be switched on. It’s about the user-facing surface: which commands exist for someone typing --help.
rust-tool-base keeps this idea. A command carries a CommandSpec with an optional feature field, and the runtime decides whether a feature-gated command is reachable. Same purpose: shape the surface per invocation.
If that were the whole story, there’d be nothing to write. The reason there’s a post is the other kind of flag, which Rust makes available and Go really doesn’t.
The flag Rust adds
Cargo features are a compile-time mechanism. The rtb umbrella crate declares them like this:
[features]
default = ["cli", "update", "docs", "mcp", "credentials", "tui"]
cli = ["dep:rtb-cli"]
update = ["dep:rtb-update"]
ai = ["dep:rtb-ai", "rtb-docs?/ai"]
vcs = ["dep:rtb-vcs"]
telemetry = ["dep:rtb-telemetry"]
full = ["cli", "update", "docs", "mcp", "ai", "credentials", "tui", "telemetry", "vcs"]
Each subsystem is an optional crate dependency, and a feature switches it on. This is a different kind of switch entirely, and the difference is the whole point.
A runtime flag decides what a command does while the program runs. The code is in the binary either way; the flag just gates it.
A Cargo feature decides what is in the binary in the first place. Build a tool without the vcs feature and rtb-vcs is not compiled. Its dependencies are not compiled. gix, the pure-Rust Git implementation rtb-vcs pulls in, roughly two and a half megabytes of it, is not compiled and not linked. It is not switched off in the binary. It was never in the binary. The compiler never saw it.
That is something a runtime flag cannot do, because by the time anything runs, the binary already exists with everything in it.
Two axes, kept separate
So rust-tool-base has two flag systems answering two genuinely different questions.
Cargo features answer: what is this binary made of? They’re decided when you build the tool, in Cargo.toml. They control compilation, binary size, dependency surface, and compile time. A tool that never touches Git builds without vcs and is smaller, faster to compile, and has a smaller dependency tree to audit. A tool that wants everything turns on full.
Runtime feature flags answer: what can the user do in this run? They’re decided as the program starts. They control which commands appear, which paths are reachable.
These could have been mashed into one mechanism, and it would have been a mistake. The app-context design notes are blunt about it: feature gating doesn’t belong on the per-command context object, because a feature-gated command “either exists or doesn’t” rather than changing its behaviour mid-run. Compile-time composition is one decision, made by the person building the tool. Runtime gating is another, made per invocation. Conflating them would mean you couldn’t reason about either.
The Go version of this had to be hand-built
This isn’t a thing Go simply lacks. I wrote a whole post about how go-tool-base keeps its optional keychain dependency out of binaries that don’t want it, using a blank import and the linker’s dead-code elimination. It works. But it was a piece of deliberate engineering for one dependency, and getting it right took care.
Cargo features make that same outcome a first-class, declarative thing, and not for one dependency but for every subsystem the framework has. You don’t engineer the exclusion. You name a feature and leave it off. The crate, and its whole subtree, stays out. Rust’s build system was designed for exactly this, and rust-tool-base leans on it across the entire workspace rather than hand-rolling it once.
What it comes down to
go-tool-base has runtime feature flags: they decide, per invocation, which built-in commands are reachable. rust-tool-base keeps that, and adds a second kind that Rust makes available.
Cargo features decide what the binary is compiled from. Each of the framework’s seventeen crates is an optional dependency, and a feature switched off means that crate and its entire dependency subtree are never compiled or linked. A runtime flag gates what code does; a Cargo feature gates whether code is there at all. Two axes, two questions, deliberately kept as separate systems.