<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Debugging on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/debugging/</link><description>Recent content in Debugging on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Wed, 24 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/debugging/index.xml" rel="self" type="application/rss+xml"/><item><title>The cobra hook I was sure I'd enabled</title><link>https://blog-570662.gitlab.io/the-cobra-hook-i-was-sure-id-enabled/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-cobra-hook-i-was-sure-id-enabled/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-cobra-hook-i-was-sure-id-enabled/cover-the-cobra-hook-i-was-sure-id-enabled.png" alt="Featured image of post The cobra hook I was sure I'd enabled" /&gt;&lt;p&gt;It came out of an audit. I&amp;rsquo;d recently pointed a small army of review agents at the
whole go-tool-base codebase, back
&lt;a class="link" href="https://blog-570662.gitlab.io/they-switched-it-off-while-it-was-fixing-my-code/" &gt;before that became a political problem&lt;/a&gt;,
and one of the findings was that a subcommand could quietly skip the framework&amp;rsquo;s own
start-up code. My first reaction was the dangerous one: surely not&amp;hellip; we switched that
on ages ago. So I asked for a second pair of eyes on the exact line.&lt;/p&gt;
&lt;p&gt;There was no line. I was certain I&amp;rsquo;d enabled it, but I had simply never done it.&lt;/p&gt;
&lt;h2 id="what-the-start-up-hook-is-for"&gt;What the start-up hook is for
&lt;/h2&gt;&lt;p&gt;go-tool-base is built on &lt;a class="link" href="https://github.com/spf13/cobra" target="_blank" rel="noopener"
 &gt;cobra&lt;/a&gt;, the library most
Go command-line tools are built on. In cobra, a command can carry a
&lt;code&gt;PersistentPreRunE&lt;/code&gt;: a function that runs &lt;em&gt;before&lt;/em&gt; the command itself, and that, the
name strongly implies, persists down to the command&amp;rsquo;s children. Think of it as the
&amp;ldquo;before you do anything, get the tool ready&amp;rdquo; step.&lt;/p&gt;
&lt;p&gt;go-tool-base uses exactly one of them, on the root command, to do all the
unglamorous setup: load and merge configuration, set up logging, ask about telemetry
the first time you run, wire up the telemetry collector, and check whether there&amp;rsquo;s a
newer release to install. Everything the tool does afterwards leans on that having
happened. By the time your actual command runs, &lt;code&gt;props.Config&lt;/code&gt; is meant to be
populated and the collector is meant to exist.&lt;/p&gt;
&lt;p&gt;The reasonable assumption (the one I&amp;rsquo;d made, anyway) is that &amp;ldquo;persistent&amp;rdquo; means it cascades.
Define it once at the root and every &lt;code&gt;mytool foo bar&lt;/code&gt; three levels down gets it for
free.&lt;/p&gt;
&lt;h2 id="persistent-promises-less-than-it-says"&gt;&amp;ldquo;Persistent&amp;rdquo; promises less than it says
&lt;/h2&gt;&lt;p&gt;Here is the catch, and it is a good one to file away if you ever build a command
tree. cobra runs only the &lt;em&gt;nearest&lt;/em&gt; &lt;code&gt;PersistentPreRunE&lt;/code&gt; it finds, walking up from
the command you actually invoked. If a subcommand defines its own, that one runs and
the root&amp;rsquo;s does not. Not as well as. Instead of. There&amp;rsquo;s no warning and no error; the
child&amp;rsquo;s hook simply wins, and the parent&amp;rsquo;s is passed over in silence.&lt;/p&gt;
&lt;p&gt;So the moment any command below the root declared its own &lt;code&gt;PersistentPreRunE&lt;/code&gt;, the
entire start-up for that branch, the config, the logging, the telemetry, the update
check, would just not happen. &lt;code&gt;props.Config&lt;/code&gt; would be nil. The collector would be
nil. The first you&amp;rsquo;d hear of it is a nil-pointer panic a long way from the cause, or,
worse, no panic at all and a tool running happily unconfigured.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EnableTraverseRunHooks&lt;/code&gt; is cobra&amp;rsquo;s opt-in to the behaviour most people assume is
already the default: run every &lt;code&gt;PersistentPreRunE&lt;/code&gt; from the root down to the leaf, in
order. I&amp;rsquo;d assumed it was the default. It is not, and I&amp;rsquo;d never turned it on.&lt;/p&gt;
&lt;h2 id="a-landmine-nobody-had-stepped-on"&gt;A landmine nobody had stepped on
&lt;/h2&gt;&lt;p&gt;The saving grace was that nothing was actually broken yet. In go-tool-base&amp;rsquo;s own
command tree, the root is the only command that defines a persistent pre-run, so
&amp;ldquo;root to leaf&amp;rdquo; and &amp;ldquo;nearest only&amp;rdquo; happen to produce the identical result. The flag
being off changed nothing I could observe.&lt;/p&gt;
&lt;p&gt;The bug was latent. It was a trap laid for the first person to do something entirely
reasonable: add a &lt;code&gt;PersistentPreRunE&lt;/code&gt; to one of &lt;em&gt;their own&lt;/em&gt; subcommands. go-tool-base
is a framework other tools are built on, so that person was never going to be me. The
instant a downstream author did the obvious thing, their config and telemetry would
vanish for that branch of their tool and nothing would tell them why.&lt;/p&gt;
&lt;p&gt;That is the kind of bug I least like shipping. It compiled. It passed the tests. It
would have demoed perfectly. And it sat there waiting to hand a stranger a debugging
session with no breadcrumbs, for the crime of using a standard cobra feature the
obvious way.&lt;/p&gt;
&lt;h2 id="one-line-and-a-note-for-whoevers-next"&gt;One line, and a note for whoever&amp;rsquo;s next
&lt;/h2&gt;&lt;p&gt;The fix is the line I was so sure already existed
(&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/26eb355/pkg/cmd/root/root.go#L351-359" target="_blank" rel="noopener"
 &gt;root.go&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;// Run every parent PersistentPreRunE from root to leaf rather than only the&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;// closest one. Without this, a downstream subcommand that defines its own&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;// PersistentPreRunE silently shadows the root bootstrap (config load,&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;// telemetry, update check) for that subtree. With it set, the framework&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;// bootstrap always runs first and a child hook runs after it.&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;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EnableTraverseRunHooks&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="kc"&gt;true&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;With it set, cobra runs the root start-up first and then the child&amp;rsquo;s hook, in order,
so a downstream command &lt;em&gt;adds to&lt;/em&gt; the setup instead of replacing it.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want to stop there, because the next author to add a child hook still
deserves to understand the ordering. So the change also drops a one-time debug line if
it spots any command in the tree carrying its own &lt;code&gt;PersistentPreRunE&lt;/code&gt;
(&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/26eb355/pkg/cmd/root/root.go#L397-406" target="_blank" rel="noopener"
 &gt;the same file&lt;/a&gt;),
saying out loud what&amp;rsquo;s going on:&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a downstream command defines its own PersistentPreRunE; &amp;#34;&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&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="s"&gt;&amp;#34;it runs AFTER the framework bootstrap (config load, telemetry, update check), not instead of it&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;p&gt;And, belt and braces, the collector now defaults to a no-op so the few paths that do
legitimately return early, like &lt;code&gt;init&lt;/code&gt; and &lt;code&gt;help&lt;/code&gt;, still satisfy the &amp;ldquo;always
non-nil&amp;rdquo; promise the rest of the code relies on. The whole thing shipped with a pair
of regression tests that assert the bootstrap really does run when a child hook is
present, and that it runs first. It&amp;rsquo;s all written up in a short
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/26eb355/docs/development/specs/2026-06-12-bootstrap-prerun-traversal.md" target="_blank" rel="noopener"
 &gt;spec&lt;/a&gt;
and landed in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/commit/26eb355" target="_blank" rel="noopener"
 &gt;one commit&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="trust-but-grep"&gt;Trust, but grep
&lt;/h2&gt;&lt;p&gt;There are two things worth taking away. The cobra one is portable: if you rely on
&lt;code&gt;PersistentPreRunE&lt;/code&gt; cascading down a command tree, set &lt;code&gt;EnableTraverseRunHooks&lt;/code&gt;,
because &amp;ldquo;persistent&amp;rdquo; means less than it sounds and the nearest hook wins by default.&lt;/p&gt;
&lt;p&gt;The other is the one I keep having to relearn. The settings I&amp;rsquo;m most certain about
are the ones I never check, precisely because the certainty is what stops me looking.
Somewhere along the line I&amp;rsquo;d promoted &amp;ldquo;I meant to&amp;rdquo; straight to &amp;ldquo;I did&amp;rdquo;, with nothing
in between&amp;hellip; and then defended it out loud before I&amp;rsquo;d even gone to look. A review agent is good
at exactly that blind spot: it has no memory of intending to do something, only the
code in front of it. The best thing the audit turned up wasn&amp;rsquo;t a clever bug. It was a
flag that was never there.
&lt;a class="link" href="https://blog-570662.gitlab.io/the-campsite-was-never-the-point/" &gt;Leaving the campsite better than you found it&lt;/a&gt;
has to include the traps nobody&amp;rsquo;s stepped on yet.&lt;/p&gt;</description></item></channel></rss>