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
- keryx Keryx is ancient Greek for 'the herald', which is precisely the job.
- krites Krites is ancient Greek for 'the judge', and judging photographs is the whole job.
- phpbotscout A support and moderation bot bridging Discord and GitLab: it answers what the docs already cover, with citations, and offers to raise an issue with the thread attached when it can't. Every question it can't answer is a documentation gap with a timestamp on it.
- sigillum A standalone command-line tool for signing and verifying release artefacts with OpenPGP. The private key never leaves your KMS or PEM file, and nothing about it cares what language your project is written in.
- skillup Version the segments of a Claude Code plugin marketplace from the history of their manifests, so each plugin gets the version its own changes earned. Correct even on a repository that has never been tagged.
The story in posts
- What happens when an AI burns out
- The bill for fifty-one modules
- The extraction playbook only got real when chat left
- Prove the bug before you fix it
- The packages that didn't want to leave
- I graded every package for how badly it wants to leave
- The day a script needs changing everywhere
- Config is a border, not a chain
- An agent is a loop with real tools and a fence
- Publish your key where the platform can't touch it
Last reviewed .
