Showcase · Framework · Go

go-tool-base

The underpinnings for any Go application that ships as a binary: command-line tools to start with, and (as more than half of what is built on it now proves) long-running services and local web UIs too.

v0.46.1, released Pre-1.0

DocsInstallRepositoryGuide

The problem it exists for

Most of a Go application isn’t the part that’s actually yours. It’s configuration that layers files, environment and flags and can tell you where a value came from, an update you can trust not to install somebody else’s binary, errors a user can act on, a clean way to start, run and stop, and the same handful of commands (init, update, doctor, config, docs) written again for every new thing you build.

go-tool-base writes those once. It’s a batteries-included framework (think Rails or Laravel, for a Go binary), and it started out for command-line tools, because everything starts life as a binary you run. But more than half of what’s built on it today doesn’t exit when a command finishes. keryx and krites each serve a local web studio from the same binary, phpbotscout runs as a long-lived bot, and Scout has a window and a voice.

How it works

The usual commands, already written

A tool built on go-tool-base starts from one shared bundle of the things every command needs (its name and version, a logger, its config, the filesystem, error handling) and adds its own commands, and the framework builds the rest of the tool around them. The built-in commands arrive as features: update, init, MCP, docs, doctor and changelog are on by default, and AI, config, telemetry and man pages are opt-in. A tool switches each one on or off, and the framework behaves accordingly. A tool without an init command, for instance, is allowed to start with no config file at all, because it has no way to create one.

A generator that keeps writing the boring bits

The gtb binary is the generator. gtb generate project scaffolds a new tool, gtb generate command adds a command, and add-flag, regenerate, remove, enable and disable keep working on the tool after day one. gtb enable signing, for example, wires in signed self-update with the verification turned on.

Long-running, too

A tool that needs to stay up gets the same supervisor the framework’s service commands use, controls: concurrent startup, health and readiness probes, shutdown in reverse order of registration, and restarts when something falls over. That’s what sits under a studio served from the CLI, or a bot that’s meant to run for weeks, and the config, update and signing story is the same one the short-lived commands get.

Made of modules, not a monolith

Most of what go-tool-base does lives in separate toolkit modules it requires: config, errors, transport, observability, forge for releases and self-update, signing, MCP and chat. go-tool-base is assembled from 45 of them, and the toolkit they come from now holds 93 public modules, nearly all of them split out since the extraction playbook was written in mid-July 2026. You can use any of them without go-tool-base, and go-tool-base is what makes them agree with each other.

Decisions and what they cost

  • Split into modules. The framework was broken out into modules one package at a time, following an extraction playbook (spec 0119), and the generator itself became a nested module of its own (spec 0194). The payoff was measurable: a minimal binary went from 81.3 MB to 34.3 MB, 58% smaller, once the adapters stopped coming along for the ride. What it cost: shared test harnesses get copied into each module, and the generator’s own packages are importable but not supported.
  • Off Viper. Config moved from a Viper wrapper to go/config’s store (spec 0136). What decided it was coherence: reading from a frozen snapshot means a set of related reads never mixes values from before and after a reload, and config’s own test holds that at zero mismatches. A forwarding adapter that would have kept the old API was turned down because it brought the mismatch straight back. What it cost: roughly 350 call sites and 595 lines of documentation, rewritten.
  • An errors package of my own. I’d originally chosen cockroachdb/errors (spec 0001), and later reversed my own decision: the estate used ten of its functions, and paid 46 of go/forge’s 376 transitive packages for them. go/errors replaced it, with hints for the user built in (the reason an alternative without them was passed over). What it cost: one change touching 116 files and 50 sentinel errors.
  • Signed self-update. An update is verified against a signature before it’s installed (spec 0056), using GPG with keys published over WKD, chosen over cosign so verification still works offline and behind a firewall. What it cost: a compromise of both the forge and the key directory at once wouldn’t be detected until a transparency log lands, and that’s still a draft.

Proof in use

  • Seven tools are built on it today, all on the same release: keryx, krites, sigillum, skillup, phpbotscout, colophon and Scout.
  • 66 releases since v0.1.0 in May 2026, and the version in the header above is the current one. A tool’s release merge request runs cicd’s go-core-currency check, which flags a release still built on an older go-tool-base. All seven, and go-tool-base itself, build and publish their binaries through cicd’s goreleaser component.
  • 290 behaviour scenarios across 52 feature files, and 2,954 test and fuzz functions, with a 90% coverage threshold on the pipeline.

Use it when, and when not to

Use it if you’re building a Go application that real people will install and keep updated, whether that’s a command-line tool, something that runs for weeks, or a local web UI served from the same binary, and you’d rather not write config layering, signed self-update, graceful shutdown, a doctor command and an MCP server for the fourth time. Release builds cover Linux, macOS and Windows on amd64 and arm64.

It isn’t a web framework, though. It won’t route your requests or render your pages, and it won’t scaffold a microservice for you: what a long-running process gets from it is the lifecycle around the work, not the work. And it’s overkill if all you need is a command router. It’s also pre-1.0: breaking changes still ship as minor releases and the stability tiers aren’t in force yet, so pin the version.

Where it’s going

The static release channel (spec 0203) has shipped. A tool can find and fetch its own releases from a plain HTTPS location, with no forge in either step, and an updater that needs nothing but HTTPS and its embedded keys is one that can repair a broken install. keryx is the next tool to move onto it. Behind that, update channels and rollback (spec 0104) are drafted, and need another look now that the channel lists every version (go-tool-base#117).

Built on it

The story in posts

All 84 posts →

Last reviewed .