<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Config on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/config/</link><description>Recent content in Config on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Thu, 21 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/config/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a CLI with go-tool-base, part 2: configuration your tool can trust</title><link>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/cover-building-a-cli-with-go-tool-base-part-2.png" alt="Featured image of post Building a CLI with go-tool-base, part 2: configuration your tool can trust" /&gt;&lt;p&gt;In &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/" &gt;part 1&lt;/a&gt;
you scaffolded a tool and gave it a &lt;code&gt;hello&lt;/code&gt; command. It says the same thing
every time, which is fine for a first command and useless for a real one. The
moment a tool does anything worth doing it needs settings: an endpoint, a
default, a token, a log level. And the moment you have settings, you have the
problem nobody warns you about. You set one in a file, the tool ignores it, the
code that reads it looks perfectly correct, and an hour later you find you&amp;rsquo;d
typed &lt;code&gt;tiemout&lt;/code&gt;. Nothing in the whole stack thought that worth a word.&lt;/p&gt;
&lt;p&gt;The good news is you don&amp;rsquo;t have to build any of this. Your scaffold already
wired up a config system in part 1, the same one the rest of go-tool-base uses.
This part puts it to work: where a setting&amp;rsquo;s value actually comes from, how to
ship sensible defaults alongside the command they belong to, how to layer files
so a team and a laptop can disagree politely, and how to turn a fat-fingered key
from a silent shrug into an error that tells you exactly what you got wrong.&lt;/p&gt;
&lt;p&gt;The same version note as part 1, since each of these stands on its own:
everything here is written against &lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt; (&lt;code&gt;gtb version&lt;/code&gt; will
tell you what you&amp;rsquo;re on). The tool is young and still changing shape, so if you&amp;rsquo;re
on a newer release and a detail has drifted, that&amp;rsquo;s the first thing to check. I&amp;rsquo;ll
flag anything that breaks across versions as it comes up.&lt;/p&gt;
&lt;h2 id="you-already-have-a-config-system"&gt;You already have a config system
&lt;/h2&gt;&lt;p&gt;The root command loads configuration for you before any of your command code
runs, merges every source together, and hands the result to each command through
&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;&lt;/a&gt;.
By the time your &lt;code&gt;RunHello&lt;/code&gt; runs, &lt;code&gt;props.Config&lt;/code&gt; is populated and ready.&lt;/p&gt;
&lt;p&gt;A value can arrive from several places at once, so there&amp;rsquo;s an order. Highest
wins:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Command-line flags&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment variables&lt;/strong&gt; (your tool&amp;rsquo;s prefix plus the key, so
&lt;code&gt;hello.greeting&lt;/code&gt; reads &lt;code&gt;MYTOOL_HELLO_GREETING&lt;/code&gt;, with the dots turned into
underscores)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config files&lt;/strong&gt; (on disk, in the order they were loaded)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That ladder is the mental model for what beats what: a flag beats an env var, an
env var beats a file. The files are worth pinning down, though, because there&amp;rsquo;s
more than one and they don&amp;rsquo;t all come from the same place. This is the bit that&amp;rsquo;s
easy to trip over:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedded defaults&lt;/strong&gt; are baked into the binary, one slice per command. You
don&amp;rsquo;t read these at runtime directly. The &lt;code&gt;init&lt;/code&gt; command (coming up) bakes them
into your config file for you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The file &lt;code&gt;init&lt;/code&gt; writes&lt;/strong&gt;, &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;, is the default the tool
reads, along with a machine-wide &lt;code&gt;/etc/mytool/config.yaml&lt;/code&gt; if one exists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Files passed with &lt;code&gt;--config&lt;/code&gt;&lt;/strong&gt; replace those defaults for that run rather than
adding to them. Name one or more and the tool reads exactly those.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;rsquo;ll set each of these up in turn. The full reference lives in the
&lt;a class="link" href="https://gtb.phpboyscout.uk/components/config/" target="_blank" rel="noopener"
 &gt;config docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Reading a value is one call, and it&amp;rsquo;s typed:&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;greeting&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="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.greeting&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="nx"&gt;timeout&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="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;server.timeout&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="nx"&gt;debug&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="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;verbose&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="give-a-command-a-setting"&gt;Give a command a setting
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s make &lt;code&gt;hello&lt;/code&gt; configurable. Open &lt;code&gt;pkg/cmd/hello/main.go&lt;/code&gt; (your file, the one
the generator leaves alone) and read the greeting from config instead of
hard-coding it:&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;RunHello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;HelloOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&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;greeting&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="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.greeting&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="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&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;Build and run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;just build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERRO failed to load config: no configuration files found
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;please run init, or provide a config file using the --config flag
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not what you expected, maybe, but it&amp;rsquo;s the right instinct from the tool. It has
no configuration to read yet, and rather than guess, it stops and says so. Which
brings us neatly to where settings actually come from.&lt;/p&gt;
&lt;h2 id="defaults-belong-to-the-command"&gt;Defaults belong to the command
&lt;/h2&gt;&lt;p&gt;You could drop a default into the project&amp;rsquo;s central config, and for something
truly global like the log level that&amp;rsquo;s the right home. But a setting that belongs
to &lt;code&gt;hello&lt;/code&gt; should live with &lt;code&gt;hello&lt;/code&gt;, not in a shared file you have to remember to
edit every time you add a command. The generator does this for you, you just have
to ask. Back in part 1 you generated &lt;code&gt;hello&lt;/code&gt; without config support, so run the
same command again with &lt;code&gt;--assets&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; --name hello --short &lt;span class="s2"&gt;&amp;#34;Say hello&amp;#34;&lt;/span&gt; --assets
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is safe to re-run. The generator honours the code you&amp;rsquo;ve already written:
it refreshes the boilerplate &lt;code&gt;cmd.go&lt;/code&gt;, adds the asset scaffolding, and leaves
your &lt;code&gt;main.go&lt;/code&gt;, and the &lt;code&gt;RunHello&lt;/code&gt; you&amp;rsquo;ve been editing, completely alone. One
thing to hold off on here: don&amp;rsquo;t reach for &lt;code&gt;--force&lt;/code&gt;. Force rewrites everything,
including that &lt;code&gt;main.go&lt;/code&gt;, which is exactly the work you want to keep.&lt;/p&gt;
&lt;p&gt;You now have &lt;code&gt;pkg/cmd/hello/assets/init/config.yaml&lt;/code&gt;, and the generator has
already opened it under the command&amp;rsquo;s own namespace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&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;Fill in your defaults under it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&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="nt"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Hello&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="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;plain&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;Those values are embedded into the binary as an asset, and the generated &lt;code&gt;cmd.go&lt;/code&gt;
registers them with &lt;code&gt;Props&lt;/code&gt; for you (&lt;code&gt;props.Assets.Register(&amp;quot;hello&amp;quot;, &amp;amp;assets)&lt;/code&gt;),
so the config system knows where your command&amp;rsquo;s defaults live. A quick word on
&lt;code&gt;style&lt;/code&gt;, since we&amp;rsquo;ll lean on it shortly: it&amp;rsquo;s a second setting I&amp;rsquo;m giving a
default now so it&amp;rsquo;s ready when we need it. Plain says the greeting as written;
loud will shout it.&lt;/p&gt;
&lt;p&gt;That per-command home comes with one rule worth taking seriously: namespace your
keys. Notice the generator opened the file under a &lt;code&gt;hello:&lt;/code&gt; key rather than at
the top level. Copy that. Every command ships its defaults in its own embedded
file, and those files are all merged together to build the config, but the order
they merge in is not guaranteed. If two commands both defined a top-level
&lt;code&gt;timeout&lt;/code&gt;, which one won would be a toss-up that could flip between builds. Keep
each command&amp;rsquo;s settings under its own name (&lt;code&gt;hello.greeting&lt;/code&gt;, &lt;code&gt;report.timeout&lt;/code&gt;)
and the clash can&amp;rsquo;t happen in the first place. The generator namespacing the file
for you is a hint worth taking.&lt;/p&gt;
&lt;p&gt;One thing the defaults file does not do is set values through struct tags. If you
later add a &lt;code&gt;default:&amp;quot;info&amp;quot;&lt;/code&gt; tag to a config field, that&amp;rsquo;s documentation for the
error messages, nothing more. Real defaults live here, in the embedded YAML. It&amp;rsquo;s
an easy thing to assume otherwise and then wonder why your default never applied.&lt;/p&gt;
&lt;h2 id="first-run-init"&gt;First run: init
&lt;/h2&gt;&lt;p&gt;So your defaults are baked into the binary. The tool still needs an actual config
file to read, and that&amp;rsquo;s what &lt;code&gt;init&lt;/code&gt; is for. It&amp;rsquo;s one of the features your tool
shipped with, so it&amp;rsquo;s already there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Configuration initialised in /home/you/.mytool/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Open that file and you&amp;rsquo;ll find your command&amp;rsquo;s defaults waiting in it, merged with
the framework&amp;rsquo;s own:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&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="nt"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Hello&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="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;plain&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="nt"&gt;log&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="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;info&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the missing piece. &lt;code&gt;init&lt;/code&gt; gathers every command&amp;rsquo;s embedded defaults
through the &lt;code&gt;Assets&lt;/code&gt; layer, writes them to &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;, locks the
file down to &lt;code&gt;0600&lt;/code&gt; (it may hold secrets later), and drops in a &lt;code&gt;.gitignore&lt;/code&gt; so
nobody commits it by accident. Now &lt;code&gt;hello&lt;/code&gt; has something to read:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Prefer no init step?&lt;/strong&gt; &lt;code&gt;init&lt;/code&gt; is a feature, and you can leave it out of your
tool&amp;rsquo;s feature set. With it off, the tool loads its embedded defaults directly
and runs with no config file at all, you&amp;rsquo;d only add one to override something.
That suits a small, self-contained tool. This tutorial keeps &lt;code&gt;init&lt;/code&gt; on, which
is the default and the right call while a tool is finding its feet, so the rest
of the article assumes it.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="setup-that-needs-a-human-initialisers"&gt;Setup that needs a human: initialisers
&lt;/h2&gt;&lt;p&gt;Static defaults cover the values you can decide for the user. Some you can&amp;rsquo;t: a
token, an API key, an endpoint that differs per person. Writing a blank or
guessed value for those is worse than useless. This is where go-tool-base does
something I&amp;rsquo;ve not seen many CLI frameworks bother with: it lets a command bring
its own first-run setup, and wires it in for you. It&amp;rsquo;s one of the genuine reasons
to build on the framework rather than roll your own, so it&amp;rsquo;s worth a proper look.&lt;/p&gt;
&lt;p&gt;Generate a command with &lt;code&gt;--with-initializer&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; --name greet --short &lt;span class="s2"&gt;&amp;#34;Greet someone&amp;#34;&lt;/span&gt; --with-initializer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alongside the usual files you get an &lt;code&gt;init.go&lt;/code&gt;. It&amp;rsquo;s generated and marked &lt;code&gt;DO NOT EDIT&lt;/code&gt;, and it does all the wiring. Here&amp;rsquo;s the heart of it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Code generated by gtb. DO NOT EDIT.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greet&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="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;setup&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="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FeatureCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet&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="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InitialiserProvider&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Initialiser&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="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;skipGreet&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&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="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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&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="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="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubcommandProvider&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&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="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;NewCmdInitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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="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="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FeatureFlag&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&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;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;BoolVar&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;skipGreet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;skip-greet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;skip initializing greet configuration&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="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="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;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="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&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="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet&amp;#34;&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="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;IsConfigured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet&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="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="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;InitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&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;That package &lt;code&gt;init()&lt;/code&gt; registers three things with the framework the moment your
command is imported, with no central setup file for you to edit: the initialiser
itself, an &lt;code&gt;init greet&lt;/code&gt; subcommand so the user can reconfigure just this command
later, and a &lt;code&gt;--skip-greet&lt;/code&gt; flag on the main &lt;code&gt;init&lt;/code&gt;. &lt;code&gt;IsConfigured&lt;/code&gt; is how the
framework avoids nagging: if the &lt;code&gt;greet&lt;/code&gt; key is already in the config, &lt;code&gt;init&lt;/code&gt;
leaves it be and moves on.&lt;/p&gt;
&lt;p&gt;All of that is generated for you. The one piece that&amp;rsquo;s yours is the &lt;code&gt;InitGreet&lt;/code&gt;
function in &lt;code&gt;main.go&lt;/code&gt;, which starts as a stub:&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;InitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&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;// TODO: Implement custom initialization logic for greet&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&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;Fill it in with whatever the setup needs. go-tool-base leans on
&lt;a class="link" href="https://github.com/charmbracelet/huh" target="_blank" rel="noopener"
 &gt;huh&lt;/a&gt; for prompts, the same library its own
GitHub and AI setup use, so a one-question form looks like this:&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;InitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&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="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&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="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;form&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;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewForm&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;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGroup&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;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewInput&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="nf"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;What greeting should greet use?&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="nf"&gt;Value&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;greeting&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="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="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="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&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="kc"&gt;nil&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&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="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="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet.greeting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greeting&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&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;Set the value on &lt;code&gt;cfg&lt;/code&gt; and you&amp;rsquo;re done. After the initialisers run, &lt;code&gt;init&lt;/code&gt; writes
the whole config out to disk, so the answer persists into &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;
with everything else. Run &lt;code&gt;mytool init&lt;/code&gt; on a fresh machine now and it stops to ask
for the greeting; run it again and it sails past, because &lt;code&gt;IsConfigured&lt;/code&gt; sees the
key is already there. Need to redo just this one command&amp;rsquo;s setup? &lt;code&gt;mytool init greet&lt;/code&gt;. The framework hands each command its own setup step, its own subcommand
and its own skip flag, and asks you for a single function in return. That&amp;rsquo;s the
trade worth making: static defaults in your embedded YAML, anything that needs a
human in an initialiser.&lt;/p&gt;
&lt;h2 id="overriding-the-environment-and-layered-files"&gt;Overriding: the environment and layered files
&lt;/h2&gt;&lt;p&gt;With a config file in place, the other sources come into their own. The quickest
override is an environment variable. Remember the prefix you set when scaffolding
in part 1: &lt;code&gt;hello.greeting&lt;/code&gt; maps to &lt;code&gt;MYTOOL_HELLO_GREETING&lt;/code&gt;, the prefix and key
joined up, uppercased, dots turned to underscores:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MYTOOL_HELLO_GREETING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Hello from mytool&amp;#34;&lt;/span&gt; ./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Hello from mytool
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You didn&amp;rsquo;t register that variable anywhere; the config system binds it for you.
The prefix is what keeps it from colliding with some other tool&amp;rsquo;s &lt;code&gt;LOG_LEVEL&lt;/code&gt; on
the same machine, which is exactly why it&amp;rsquo;s worth having.&lt;/p&gt;
&lt;p&gt;Files are the other half, and they&amp;rsquo;re where that precedence list earns a closer
look. A single config file is fine until two people, or two machines, want
slightly different settings, and then you&amp;rsquo;re copying files around by hand. The
&lt;code&gt;--config&lt;/code&gt; flag fixes that: pass it more than once and the tool merges the files
in order.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --config ./config.yaml &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --config ./config.local.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Between the files you name, the rule is &lt;strong&gt;later wins on a clash, and every key
that doesn&amp;rsquo;t clash is kept.&lt;/strong&gt; If &lt;code&gt;config.yaml&lt;/code&gt; sets &lt;code&gt;hello.greeting: Hello&lt;/code&gt; and
&lt;code&gt;config.local.yaml&lt;/code&gt; sets &lt;code&gt;hello.greeting: Oi&lt;/code&gt;, you get &lt;code&gt;Oi&lt;/code&gt;, but keys that appear
in only one file survive untouched. It&amp;rsquo;s a merge between them, not a replacement.&lt;/p&gt;
&lt;p&gt;The edge to remember is what &lt;code&gt;--config&lt;/code&gt; does to the default locations: it replaces them.
The moment you name a file, &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt; drops out of the picture
unless you name it too. So you pass the whole stack you want, a shared base and a
local override together, and let precedence settle it. Commit a &lt;code&gt;config.yaml&lt;/code&gt; with
the team&amp;rsquo;s settings, keep an untracked &lt;code&gt;config.local.yaml&lt;/code&gt; for your own, run with
both, and your local tweaks win without anyone editing a shared file. Leave
&lt;code&gt;--config&lt;/code&gt; off and you&amp;rsquo;re back on the defaults &lt;code&gt;init&lt;/code&gt; wrote: &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;
plus that machine-wide &lt;code&gt;/etc/mytool/config.yaml&lt;/code&gt; if it&amp;rsquo;s there. Whichever set of
files you land on, environment variables and flags still sit on top.&lt;/p&gt;
&lt;h2 id="the-typo-that-does-nothing"&gt;The typo that does nothing
&lt;/h2&gt;&lt;p&gt;Now for the failure I keep circling. Say you want to change the greeting. Open
your config, but fat-finger the key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&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="nt"&gt;greting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Oi &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# meant to be greeting&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;Run it, and you get a blank line. The greeting you set never applied: the
misspelled key was read, matched nothing, and was silently dropped, and the real
&lt;code&gt;greeting&lt;/code&gt; is now nowhere to be found. Nothing said a word. For a greeting it&amp;rsquo;s a
shrug. For a timeout or a retry count it&amp;rsquo;s the bug you chase at 2am, and I wrote
up the why of it in
&lt;a class="link" href="https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/" &gt;the config key that quietly did nothing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;go-tool-base won&amp;rsquo;t catch this for you by default, and that&amp;rsquo;s a choice rather than
an oversight. There&amp;rsquo;s no central schema that knows every key your tool could ever
take, because keys belong to the commands that use them. What you get instead is
a way to opt a command in, so it validates its own slice and nobody else&amp;rsquo;s.&lt;/p&gt;
&lt;h2 id="making-mistakes-loud"&gt;Making mistakes loud
&lt;/h2&gt;&lt;p&gt;Tell the generator you want validation for a command and it scaffolds exactly
this (&lt;code&gt;gtb generate command --name hello --with-config-validation&lt;/code&gt;). Since
&lt;code&gt;hello&lt;/code&gt; already exists, it&amp;rsquo;s a small file to add by hand. Create
&lt;code&gt;pkg/cmd/hello/config.go&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hello&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/config&amp;#34;&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="c1"&gt;// HelloConfig describes the config keys the hello command consumes.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HelloConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&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;Greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;hello.greeting&amp;#34; validate:&amp;#34;required&amp;#34;`&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;Style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;hello.style&amp;#34; enum:&amp;#34;plain,loud&amp;#34; default:&amp;#34;plain&amp;#34;`&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;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="c1"&gt;// ValidateHelloConfig checks the hello config against its schema.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ValidateHelloConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ValidateStruct&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HelloConfig&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;cfg&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;The tags carry the rules. &lt;code&gt;validate:&amp;quot;required&amp;quot;&lt;/code&gt; means the key has to be present
and non-empty. &lt;code&gt;enum:&amp;quot;plain,loud&amp;quot;&lt;/code&gt; means &lt;code&gt;style&lt;/code&gt; has to be one of those two words.
&lt;code&gt;config.ValidateStruct[HelloConfig]&lt;/code&gt; does the rest: it derives a schema from those
tags and checks the config against it, returning a readable error if anything is
off. It takes &lt;code&gt;props.Config&lt;/code&gt; as it is, the &lt;code&gt;Containable&lt;/code&gt; interface, so there&amp;rsquo;s no
casting to a concrete type. Call it at the top of &lt;code&gt;RunHello&lt;/code&gt;, before you trust any
of the values, and use the style while you&amp;rsquo;re there:&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;RunHello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;HelloOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&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="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;ValidateHelloConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&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="kc"&gt;nil&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&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="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="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;greeting&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="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.greeting&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="k"&gt;if&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="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.style&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&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="s"&gt;&amp;#34;loud&amp;#34;&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;greeting&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="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&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="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="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="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&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="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&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;(You&amp;rsquo;ll add &lt;code&gt;strings&lt;/code&gt; to the imports at the top of &lt;code&gt;main.go&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;Now make a real mistake. Set the style to something that isn&amp;rsquo;t allowed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&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="nt"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Hello&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="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;shout&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;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERRO config validation failed:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; hello.style: value &amp;#34;shout&amp;#34; is not allowed (hint: allowed values: plain, loud)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the difference. The command stops and tells you the key, the bad value,
and what it would have accepted. The same check catches a misspelled
&lt;code&gt;greeting&lt;/code&gt;: the moment the real key goes missing, &lt;code&gt;required&lt;/code&gt; fails with
&lt;code&gt;hello.greeting: required field is missing&lt;/code&gt; instead of quietly running on
nothing. Set &lt;code&gt;style: loud&lt;/code&gt; and you get &lt;code&gt;HELLO&lt;/code&gt;, because the value finally passes
and the code downstream can trust it.&lt;/p&gt;
&lt;p&gt;If you switch on the optional &lt;code&gt;config&lt;/code&gt; feature (it isn&amp;rsquo;t in the default set, so
you opt into it), you also get a ready-made &lt;code&gt;mytool config validate&lt;/code&gt; command that
runs these checks without you wiring anything into a command at all. Either way,
the principle holds: the program already knows what good config looks like, so
make it say so when the config is bad.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;Your &lt;code&gt;hello&lt;/code&gt; command now reads a real setting, ships a sensible default that
&lt;code&gt;init&lt;/code&gt; writes into place, honours overrides from the environment and from layered
files in a predictable order, and refuses to run on a value it doesn&amp;rsquo;t understand.
That&amp;rsquo;s most of what configuration ever needs to be, and you wrote almost none of
the machinery.&lt;/p&gt;
&lt;p&gt;One thing I&amp;rsquo;ve skipped: config can also reload while the tool is running, so a
long-lived process picks up a changed file without a restart. That&amp;rsquo;s its own
capability with its own moving parts, and I pulled it apart in
&lt;a class="link" href="https://blog-570662.gitlab.io/reloading-config-without-a-restart/" &gt;reloading config without a restart&lt;/a&gt;
if you need it.&lt;/p&gt;
&lt;p&gt;Next part, we give the tool something to do with all this config: we turn it into
an AI tool, with a chat command and an MCP server. Until then, go add a couple of
validated settings to your own commands. You&amp;rsquo;ve got the shape of it now.&lt;/p&gt;</description></item><item><title>Reloading config without a restart</title><link>https://blog-570662.gitlab.io/reloading-config-without-a-restart/</link><pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/reloading-config-without-a-restart/</guid><description>&lt;img src="https://blog-570662.gitlab.io/reloading-config-without-a-restart/cover-reloading-config-without-a-restart.png" alt="Featured image of post Reloading config without a restart" /&gt;&lt;p&gt;A config file changes. Someone edits a setting, rotates a credential, flips a feature flag. How does the running process find out? For most processes the answer is blunt: it doesn&amp;rsquo;t, until you restart it. For a short-lived CLI that&amp;rsquo;s completely fine. For a long-running service, &amp;ldquo;just restart it&amp;rdquo; is a much bigger ask than it sounds.&lt;/p&gt;
&lt;h2 id="the-default-answer-is-a-restart"&gt;The default answer is a restart
&lt;/h2&gt;&lt;p&gt;Configuration lives in a file. The file changes: someone edits a setting, rotates a credential, flips a feature flag. How does the running process find out?&lt;/p&gt;
&lt;p&gt;Overwhelmingly, the answer is that it doesn&amp;rsquo;t. A process reads its config once, at startup, and that snapshot is frozen for the life of the process. Change the file and nothing happens until you restart, at which point a fresh process reads the fresh file.&lt;/p&gt;
&lt;p&gt;For a short-lived CLI invocation that&amp;rsquo;s completely fine. It reads config, does its job, exits, and the next invocation reads whatever the file says then. But the same frameworks are also used to build long-running services, and for a service &amp;ldquo;just restart it&amp;rdquo; is not the small thing it sounds like.&lt;/p&gt;
&lt;h2 id="what-a-restart-actually-costs"&gt;What a restart actually costs
&lt;/h2&gt;&lt;p&gt;Restarting a long-running service means every open connection drops. Any in-flight request is lost, or has to be retried by whoever sent it. Caches that took real time to warm are cold again. There&amp;rsquo;s a window, short but real, where the service simply isn&amp;rsquo;t serving.&lt;/p&gt;
&lt;p&gt;If the thing you changed was a log level, or a feature flag, or a timeout, you&amp;rsquo;ve paid a disruption wildly out of proportion to the change. And the calculation only gets worse as the service gets more important, because the services you least want to bounce on a whim are exactly the ones that matter most.&lt;/p&gt;
&lt;h2 id="hot-reload-re-read-in-place"&gt;Hot-reload: re-read in place
&lt;/h2&gt;&lt;p&gt;Hot-reload is the alternative, and both go-tool-base and rust-tool-base support it.&lt;/p&gt;
&lt;p&gt;The process doesn&amp;rsquo;t read config once and freeze it. It &lt;em&gt;watches&lt;/em&gt; the config file. When the file changes, it re-reads it, re-applies it, and carries on running. No new process, no dropped connections, no cold start. The change lands in the live process.&lt;/p&gt;
&lt;p&gt;The shape is the same in both frameworks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A file watcher notices the config file changed. Underneath, this is the operating system&amp;rsquo;s own file-notification facility, &lt;code&gt;inotify&lt;/code&gt; on Linux and its equivalents elsewhere. rust-tool-base reaches it through the &lt;code&gt;notify&lt;/code&gt; crate; go-tool-base, through the watcher built into Viper.&lt;/li&gt;
&lt;li&gt;A debounce step waits for the writes to settle. Saving a file is often several separate operations, and you don&amp;rsquo;t want to reload three times for one edit.&lt;/li&gt;
&lt;li&gt;The config is re-parsed from disk.&lt;/li&gt;
&lt;li&gt;The new config is swapped in atomically.&lt;/li&gt;
&lt;li&gt;Observers are notified, so the subsystems that care can react.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps four and five are the ones worth slowing down on, because they&amp;rsquo;re where a naive hot-reload quietly goes wrong.&lt;/p&gt;
&lt;h2 id="the-two-details-that-make-it-safe"&gt;The two details that make it safe
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The atomic swap.&lt;/strong&gt; You do not mutate the live config object in place. A reader on another thread, partway through reading it, would see a torn mix of old and new values, and that&amp;rsquo;s a genuinely nasty class of bug. Instead the process builds a &lt;em&gt;new&lt;/em&gt;, complete config value and swaps the pointer to it in a single atomic operation. Any reader sees either the entire old config or the entire new one, never a blend. rust-tool-base does this with &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-config/src/watch.rs" target="_blank" rel="noopener"
 &gt;&lt;code&gt;arc-swap&lt;/code&gt;&lt;/a&gt;; go-tool-base does the equivalent. Reads stay cheap and lock-free, and an update is one pointer swap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The observer notification.&lt;/strong&gt; Re-reading the file isn&amp;rsquo;t the end of the job. Some subsystems have to &lt;em&gt;do something&lt;/em&gt; when config changes: a connection pool resizes, a logger changes level, a rate limiter takes a new ceiling. So a hot-reload system has to let those subsystems subscribe. rust-tool-base hands observers a &lt;code&gt;watch::Receiver&lt;/code&gt;, a channel that always holds the latest value; go-tool-base exposes an &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/config/observer.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Observable&lt;/code&gt; interface&lt;/a&gt;. A subsystem subscribes once and reacts every time config changes, for the life of the process.&lt;/p&gt;
&lt;h2 id="where-this-earns-its-keep-a-kubernetes-pod"&gt;Where this earns its keep: a Kubernetes pod
&lt;/h2&gt;&lt;p&gt;Hot-reload is a nicety on a developer&amp;rsquo;s laptop. Inside a Kubernetes pod it becomes genuinely valuable, and the reason is a neat fit between how Kubernetes delivers config and how a file watcher works.&lt;/p&gt;
&lt;p&gt;In Kubernetes you don&amp;rsquo;t usually bake configuration into the container image. It lives in ConfigMap and Secret objects, and the clean way to consume them is to &lt;em&gt;mount them as volumes&lt;/em&gt;. Mount a ConfigMap as a volume and each key becomes a file in the pod&amp;rsquo;s filesystem.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part that connects to everything above. When you update that ConfigMap or Secret, Kubernetes does not restart your pod. The kubelet notices the object changed and rewrites the projected files inside the still-running pod. The files on disk change underneath a process that never stopped.&lt;/p&gt;
&lt;p&gt;That file rewrite is exactly the event a hot-reload watcher exists to catch. So the whole chain becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You &lt;code&gt;kubectl apply&lt;/code&gt; an updated ConfigMap, or rotate a Secret.&lt;/li&gt;
&lt;li&gt;The kubelet updates the projected files inside the pod.&lt;/li&gt;
&lt;li&gt;The framework&amp;rsquo;s file watcher sees the write.&lt;/li&gt;
&lt;li&gt;The config is re-parsed, swapped in atomically, and observers are notified.&lt;/li&gt;
&lt;li&gt;The new configuration is live, and the pod never cycled.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;ve changed a running service, in a running pod, with no rollout, nothing terminated and recreated, no dropped traffic. Rotate a database credential, raise a log level to debug an incident in progress, flip a feature flag: all of it live. For a service where a restart is the very thing you&amp;rsquo;re trying hard to avoid, the kind of &lt;a class="link" href="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/" &gt;long-running service&lt;/a&gt; these frameworks are built for, that&amp;rsquo;s the difference between a config change being routine and being an event.&lt;/p&gt;
&lt;h2 id="the-caveats"&gt;The caveats
&lt;/h2&gt;&lt;p&gt;Two things, so this doesn&amp;rsquo;t read as magic.&lt;/p&gt;
&lt;p&gt;First, not everything can be hot-reloaded. Some configuration genuinely needs a restart: the port a server binds to, the size of a thread pool, anything wired up exactly once at process start. Hot-reload covers the large category of settings a subsystem can re-read and re-apply; it doesn&amp;rsquo;t abolish restarts. A config system worth its salt is clear about which settings are live and which are not.&lt;/p&gt;
&lt;p&gt;Second, a Kubernetes gotcha that catches people out. The in-place file update happens for ConfigMaps and Secrets mounted as &lt;em&gt;volumes&lt;/em&gt;. Consume the same ConfigMap as &lt;em&gt;environment variables&lt;/em&gt; instead, and those are fixed when the container starts and never update, short of a restart. If you want hot-reload in a pod, mount config and secrets as files, not env vars. And even with volumes the update isn&amp;rsquo;t instant: the kubelet syncs on a period, around a minute by default, so a reload is &amp;ldquo;within a minute or so&amp;rdquo;, not &amp;ldquo;the moment you hit apply&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;A config file changes, and the default way to pick it up is to restart the process. For a long-running service that restart costs dropped connections, lost work and a cold start, often for a change as small as a log level.&lt;/p&gt;
&lt;p&gt;go-tool-base and rust-tool-base both support hot-reload instead: a file watcher catches the change, the config is re-parsed and swapped in atomically so no reader sees torn state, and observers are notified so subsystems can react, all in a live process. The setting where it pays off most is a Kubernetes pod, where ConfigMaps and Secrets mounted as volumes are rewritten in place by the kubelet and the watcher catches that write directly. Mount them as volumes rather than env vars, allow for the kubelet&amp;rsquo;s sync delay, accept that some settings still need a restart, and within those limits &amp;ldquo;the config changed&amp;rdquo; stops meaning &amp;ldquo;cycle the pod&amp;rdquo;.&lt;/p&gt;</description></item><item><title>The config key that quietly did nothing</title><link>https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/cover-the-config-key-that-quietly-did-nothing.png" alt="Featured image of post The config key that quietly did nothing" /&gt;&lt;p&gt;I once spent the better part of an hour convinced a timeout setting was broken. I&amp;rsquo;d set it in the config file, the tool ignored it, and the code that read it looked perfectly correct. The setting was &lt;code&gt;tiemout&lt;/code&gt;. I&amp;rsquo;d typed it wrong, and not one thing in the entire stack had thought that worth mentioning.&lt;/p&gt;
&lt;h2 id="config-loaders-are-too-polite"&gt;Config loaders are too polite
&lt;/h2&gt;&lt;p&gt;Most config loaders have the same agreeable flaw: they&amp;rsquo;ll read whatever&amp;rsquo;s in the file and quietly ignore anything they weren&amp;rsquo;t expecting. Put a key the tool doesn&amp;rsquo;t know about and it sails straight past. No error, no warning, nothing. The loader assumes you meant it, or assumes some other layer will care, and neither turns out to be true.&lt;/p&gt;
&lt;p&gt;That politeness costs you in two directions. A key you misspelled is silently dropped, so the setting you thought you&amp;rsquo;d changed keeps running on its old value. And a key you &lt;em&gt;forgot&lt;/em&gt; leaves the field at its zero value, which you then discover at runtime, usually at the least convenient moment, when something downstream divides by a timeout of zero. The file looked fine. It parsed fine. It was just quietly wrong, and nothing was watching for that.&lt;/p&gt;
&lt;h2 id="the-struct-already-knows-the-answer"&gt;The struct already knows the answer
&lt;/h2&gt;&lt;p&gt;The thing is, the program already has a complete description of what valid config looks like. It&amp;rsquo;s the struct you unmarshal into. The field names, the types, which ones matter. That description exists; it&amp;rsquo;s just not being used to &lt;em&gt;check&lt;/em&gt; anything.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s config package puts it to work. You hand it a tagged struct and it derives a schema from the tags, in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/config/schema.go#L48" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/config/schema.go&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// WithStructSchema derives a schema from a tagged Go struct.&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="c1"&gt;// Supported tags: `config:&amp;#34;key&amp;#34; validate:&amp;#34;required&amp;#34; enum:&amp;#34;a,b,c&amp;#34; default:&amp;#34;value&amp;#34;`.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;WithStructSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SchemaOption&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="o"&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So a feature&amp;rsquo;s config type carries its own rules inline:&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;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ServerConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&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;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;host&amp;#34; validate:&amp;#34;required&amp;#34;`&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;Port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;port&amp;#34; validate:&amp;#34;required&amp;#34;`&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;LogMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;log_mode&amp;#34; enum:&amp;#34;text,json&amp;#34;`&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;There&amp;rsquo;s no second artefact to keep in sync, which is the same instinct go-tool-base leans on for &lt;a class="link" href="https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/" &gt;structured AI output&lt;/a&gt;: the type is the schema, and the schema is a projection of the type, so the two can&amp;rsquo;t drift apart because there&amp;rsquo;s only one of them. Each package describes its own slice of config on its own struct, and &lt;code&gt;NewSchema&lt;/code&gt; composes them into the schema the loaded config gets checked against.&lt;/p&gt;
&lt;h2 id="strict-mode-turns-the-typo-into-an-error"&gt;Strict mode turns the typo into an error
&lt;/h2&gt;&lt;p&gt;Deriving the schema is half of it. The half that actually catches &lt;code&gt;tiemout&lt;/code&gt; is this one, also from &lt;code&gt;schema.go&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// WithStrictMode treats unknown keys as errors instead of warnings.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;WithStrictMode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SchemaOption&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="o"&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By default a key the schema doesn&amp;rsquo;t recognise is a &lt;em&gt;warning&lt;/em&gt;: surfaced, but not fatal, which is the right call when a config file might legitimately carry extra keys for tools other than yours. Turn on strict mode and an unknown key becomes an &lt;em&gt;error&lt;/em&gt;. &lt;code&gt;tiemout&lt;/code&gt; isn&amp;rsquo;t in the schema, so the tool refuses to start and tells me which key it didn&amp;rsquo;t recognise, instead of shrugging and using the default for an hour while I lose my mind. The validator walks every key actually present in the file and checks it against the known set, so a typo has nowhere to hide.&lt;/p&gt;
&lt;h2 id="what-it-deliberately-doesnt-do"&gt;What it deliberately doesn&amp;rsquo;t do
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one decision in here I think is worth calling out, because the obvious feature is conspicuously absent. The schema knows each field&amp;rsquo;s default value. It would be the easiest thing in the world to have validation &lt;em&gt;fill in&lt;/em&gt; missing fields from those defaults.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t, on purpose. Validation validates. It tells you what&amp;rsquo;s wrong and what to do about it, and it stops there. Defaults are a separate job, handled by the &lt;a class="link" href="https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/" &gt;embedded default config that every feature ships&lt;/a&gt; and merges in before validation ever runs. Keeping the two apart means the validator has exactly one responsibility, and the defaults live in one place rather than being half in an embedded file and half injected by a check. A field&amp;rsquo;s &lt;code&gt;default&lt;/code&gt; tag is there for the documentation and the error hint, not as a sneaky second source of values.&lt;/p&gt;
&lt;h2 id="errors-you-can-act-on"&gt;Errors you can act on
&lt;/h2&gt;&lt;p&gt;The output isn&amp;rsquo;t a bare boolean. Validation returns a result that separates the fatal from the advisory: the missing required field and the wrong type are errors that stop the tool; the unrecognised-but-harmless key is a warning that informs you without blocking. And because each problem carries the offending key by name and a hint about the fix, the message tells you what to change, in the spirit of &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;errors that tell you what to do next&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;A config loader that silently ignores keys it doesn&amp;rsquo;t recognise will, sooner or later, ignore one you meant. go-tool-base derives a validation schema straight from your tagged config struct, so there&amp;rsquo;s no separate schema to maintain, and strict mode promotes an unknown key from a quiet shrug to a real error that names the typo. It validates without injecting defaults, because defaults are the embedded config&amp;rsquo;s job and a validator with one responsibility is easier to trust. Set &lt;code&gt;tiemout&lt;/code&gt; now and the tool tells you, which is roughly fifty-nine minutes sooner than I found out.&lt;/p&gt;</description></item></channel></rss>