<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matt Cockayne</title>
    <description>The latest articles on DEV Community by Matt Cockayne (@phpboyscout).</description>
    <link>https://dev.to/phpboyscout</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F284848%2Feccd93ba-1bff-4e17-85cb-ff4c4bac6c6d.png</url>
      <title>DEV Community: Matt Cockayne</title>
      <link>https://dev.to/phpboyscout</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phpboyscout"/>
    <language>en</language>
    <item>
      <title>A terrible lead to an AI junior</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Sun, 20 Sep 2026 01:41:09 +0000</pubDate>
      <link>https://dev.to/phpboyscout/a-terrible-lead-to-an-ai-junior-31fa</link>
      <guid>https://dev.to/phpboyscout/a-terrible-lead-to-an-ai-junior-31fa</guid>
      <description>&lt;p&gt;I was shouting at my screen. Out loud, in an empty room, at a text editor (I really was).&lt;/p&gt;

&lt;p&gt;Three goes in, and the thing still couldn't put a caption where I'd dragged it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I have built this exact thing before
&lt;/h2&gt;

&lt;p&gt;The feature is easy to describe: grab a caption in &lt;a href="https://keryx.phpboyscout.uk" rel="noopener noreferrer"&gt;keryx&lt;/a&gt;'s studio, drag it where you want it on the panel, and have the rendered MP4 agree with you. A WYSIWYG editor sat over a video renderer. And I have done it before, properly, by hand, a very long time ago.&lt;/p&gt;

&lt;p&gt;I was at an agency that had been brought in to build a greetings card website. Our client was an upstart with ambitions, and the behemoths they were trying to topple were Moonpig and Funky Pigeon. You picked a card, you dragged your text and your photo about on a preview, and what came out the other end had to be the thing you'd actually see when it dropped through someone's letterbox.&lt;/p&gt;

&lt;p&gt;They came in the door and asked for it in Flash, like the others had. And I, a young whippersnapper absolutely brimming with confidence, said &lt;em&gt;no&lt;/em&gt;. Flash was legacy, it was turning into a security problem, and everything they were describing was perfectly possible in JavaScript, because all of it, every last bit of it, is just maths!&lt;/p&gt;

&lt;p&gt;This was long before AI was any use to anybody (and back when jQuery was still a reasonable thing to reach for), so it was hand-crafted, every line. And it was pixel perfect: every element positioned on that preview landed just where it should on the final render. Took me a couple of days (and I've clearly dined out on it since).&lt;/p&gt;

&lt;h2&gt;
  
  
  So why was this taking three goes?
&lt;/h2&gt;

&lt;p&gt;That's why I was shouting. I knew how this worked, I'd known for close to twenty years: absolute positioning and a scaling ratio. It isn't rocket science, and I'd done it under deadline with worse tooling and a lot less experience. An AI ought to knock that out in minutes.&lt;/p&gt;

&lt;p&gt;Instead, three rounds. Each time I'd render a real reel, watch it back, and write down what was still off, and each time the thing that got fixed stayed fixed while something new stepped up to take its place.&lt;/p&gt;

&lt;p&gt;First the text boxes jumped and resized while I dragged them. Fixed. Then the dragging went smooth, and the font size was perfect in the preview and had no fidelity whatsoever to the render. Fixed that too... or thought I had. Then the alignment came out right vertically and still wrong horizontally.&lt;/p&gt;

&lt;p&gt;That last one is the tell. Vertical correct, horizontal wrong, out of the same code doing the same arithmetic on a different axis. That's not a maths error. Maths errors don't pick a favourite axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  It was measuring. It should have been reading.
&lt;/h2&gt;

&lt;p&gt;The ratio has two inputs, and it was &lt;em&gt;measuring both of them&lt;/em&gt;! The preview size came from asking the browser how big the element had ended up, and the render size came from probing the encoded video file. Two measurements, both perfectly sensible on their own, both taken from things sitting downstream of the actual answer.&lt;/p&gt;

&lt;p&gt;And that video wasn't even the render, it was a scaled-down proxy made for the preview, so probing it doesn't give you the target dimensions at all, it gives you the proxy's plus whatever the encoder decided about even numbers. Ask a browser for an element's size, meanwhile, and you get sub-pixel layout, device pixel ratio, and a rounding decision nobody wrote down. Second-hand, both of them, a copy of a copy, with very careful arithmetic being done on the result.&lt;/p&gt;

&lt;p&gt;Meanwhile the real number, the actual target handed to the encoder, was sat in the codebase as a constant. Seven lines into &lt;code&gt;internal/reel/timing.go&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Geometry / timing defaults (parity with gen-reel.py).&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Width&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1080&lt;/span&gt;
    &lt;span class="n"&gt;Height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1920&lt;/span&gt;
    &lt;span class="n"&gt;FPS&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1080 by 1920. Defined by us, passed to the encoder by us, and never once consulted by the code trying to work out how big the render was going to be. It also explains the axis thing, since two dimensions pick up different rounding in different places, so of course one lands while the other misses.&lt;/p&gt;

&lt;p&gt;We found it, we fixed it, and it's been perfect since. And the principle underneath is worth having: when a value is defined by your own code, don't go re-deriving it by measuring something downstream... read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I should have asked three rounds earlier
&lt;/h2&gt;

&lt;p&gt;If I'd already solved this, by hand, more than once, why was I letting a machine solve it at all?&lt;/p&gt;

&lt;p&gt;Laziness, mostly, and I'll own that one. It looked trivial &lt;em&gt;to me&lt;/em&gt; because I'd done it several times before, so it registered as boilerplate, and boilerplate gets handed off without a second thought. That's the mistake, and it isn't really a technical one: I gave it a list of requirements and let it get on with it.&lt;/p&gt;

&lt;p&gt;No technical guidance, no "the target dimensions are a constant, they're in &lt;code&gt;timing.go&lt;/code&gt;, use those and don't measure anything", nothing about the solution I already knew worked, because to me it wasn't a solution, it was just how you do it.&lt;/p&gt;

&lt;p&gt;I was a terrible lead to a junior engineer!&lt;/p&gt;

&lt;p&gt;Because that's what happened. I briefed a capable junior on outcomes, gave it nothing on approach, and then got annoyed when it took the reasonable-looking path instead of the correct one. It didn't have my twenty-eight years to draw on. It had my requirements.&lt;/p&gt;

&lt;p&gt;The maddening part is that the thing I skipped is the thing I'd have given a human without a moment's thought. You don't hand a junior a ticket that says "make the preview match the render" and walk off, not if you've solved it before and know where the sharp edge is. You tell them about the sharp edge. That's most of what leading is.&lt;/p&gt;

&lt;p&gt;I did it in a couple of days once, with jQuery, on a deadline, while arguing a client out of Flash, all of which taught me absolutely nothing about how to explain it to someone else. Maybe if Musk gets his way with Neuralink I'll be able to just think the context at it. Until then I need to be a better lead engineer for my junior.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/a-terrible-lead-to-an-ai-junior/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 20 September 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>agents</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Fifty-four tools before a word was typed</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Fri, 18 Sep 2026 11:31:31 +0000</pubDate>
      <link>https://dev.to/phpboyscout/fifty-four-tools-before-a-word-was-typed-d9p</link>
      <guid>https://dev.to/phpboyscout/fifty-four-tools-before-a-word-was-typed-d9p</guid>
      <description>&lt;p&gt;gtb, the command-line tool that sits in the middle of my Go framework, has fifty-four commands. Until this week, every conversation with an assistant that had gtb attached opened by handing over all fifty-four of them, schemas included, before anyone had typed a word. About 112 KB of JSON, sat in the context of a chat whose first question was probably "what version is this".&lt;/p&gt;

&lt;p&gt;And I'd written a post, back in March, calling that a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The door I'd framed, and what was behind it
&lt;/h2&gt;

&lt;p&gt;I still stand by most of &lt;a href="https://phpboyscout.uk/your-cli-is-already-an-ai-tool/" rel="noopener noreferrer"&gt;that post&lt;/a&gt;. A well-built CLI is already a structured description of a set of capabilities, and the Model Context Protocol is the translator that lets an assistant read it, and go-tool-base put that translator in the framework so each tool got it for free, and all of that still holds.&lt;/p&gt;

&lt;p&gt;What I hadn't clocked is where the bill lands. A tool with eighty commands ships eighty schemas into each conversation, most of them never used, and the tool doesn't pay for any of that. The model does, in context it can no longer spend on the actual problem, and so does the person sat in front of it, in an assistant that gets a little vaguer with every command you add. I'd built a floodlight, pointed it at the whole yard, and called the glare "discoverability".&lt;/p&gt;

&lt;p&gt;The spike that measured it is on the &lt;a href="https://gitlab.com/phpboyscout/go/mcp/-/wikis/reports/2026-09-10-mcp-feasibility-spike" rel="noopener noreferrer"&gt;project wiki&lt;/a&gt;, and the numbers aren't subtle. Ten commands published one-tool-each cost a client 16.7 KB of descriptors, a hundred cost 167 KB, and a thousand cost 1.67 MB.&lt;/p&gt;

&lt;p&gt;That's a novel, delivered with each connection!&lt;/p&gt;

&lt;p&gt;The three-tool facade that replaces them cost 691 bytes at every size, because it is the same three tools whether the catalogue has ten operations in it or a thousand, and that one row of the table is the whole argument for the module.&lt;/p&gt;

&lt;p&gt;So, &lt;a href="https://gitlab.com/phpboyscout/go/mcp" rel="noopener noreferrer"&gt;&lt;strong&gt;go/mcp&lt;/strong&gt;&lt;/a&gt; is public, &lt;code&gt;v0.2.0&lt;/code&gt; is out (&lt;code&gt;v0.1.0&lt;/code&gt; lasted a day), and it carries a lantern instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three tools, however big the catalogue
&lt;/h2&gt;

&lt;p&gt;Let me back up a step, because the fix only makes sense once you see what a "tool" is from the model's side of the table. When an assistant connects to something over MCP, the first thing it does is ask for the list of tools on offer, and each entry in that list carries a name, a description of what it does, and a schema saying what arguments it takes. That list gets loaded into the conversation, and it stays there for the whole chat.&lt;/p&gt;

&lt;p&gt;So one tool per command means the model is reading gtb's entire manual before you've asked it anything, and every message after that is written with the manual still open on the desk.&lt;/p&gt;

&lt;p&gt;Now, by default a server built on go/mcp hands over three tools instead: &lt;code&gt;search_tools&lt;/code&gt;, &lt;code&gt;get_tool_details&lt;/code&gt; and &lt;code&gt;call_tool&lt;/code&gt;. The model asks for what it's after (or browses, with an empty search), gets back a short list of matches with a line each, picks one, asks for that one's details, and then calls it. Only at that third step does a schema turn up in the conversation, and only the one it's about to use. The catalogue underneath can be as big as the tool wants to be, and what the model is carrying stays three descriptions long.&lt;/p&gt;

&lt;p&gt;The trick is that nothing in the protocol knows this is happening. To the client, &lt;code&gt;call_tool&lt;/code&gt; is an ordinary tool that happens to take a name and a bag of arguments. The searching and inspecting is a convention the server and the model agree on between themselves, through nothing more than the tool descriptions, and the protocol is none the wiser. So it works with any client that speaks MCP today, no client-side support needed and nothing to wait for!&lt;/p&gt;

&lt;p&gt;Search is bounded too, so it can't hand the whole catalogue back a page at a time... five results a page by default, twenty at most, a 256-byte query, and page cursors that are encrypted, tied to the caller and expire after ten minutes (starting numbers from the spec, tuned against a made-up catalogue of a thousand operations rather than anything real, and I suspect they'll move).&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade, and the floodlight is still in the cupboard
&lt;/h2&gt;

&lt;p&gt;A lantern shows you the next step and hides the rest of the yard, and there's a cost to that, so let me walk you through it.&lt;/p&gt;

&lt;p&gt;Some clients, Claude Desktop among them, will stop and ask you "are you sure?" before running a tool that might do damage. They know which ones might, because a tool can carry a few little labels on it (MCP calls them annotations: &lt;em&gt;this one only reads&lt;/em&gt;, &lt;em&gt;this one is destructive&lt;/em&gt;, &lt;em&gt;this one talks to the outside world&lt;/em&gt;), and the client reads those labels before it decides whether to ask. That's a good mechanism, and it's one worth keeping.&lt;/p&gt;

&lt;p&gt;Here's the catch. That client asks about the tool it can see, and with three tools on the table the only one it ever runs is &lt;code&gt;call_tool&lt;/code&gt;. So &lt;code&gt;call_tool&lt;/code&gt; has to wear the cautious labels (destructive, talks to the outside world, the lot), and every operation behind it gets the same "are you sure?" whether it deserves one or not. The per-command labels your application declared still exist. The model can read them when it asks for a tool's details, and the server can read them when deciding what to allow. The client's own prompt, though, can't, because it's looking at &lt;code&gt;call_tool&lt;/code&gt; and &lt;code&gt;call_tool&lt;/code&gt; is all it sees.&lt;/p&gt;

&lt;p&gt;If your users lean on that prompt, compact mode has taken something from them.&lt;/p&gt;

&lt;p&gt;So the floodlight is still in the cupboard, one option away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Direct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct mode is the old shape: every operation published as its own tool, with its own schema and its own labels, and the client's prompt works exactly as it did before. Same registry underneath, same rules about who can run what, same results coming back. The only thing that changes is how much the model has to carry. You choose it when you build the server, and it is never guessed from how a client behaves, so a host that wants both runs two servers on two endpoints.&lt;/p&gt;

&lt;p&gt;(I'd stay compact, but then I would.)&lt;/p&gt;

&lt;h2&gt;
  
  
  It runs the command the way you would have
&lt;/h2&gt;

&lt;p&gt;The next question, once a model has picked a command, is how it actually gets run, and this is where I kept an old decision instead of replacing it.&lt;/p&gt;

&lt;p&gt;go-tool-base had been serving MCP through &lt;a href="https://github.com/spechtlabs/ophis" rel="noopener noreferrer"&gt;ophis&lt;/a&gt; since its first commit, and ophis ran every call as a separate child process rather than calling the command inside the server. That looked like overhead until I thought about what a Cobra command tree actually is: a big lump of shared, mutable state, with every flag bound to a variable when the tree is built. Two calls to the same command in one process are two callers writing the same variables, and no amount of being careful makes that safe.&lt;/p&gt;

&lt;p&gt;So the Cobra binding in go/mcp never calls a command's &lt;code&gt;RunE&lt;/code&gt; in the server's own process either. Each call starts the bound binary again as a subprocess, with the command path, one &lt;code&gt;--name=value&lt;/code&gt; token per flag, a &lt;code&gt;--&lt;/code&gt; separator, then the positional arguments, so an argument that looks like a flag stays an argument and nothing passes through a shell on the way. The command's own pre-runs, config loading and middleware run as they would from a terminal, which means what the model gets back is what you'd have got.&lt;/p&gt;

&lt;p&gt;It's kept on a short lead, too. One command runs at a time per binding (a second call fails straight away with a retryable &lt;code&gt;busy&lt;/code&gt; rather than queueing, because a call that runs later than the caller expected has no way to say so), it gets five minutes, a megabyte of retained output across both streams, and a five-second cleanup budget, and searching and inspecting never take the slot, so the lantern stays lit while a command runs.&lt;/p&gt;

&lt;p&gt;A cancelled command isn't just killed, either. It was started inside a process group (a job object on Windows), so a &lt;code&gt;sleep 600&lt;/code&gt; a script happened to spawn goes with it, and cleanup polls until the group reports no live process.&lt;/p&gt;

&lt;p&gt;Ophis got the isolation right first, and I'm grateful for it. What it couldn't give me was the discovery model, a registry a service could share with a CLI, or the rules about who can run what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search is not a gate
&lt;/h2&gt;

&lt;p&gt;Those rules are the last piece, and the one thing from the docs I'd want you to carry around.&lt;/p&gt;

&lt;p&gt;When you build a server on go/mcp you hand it a policy, which is your answer to "may this caller run this operation, with these arguments?". The server asks that question three times: when a caller searches, when a caller asks for a tool's details, and again when a caller invokes it, with the validated arguments in hand, even if the details were fetched a moment ago. Nothing you learn by asking grants you anything, and neither does being in the catalogue in the first place.&lt;/p&gt;

&lt;p&gt;And a name a caller isn't allowed to run gets the same answer as a name that doesn't exist, so there's no probing the catalogue by guessing.&lt;/p&gt;

&lt;p&gt;Paranoid, kinda, for a CLI on your own laptop, and mostly it is... right up until the same registry is mounted inside a service and the caller is someone you've never met.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it mounts
&lt;/h2&gt;

&lt;p&gt;That leaves the three places it lives, and for a Cobra CLI it's one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpcli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpcli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpcobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithExposure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isExposed&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives your tool an &lt;code&gt;mcp&lt;/code&gt; command. &lt;code&gt;my-tool mcp start&lt;/code&gt; serves every exposed command over stdio, which is how an editor talks to it; &lt;code&gt;mcp stream&lt;/code&gt; serves the same thing over HTTP; &lt;code&gt;mcp tools&lt;/code&gt; exports the catalogue as JSON so you can see what a model would; and &lt;code&gt;mcp claude&lt;/code&gt;, &lt;code&gt;mcp cursor&lt;/code&gt; and &lt;code&gt;mcp vscode&lt;/code&gt; write the editor's config entry for you (and remove it, and list it) without touching anything else in that file. The spellings are ophis's on purpose, so a script or an editor entry written for it keeps working.&lt;/p&gt;

&lt;p&gt;For an HTTP service the shape is different but the pieces are the same. You register the operations you want to expose explicitly, and you get back one &lt;code&gt;http.Handler&lt;/code&gt; to mount beside your own routes, behind the middleware you already run and under the lifecycle you already have. Your body limit applies to it, a request from a website you didn't list is refused before the protocol sees it, a client disconnecting mid-call cancels the operation, and shutdown waits for what's in flight.&lt;/p&gt;

&lt;p&gt;And for a gRPC service, which is what &lt;code&gt;v0.2.0&lt;/code&gt; added this morning, you bind each unary method as an operation with &lt;a href="https://mcp.go.phpboyscout.uk/how-to/expose-a-grpc-service/" rel="noopener noreferrer"&gt;&lt;code&gt;mcpgrpc.Unary&lt;/code&gt;&lt;/a&gt;, naming it by its generated full-method constant and handing over the generated server method itself. A call runs in-process rather than dialling the service's own listener, but it runs through the service's own interceptor chain, with the identity your HTTP middleware verified established on the context the way the gRPC auth interceptor would have, the same authorisation predicate applied, the call's deadline and trace carried in. Arguments and results are the request and response messages in protojson, and the schemas a model sees are derived from the message descriptors, so there's no JSON Schema to write by hand. Streaming methods are refused at registration. Unary only, for now.&lt;/p&gt;

&lt;p&gt;The reason one registry can serve all three is that the root package imports none of it: no protocol SDK, no Cobra, no HTTP, no gRPC, no lifecycle framework, with a test that keeps it so. A CLI and a service share the operation model and nothing else.&lt;/p&gt;

&lt;p&gt;go-tool-base consumes it on &lt;code&gt;main&lt;/code&gt;, so a tool built on its next release serves MCP through go/mcp, in compact mode unless its author says otherwise. The &lt;code&gt;mcp&lt;/code&gt; feature became a link while I was at it, meaning a tool that disables it ships without the module &lt;em&gt;and&lt;/em&gt; without the MCP SDK in its binary (a separate post, that one).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;Pre-1.0, and moving... &lt;code&gt;v0.1.0&lt;/code&gt; went out last night with the operation model, the SDK adapter over stdio and Streamable HTTP in both modes, the Cobra binding and the &lt;code&gt;mcp&lt;/code&gt; command tree, and &lt;code&gt;v0.2.0&lt;/code&gt; followed it this morning with the gRPC binding. The mounted-service shape is proven by the module's own tests.&lt;/p&gt;

&lt;p&gt;Native macOS and Windows subprocess acceptance is wired up and has not yet run, and the same goes for the browser side of the Apps shell; the &lt;a href="https://mcp.go.phpboyscout.uk/reference/development/" rel="noopener noreferrer"&gt;development reference&lt;/a&gt; says what that means instead of leaving you to guess. The docs are at &lt;a href="https://mcp.go.phpboyscout.uk" rel="noopener noreferrer"&gt;mcp.go.phpboyscout.uk&lt;/a&gt;, the design is &lt;a href="https://gitlab.com/phpboyscout/go/mcp/-/wikis/specs/0001-progressive-mcp" rel="noopener noreferrer"&gt;spec 0001&lt;/a&gt; on the wiki, and it installs the usual way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get gitlab.com/phpboyscout/go/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credit where it's due, and it isn't all mine: a different agent built the bulk of the module over a week from that spec while I was busy elsewhere, ran out of quota, left a handover on the wiki, and the session I was in picked it up cold and finished it. How that went, and what the handover got wrong, deserves its own telling and I'll get to it.&lt;/p&gt;

&lt;p&gt;For six months I thought showing an assistant everything I had was the generous thing to do. Fifty-four schemas and 112 KB of JSON later, gtb carries a lantern, and the assistant on the other end gets to spend its context on the actual problem for a change... and given the sort of problems I hand it, it's going to need every bit of that.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/fifty-four-tools-before-a-word-was-typed/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 18 September 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>go</category>
      <category>cli</category>
    </item>
    <item>
      <title>Two encrypted emails in twenty years</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Fri, 18 Sep 2026 01:40:39 +0000</pubDate>
      <link>https://dev.to/phpboyscout/two-encrypted-emails-in-twenty-years-1461</link>
      <guid>https://dev.to/phpboyscout/two-encrypted-emails-in-twenty-years-1461</guid>
      <description>&lt;p&gt;Someone finds a hole in something I've published. It's late, they're half sure it's real and half sure they're about to waste everyone's time, and they're deciding whether telling me is worth the next twenty minutes of their evening.&lt;/p&gt;

&lt;p&gt;Everything I've built for that moment has been used twice in about twenty years. One of those two was me, testing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two, and one of them was mine
&lt;/h2&gt;

&lt;p&gt;That number is the best argument anyone has against me, so it goes first.&lt;/p&gt;

&lt;p&gt;I've been publishing a PGP key in one place or another since I was young enough to think it made me interesting. Various addresses, various keyservers, eventually &lt;a href="https://phpboyscout.uk/publish-your-key-where-they-cant-touch-it/" rel="noopener noreferrer"&gt;my own domain where no platform can revoke it&lt;/a&gt;. Across all of that, encrypted mail from an actual human being... one. The other was a test message I sent myself to prove the pipeline worked.&lt;/p&gt;

&lt;p&gt;If you wanted to argue that publishing a key for security reports is a ritual hardly anyone participates in, you would use my numbers. They are the best evidence you have got.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's theatre. So is airport security.
&lt;/h2&gt;

&lt;p&gt;So yes, it's theatre.&lt;/p&gt;

&lt;p&gt;But then, so is most of what happens at an airport. Very little of the queueing and the shoe-removal stops a determined person, and a great deal of it exists so that everybody moving through the building can see that someone has thought about this and taken it seriously. That isn't nothing. I'd say it's most of what the exercise is for.&lt;/p&gt;

&lt;p&gt;What the theatre buys me is the only thing that matters at the moment I opened with. A stranger, deciding whether to bother.&lt;/p&gt;

&lt;p&gt;They have no way of knowing whether my mailbox goes anywhere at all. They can't tell whether a reply comes back from a person, or from a form, or ever. What they &lt;em&gt;can&lt;/em&gt; see is whether the whole arrangement round it looks like it was built by a person who cares what turns up, and every visible piece of it counts as evidence one way or the other.&lt;/p&gt;

&lt;p&gt;Email is plaintext on the wire and always has been. So the &lt;em&gt;offer&lt;/em&gt; of encryption has to exist for the channel to be credible, even when almost no one takes it up. The offer is doing the work, and the cryptography barely gets a look in, which is an uncomfortable thing to write after the couple of weeks I spent building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line I'd forgotten to write
&lt;/h2&gt;

&lt;p&gt;I only found this next bit because I went to look at what a researcher would see, rather than what I remembered publishing.&lt;/p&gt;

&lt;p&gt;I have a &lt;code&gt;security.txt&lt;/code&gt;. It has the contact address, the expiry, the languages, the canonical URL, the policy link. It's generated rather than hardcoded so the expiry can't lapse on a day when I happen not to be looking. I'd been rather smug about it.&lt;/p&gt;

&lt;p&gt;It had no &lt;code&gt;Encryption:&lt;/code&gt; field, none at all. The key I'd deliberately published outside any platform's reach, the one I wrote a whole post about publishing properly, was not mentioned in the one file a security researcher actually opens before deciding how to get hold of you.&lt;/p&gt;

&lt;p&gt;Twenty years of publishing the thing, and no sign anywhere that I had.&lt;/p&gt;

&lt;p&gt;A man who has received two encrypted emails in twenty years had somehow failed to advertise the possibility of a third!&lt;/p&gt;

&lt;p&gt;It's there now, two lines of it.&lt;/p&gt;

&lt;p&gt;And there was a second one waiting in the same sweep: the key resolves by the WKD advanced method, and the apex path returns a 404. &lt;code&gt;gpg&lt;/code&gt; tries advanced first so in practice everybody is fine, but a client that only knows the direct method gets nothing at all.&lt;/p&gt;

&lt;p&gt;Both of those had been true for weeks while I was busy admiring the part I'd got right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the whole thing is standing on
&lt;/h2&gt;

&lt;p&gt;The foundations are not good, and it would be daft to write all this without saying so.&lt;/p&gt;

&lt;p&gt;Go looked at OpenPGP for the standard library and declined it, on the grounds that the thing is fragile. That was years ago now. The package is frozen, it carries an advisory with no fixed version at all (not "upgrade to this", just: unmaintained and unsafe by design), and the fork everybody actually uses is kept going by one company for the purposes of that company's product.&lt;/p&gt;

&lt;p&gt;It is fine, in the specific way that a load-bearing wall nobody has ever inspected is fine.&lt;/p&gt;

&lt;p&gt;That's an awkward place to stand for someone whose whole pitch is a serious security posture, and I've not resolved it. I'm certainly not going to resolve it in the back third of a blog post (though I'd love to be the man who did).&lt;/p&gt;

&lt;p&gt;The reason I keep it anyway is the stranger again. &lt;code&gt;gpg&lt;/code&gt; is what a researcher has on their machine at two in the morning. Something more defensible that they'd have to stop and install first is a channel nobody uses, and a security contact nobody can be bothered with is worse than an imperfect one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The channels I'd actually prefer
&lt;/h2&gt;

&lt;p&gt;Asked what I'd rather have, none of my answers are PGP.&lt;/p&gt;

&lt;p&gt;A properly configured and secured web form, over TLS, where the reporter doesn't have to do anything but type. A peer-encrypted messaging service, where the hard part is someone else's problem and has been solved better than I'll solve it. Or, and this one only occurred to me while writing this, a direct message to my own Discord bot, which already sits inside my infrastructure and already knows who I am.&lt;/p&gt;

&lt;p&gt;That last one is now on the bot's backlog, which is a slightly absurd outcome for a post about whether a twenty-year-old key was worth publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  And I'd publish it again
&lt;/h2&gt;

&lt;p&gt;That doesn't change the answer, and the answer is yes, publish the key.&lt;/p&gt;

&lt;p&gt;Making sure the channel for reporting a security problem actually works, and keeps working, is not paperwork &lt;em&gt;about&lt;/em&gt; the security posture, it's part of it, and a hole in the reporting path is a hole. I've spent months on the code and the infrastructure and the signing keys, and the route someone uses to tell me I got it wrong deserves the same attention, arguably more, because it's the only part a stranger ever sees.&lt;/p&gt;

&lt;p&gt;So the key stays. It'll probably sit there another twenty years and get used twice more, and the second one will be me again, checking it still works after some change I've forgotten I made.&lt;/p&gt;

&lt;p&gt;And that's fine, because being used was never quite the job. It's there so that at midnight, when someone is weighing up whether I'm worth the twenty minutes, there's one more thing on the page that says I'd want to know.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/two-encrypted-emails-in-twenty-years/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 18 September 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>security</category>
      <category>signing</category>
      <category>supplychain</category>
    </item>
    <item>
      <title>A client you can still use when you configured it wrong</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Thu, 17 Sep 2026 01:41:17 +0000</pubDate>
      <link>https://dev.to/phpboyscout/a-client-you-can-still-use-when-you-configured-it-wrong-26cc</link>
      <guid>https://dev.to/phpboyscout/a-client-you-can-still-use-when-you-configured-it-wrong-26cc</guid>
      <description>&lt;p&gt;There's a question that turns up every time you wire a chat client together, and it's this. You set a temperature. The model you picked doesn't do temperature.&lt;/p&gt;

&lt;p&gt;Now what?&lt;/p&gt;

&lt;p&gt;It's a fair question, and Go has a very firm opinion about it. I disagree with Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go wants a yes or a no
&lt;/h2&gt;

&lt;p&gt;The constructor convention is about as settled as conventions get: return &lt;code&gt;(T, error)&lt;/code&gt;, and on a non-nil error the &lt;code&gt;T&lt;/code&gt; is unusable so don't touch it. Most people know it, most follow it, and it's a good rule. It's good because it assumes construction is one thing that either happened or didn't: open a file, dial a socket, parse a document. Binary outcome, binary contract, no argument. A chat client isn't one thing.&lt;/p&gt;

&lt;p&gt;It's a provider, a model, credentials, a timeout, an endpoint, sampling controls, streaming, tool support, a fallback chain, and a fistful of knobs that only exist on some models. Building one is a dozen small decisions, and the interesting failures are always partial. Eleven of them worked. The twelfth (usually the one you cared about least) doesn't apply here.&lt;/p&gt;

&lt;p&gt;The binary contract has no way to say that, so it forces the library into one of two bad answers, and worse, it forces the choice &lt;em&gt;once&lt;/em&gt;, globally, by whoever wrote the constructor, on behalf of every caller and every combination they'll ever try. Bin a working client over a setting nobody would call essential, or keep quiet and let them think it applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  So give them a receipt
&lt;/h2&gt;

&lt;p&gt;I broke it, deliberately. Build a client with an invalid combination and you get a client: the best working one I can assemble, fully usable, minus the bits that couldn't apply, and with it an itemised list of what didn't make it on. Here's your client, and here's the receipt: two true things instead of one lie.&lt;/p&gt;

&lt;p&gt;What makes that a design, rather than me being soft about it, is the exception. There's a hard line between a setting that got &lt;em&gt;dropped&lt;/em&gt; and a construction that's &lt;em&gt;impossible&lt;/em&gt;, and that line isn't up for negotiation at runtime. Temperature on a model with no temperature is a degradation, so you get the client plus a line on the receipt; a missing credential is fatal, so you get nothing and the error says so through a sentinel called, without much ceremony, &lt;code&gt;ErrUnableToConstruct&lt;/code&gt;. Without that line the whole idea collapses into a library deciding your mistakes don't matter much.&lt;/p&gt;

&lt;h2&gt;
  
  
  A receipt nobody can read is just litter
&lt;/h2&gt;

&lt;p&gt;This pattern lives or dies on the quality of one error value, which is a nerve-wracking place to put a design. "Some settings were not applied" is worse than a hard failure: it tells you something's wrong, hands you no way to act on it, and takes away the crash that would at least have pointed at a line number. So the line item has to carry enough to actually fix the thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// DroppedSetting names a Config field that could not be applied, and why.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DroppedSetting&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Fields names the Config fields that were not applied, e.g. "Temperature".&lt;/span&gt;
    &lt;span class="n"&gt;Fields&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="c"&gt;// Capability is the capability the fields required.&lt;/span&gt;
    &lt;span class="n"&gt;Capability&lt;/span&gt; &lt;span class="n"&gt;Capability&lt;/span&gt;
    &lt;span class="c"&gt;// Reason is a short human-readable explanation.&lt;/span&gt;
    &lt;span class="n"&gt;Reason&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which field, what capability it needed, and why it didn't happen.&lt;/p&gt;

&lt;p&gt;There's a fix in the history that exists purely because my first go at this wasn't good enough: &lt;a href="https://gitlab.com/phpboyscout/go/chat/-/commit/781d131" rel="noopener noreferrer"&gt;&lt;code&gt;fix(chat): name the default model in a dropped-setting error&lt;/code&gt;&lt;/a&gt;. The original said a setting had been dropped without saying which model it had been reasoning about, so if you hadn't picked a model and were leaning on the default, the error told you something was wrong about a thing it then declined to name. That's the very failure I'd spent two weeks describing as worse than crashing... shipped by me, in the first cut of the feature designed to prevent it!&lt;/p&gt;

&lt;p&gt;It lasted about twelve hours.&lt;/p&gt;

&lt;p&gt;The whole receipt carries the same obligation, and itemises &lt;em&gt;everything&lt;/em&gt; rather than the first thing it trips over:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Construction reports every problem rather than the first, so a caller fixing three mistakes learns all three from one call instead of one round-trip at a time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also does Go's multi-error unwrapping, so &lt;code&gt;errors.Is&lt;/code&gt; reaches any member sentinel whether it was the only problem or one of four. Worth proving rather than assuming, because the project layers &lt;code&gt;cockroachdb/errors&lt;/code&gt; over the standard library and I wanted to watch &lt;code&gt;errors.Join&lt;/code&gt; and the wrapped values get along before building an API on top of them. &lt;a href="https://phpboyscout.uk/a-stack-trace-is-not-an-error-message/" rel="noopener noreferrer"&gt;A stack trace is not an error message&lt;/a&gt; is the same instinct pointed at logs. This is it pointed at a return value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this falls down
&lt;/h2&gt;

&lt;p&gt;There is a decent case against. A caller who ignores that receipt is holding a client that isn't doing what they configured, which is the failure the strict contract exists to prevent. Plenty of people will write &lt;code&gt;client, _ :=&lt;/code&gt; and get on with their day (I've done it myself, more than once); the strict version would have stopped them and mine won't.&lt;/p&gt;

&lt;p&gt;But the information was &lt;em&gt;given&lt;/em&gt;. They got a complete, specific, actionable account of what didn't apply, and they made a choice, which is a different thing entirely from a library that swallows the setting and says nothing, even if from the outside you can't tell them apart. I'd be overselling it if I claimed the trade goes away. It doesn't. I've moved a decision from the library to the caller, and some callers won't make it.&lt;/p&gt;

&lt;p&gt;Still, the strict contract was never protecting them anyway. It protects you from a &lt;em&gt;whole broken client&lt;/em&gt;, and this was never a whole broken client. It was a perfectly good one with a temperature setting that went nowhere, and the old rules had one way of mentioning it: bin the lot, and hope you read the message on the way past. So somewhere out there, right now, there's a &lt;code&gt;client, _ :=&lt;/code&gt; running along quite happily with a temperature that never happened.&lt;/p&gt;

&lt;p&gt;The receipt was in the bag all along. Whether anyone looks in the bag is a different matter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/a-client-you-can-still-use-when-you-configured-it-wrong/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 17 September 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>api</category>
      <category>errors</category>
      <category>llm</category>
    </item>
    <item>
      <title>The config key that quietly did nothing</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:24:16 +0000</pubDate>
      <link>https://dev.to/phpboyscout/the-config-key-that-quietly-did-nothing-396i</link>
      <guid>https://dev.to/phpboyscout/the-config-key-that-quietly-did-nothing-396i</guid>
      <description>&lt;p&gt;I once spent the better part of an hour convinced a timeout setting was broken. I'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'd typed it wrong, and not one thing in the entire stack had thought that worth mentioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config loaders are too polite
&lt;/h2&gt;

&lt;p&gt;Most config loaders have the same agreeable flaw: they'll read whatever's in the file and quietly ignore anything they weren't expecting. Put a key the tool doesn'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'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&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's the struct you unmarshal into. The field names, the types, which ones matter. That description exists; it's just not being used to &lt;em&gt;check&lt;/em&gt; anything.&lt;/p&gt;

&lt;p&gt;go-tool-base's config package puts it to work. You hand it a tagged struct and it derives a schema from the tags, in &lt;a href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/config/schema.go#L48" rel="noopener noreferrer"&gt;&lt;code&gt;pkg/config/schema.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// WithStructSchema derives a schema from a tagged Go struct.&lt;/span&gt;
&lt;span class="c"&gt;// Supported tags: `config:"key" validate:"required" enum:"a,b,c" default:"value"`.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WithStructSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SchemaOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a feature's config type carries its own rules inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ServerConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Host&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`config:"host" validate:"required"`&lt;/span&gt;
    &lt;span class="n"&gt;Port&lt;/span&gt;    &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="s"&gt;`config:"port" validate:"required"`&lt;/span&gt;
    &lt;span class="n"&gt;LogMode&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`config:"log_mode" enum:"text,json"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no second artefact to keep in sync, which is the same instinct go-tool-base leans on for &lt;a href="https://phpboyscout.uk/stop-regexing-the-llms-prose/" rel="noopener noreferrer"&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't drift apart because there'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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// WithStrictMode treats unknown keys as errors instead of warnings.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WithStrictMode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;SchemaOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default a key the schema doesn'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't in the schema, so the tool refuses to start and tells me which key it didn'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&gt;
  
  
  What it deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;There's one decision in here I think is worth calling out, because the obvious feature is conspicuously absent. The schema knows each field'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't, on purpose. Validation validates. It tells you what's wrong and what to do about it, and it stops there. Defaults are a separate job, handled by the &lt;a href="https://phpboyscout.uk/many-embedded-filesystems-one-merged-view/" rel="noopener noreferrer"&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'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&gt;
  
  
  Errors you can act on
&lt;/h2&gt;

&lt;p&gt;The output isn'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 href="https://phpboyscout.uk/errors-that-tell-the-user-what-to-do-next/" rel="noopener noreferrer"&gt;errors that tell you what to do next&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A config loader that silently ignores keys it doesn'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'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'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;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/the-config-key-that-quietly-did-nothing/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 27 March 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>config</category>
      <category>gotoolbase</category>
    </item>
    <item>
      <title>Middleware for CLI commands, not just web servers</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Tue, 15 Sep 2026 14:02:03 +0000</pubDate>
      <link>https://dev.to/phpboyscout/middleware-for-cli-commands-not-just-web-servers-4hkp</link>
      <guid>https://dev.to/phpboyscout/middleware-for-cli-commands-not-just-web-servers-4hkp</guid>
      <description>&lt;p&gt;Every CLI tool past a certain size grows a category of logic that doesn't really belong to any one command, and yet has to happen for loads of them. Timing. An auth check. Panic recovery, so a crash becomes a clean error instead of a stack-trace all over someone's terminal. A log line saying the command started and how it finished.&lt;/p&gt;

&lt;p&gt;Web frameworks sorted this out years ago. CLIs, for some reason, mostly still copy-paste it around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The logic that belongs to no single command
&lt;/h2&gt;

&lt;p&gt;That category of logic doesn't belong to any one command, yet needs to happen for many of them. Time how long the command took. Check the user is authenticated before a command that needs it. Recover from a panic so a crash becomes a clean error rather than a stack-trace vomited across the screen. Log that the command started and how it ended.&lt;/p&gt;

&lt;p&gt;None of that is the command's &lt;em&gt;job&lt;/em&gt;. The &lt;code&gt;deploy&lt;/code&gt; command's job is to deploy. But timing and recovery and auth still have to happen around it, and around &lt;code&gt;build&lt;/code&gt;, and around &lt;code&gt;sync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Put that logic &lt;em&gt;inside&lt;/em&gt; each command's &lt;code&gt;RunE&lt;/code&gt; and you've copied the same six lines into thirty functions, which means thirty places to fix when the logging format changes and thirty chances to forget one of them. Cross-cutting concerns copied by hand don't stay consistent. They drift, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web frameworks already solved this
&lt;/h2&gt;

&lt;p&gt;This is not a new problem. It's about the oldest problem in web frameworks, and they settled on an answer a long time ago: middleware. Gin has it, Echo has it, every HTTP stack you've ever touched has it. A middleware is a wrapper that sits around a handler, runs its cross-cutting logic, and calls through to the handler in the middle.&lt;/p&gt;

&lt;p&gt;A CLI command is, structurally, just a handler too. So go-tool-base brings the same pattern to the Cobra command tree, with the same functional &lt;a href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/setup/middleware.go#L14" rel="noopener noreferrer"&gt;&lt;code&gt;Chain&lt;/code&gt;&lt;/a&gt; shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Middleware&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&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="kt"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A middleware receives the &lt;em&gt;next&lt;/em&gt; handler in the chain and returns a new handler that wraps it. You compose a stack of them, and each command's real &lt;code&gt;RunE&lt;/code&gt; runs in the middle of the onion. Write the timing logic once, as one middleware, and every command in the chain is timed. Change the log format once and all thirty commands change with it, because there was only ever one copy. (The "write it once, in a place where everyone inherits it" drum again, which I will keep banging until the series runs out.)&lt;/p&gt;

&lt;h2&gt;
  
  
  "But Cobra already has PreRun"
&lt;/h2&gt;

&lt;p&gt;It does, and this is the objection worth answering properly, because Cobra ships &lt;code&gt;PersistentPreRun&lt;/code&gt; and &lt;code&gt;PreRun&lt;/code&gt; hooks and they look, at a glance, like they cover this.&lt;/p&gt;

&lt;p&gt;They don't, and the reason is structural. A &lt;code&gt;PreRun&lt;/code&gt; hook is a thing that happens &lt;em&gt;before&lt;/em&gt; the command. That's all it is. It can't run anything &lt;em&gt;after&lt;/em&gt;. It can't wrap the command in a &lt;code&gt;defer&lt;/code&gt;. It can't catch a panic the command throws. It can't measure how long the command took, because measuring a duration needs a start point &lt;em&gt;and&lt;/em&gt; an end point, and the hook only owns the start.&lt;/p&gt;

&lt;p&gt;A middleware wraps the &lt;em&gt;entire&lt;/em&gt; execution. Because it's a function that calls &lt;code&gt;next()&lt;/code&gt; in its own body, it straddles the command (with the handler signature abbreviated to &lt;code&gt;HandlerFunc&lt;/code&gt; here for readability):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TimingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;HandlerFunc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cobra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&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="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c"&gt;// the command runs here&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"command finished"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"took"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before, after, and around. A recovery middleware can put a &lt;code&gt;defer recover()&lt;/code&gt; in place that a &lt;code&gt;PreRun&lt;/code&gt; hook structurally cannot. An auth middleware can check a condition and return an error &lt;em&gt;instead of calling &lt;code&gt;next()&lt;/code&gt; at all&lt;/em&gt;, refusing to let the command run in the first place. &lt;code&gt;PreRun&lt;/code&gt; can't veto the command; it runs, and then the command runs regardless.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PreRun&lt;/code&gt; is a notification that the command is about to happen. Middleware is control over whether and how it happens. For genuine cross-cutting concerns you need the second thing, not the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  To sum up
&lt;/h2&gt;

&lt;p&gt;Timing, auth, recovery and logging are cross-cutting concerns: necessary for many commands, owned by none. Hand-copied into every &lt;code&gt;RunE&lt;/code&gt;, they drift out of sync. Web frameworks fixed this with middleware years ago, and a CLI command is structurally just another handler.&lt;/p&gt;

&lt;p&gt;go-tool-base brings the functional Chain middleware pattern to the Cobra command tree. A middleware wraps a command's whole execution, so it acts before and after and can decide whether the command runs at all... strictly more than Cobra's &lt;code&gt;PreRun&lt;/code&gt; hooks, which only fire beforehand and can't wrap, recover, time, or veto. Write the concern once, wrap the chain, and every command inherits it consistently.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/middleware-for-cli-commands-not-just-web-servers/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 24 March 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>go</category>
      <category>cli</category>
      <category>middleware</category>
    </item>
    <item>
      <title>Half your users don't have eyes</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Tue, 15 Sep 2026 14:01:56 +0000</pubDate>
      <link>https://dev.to/phpboyscout/half-your-users-dont-have-eyes-849</link>
      <guid>https://dev.to/phpboyscout/half-your-users-dont-have-eyes-849</guid>
      <description>&lt;p&gt;Run a command in your favourite CLI tool and look at what comes back. Colour. Neatly aligned columns. A friendly little summary sentence. Lovely... if you happen to be a human with eyes.&lt;/p&gt;

&lt;p&gt;But a good half of any tool's users aren't people at all. They're scripts, CI pipelines, bits of automation. And that pretty output you're so proud of is, to them, actively hostile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your tool has two audiences and only serves one
&lt;/h2&gt;

&lt;p&gt;I made more or less this same point about AI assistants when I argued that &lt;a href="https://phpboyscout.uk/your-cli-is-already-an-ai-tool/" rel="noopener noreferrer"&gt;your CLI is already an AI tool&lt;/a&gt;. The machines are users too. Here it isn't an AI doing the calling, it's a humble shell script, but the principle is identical.&lt;/p&gt;

&lt;p&gt;Run a CLI command and look at what comes back. Colour. Aligned columns. A friendly summary sentence. It's designed for a person reading a terminal, and for a person reading a terminal it's great.&lt;/p&gt;

&lt;p&gt;Now picture the other half of your users. A deploy script that needs to know which version is installed. A CI job that runs &lt;code&gt;doctor&lt;/code&gt; and wants to fail the build on one specific check. A bit of automation gluing your tool to three others. None of them have eyes. They have parsers.&lt;/p&gt;

&lt;p&gt;So what do they do with your beautiful human output? They butcher it. They &lt;code&gt;grep&lt;/code&gt; for a keyword, &lt;code&gt;awk&lt;/code&gt; out the third field, &lt;code&gt;sed&lt;/code&gt; off a prefix. It works in the demo. Then someone rewords a status line, or adds a column, or the colour codes shift, and every script downstream breaks at once. Silently, too, because a broken &lt;code&gt;grep&lt;/code&gt; returns nothing rather than an error. You changed a sentence and quietly took out somebody's pipeline without ever knowing.&lt;/p&gt;

&lt;p&gt;The human-readable output was never the contract. It just got &lt;em&gt;used&lt;/em&gt; as one, because it was the only output there was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the machines their own channel
&lt;/h2&gt;

&lt;p&gt;The fix is not to make the human output more parseable. That's a trap. You'd be constraining prose meant for people in order to satisfy programs, and end up serving neither of them well. The fix is to give programs their own output format, declared and stable, kept well away from the prose.&lt;/p&gt;

&lt;p&gt;So every command built with go-tool-base gets a &lt;a href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/cmd/root/root.go#L447" rel="noopener noreferrer"&gt;&lt;code&gt;--output&lt;/code&gt; flag&lt;/a&gt;. Leave it alone and you get the friendly human rendering. Pass &lt;code&gt;--output json&lt;/code&gt; and you get something a parser can actually rely on.&lt;/p&gt;

&lt;p&gt;And not just &lt;em&gt;some&lt;/em&gt; JSON. JSON with a fixed shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  One envelope, every command
&lt;/h2&gt;

&lt;p&gt;The temptation with JSON output is to let each command emit whatever structure happens to suit it. Don't. A consumer scripting against five of your commands then has to learn five shapes, and "where's the actual payload?" has a different answer every single time.&lt;/p&gt;

&lt;p&gt;go-tool-base wraps every command's JSON in one standard &lt;code&gt;Response&lt;/code&gt; envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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 class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"replicas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; says how it went. &lt;code&gt;command&lt;/code&gt; says what produced it. &lt;code&gt;data&lt;/code&gt; holds the command-specific payload, and &lt;em&gt;only&lt;/em&gt; the payload. Every built-in command (&lt;code&gt;version&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt;) emits exactly this shape. So does every command you write, because &lt;a href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/output/output.go#L45" rel="noopener noreferrer"&gt;&lt;code&gt;pkg/output&lt;/code&gt;&lt;/a&gt; hands you the envelope rather than letting you freelance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer-side payoff is the whole point. A script can check &lt;code&gt;.status&lt;/code&gt; without ever touching &lt;code&gt;.data&lt;/code&gt;. It can pull &lt;code&gt;.data.version&lt;/code&gt; and know the field is there because it's typed, not scraped. It learns the envelope once, and every command in your tool, and every tool built on the framework, honours it. The contract is explicit, versioned, and the same everywhere, which is precisely what the abused human output never was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human output gets to relax
&lt;/h2&gt;

&lt;p&gt;There's a quiet second benefit, and it's my favourite kind: the sort you get for free. Once programs have their own reliable channel, the human output is &lt;em&gt;freed&lt;/em&gt;. It no longer has to stay accidentally parseable. You can reword a status line, add colour, restructure a table, make it genuinely nicer to read, and not break a single script, because no script is reading it any more. They're all over on &lt;code&gt;--output json&lt;/code&gt;, where the real contract lives.&lt;/p&gt;

&lt;p&gt;Two audiences, two formats, each one actually suited to its reader. That's the deal a CLI tool ought to be offering, and most of them don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;p&gt;A CLI tool that only emits human-readable output is only half-built, because half its users are programs that end up &lt;code&gt;grep&lt;/code&gt;-ing prose and shattering the moment that prose changes. go-tool-base gives every command a &lt;code&gt;--output json&lt;/code&gt; flag and one standard &lt;code&gt;Response&lt;/code&gt; envelope (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;) used identically by every built-in command and by anything you write through &lt;code&gt;pkg/output&lt;/code&gt;. Machines get a stable, explicit, learn-it-once contract; humans get output that's now free to be properly readable, because nothing fragile depends on its wording any more.&lt;/p&gt;

&lt;p&gt;If your tool will ever be called by another program (and it will), give that program a front door. Don't make it climb in through the window.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/half-your-users-dont-have-eyes/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 25 March 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>cli</category>
      <category>automation</category>
      <category>json</category>
    </item>
    <item>
      <title>Check the code you're reading is current</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:58:36 +0000</pubDate>
      <link>https://dev.to/phpboyscout/check-the-code-youre-reading-is-current-79m</link>
      <guid>https://dev.to/phpboyscout/check-the-code-youre-reading-is-current-79m</guid>
      <description>&lt;p&gt;I nearly raised a merge request against one of my own libraries, at about six in the morning and one coffee in, to add a feature it already had.&lt;/p&gt;

&lt;p&gt;It had shipped the day before. Tagged, released, tests, a paragraph in the getting-started guide... I'd have looked a proper wally, in public, on a repo with my own name across the top of it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull, you clown
&lt;/h2&gt;

&lt;p&gt;My fault, straightforwardly. I was reading a checkout that was twenty commits behind and I hadn't checked, and there's no version of that where I come out well.&lt;/p&gt;

&lt;p&gt;What I wanted was for an HTTP server in &lt;code&gt;go/transport&lt;/code&gt; to bind to one interface rather than every address on the box. So I went and looked. &lt;code&gt;ServerSettings&lt;/code&gt; had a port on it. It did not have a host. I read the config, I read the server, I found nothing at all, and off I went, certain enough to start drafting the change and mildly pleased with myself for spotting a gap.&lt;/p&gt;

&lt;p&gt;The field wasn't there because &lt;a href="https://gitlab.com/phpboyscout/go/transport/-/commit/aeeaa72" rel="noopener noreferrer"&gt;the commit that added it&lt;/a&gt; had landed the previous afternoon and gone out in &lt;code&gt;v0.2.0&lt;/code&gt; the same day. Roughly four feet away from what I was reading, in a directory I had open.&lt;/p&gt;

&lt;p&gt;So, yes. Pull, you clown.&lt;/p&gt;

&lt;p&gt;But I've been chewing on it since, because "be more careful" is the sort of lesson that lasts about a fortnight, and I don't think carelessness is what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I never noticed
&lt;/h2&gt;

&lt;p&gt;I work in worktrees. There's a standing order about it and it's a good one: when the change belongs in a repo you aren't sat in, or when another session might be live on the same repo, you leave the shared checkout alone entirely. Cut a worktree off the target's freshly fetched &lt;code&gt;origin/main&lt;/code&gt;, work in there, tidy up after. It stops two sessions fighting over the same branch, and it stops one wandering into the other's checkout mid-build (which I have watched happen, and would rather not again), and I'd not give it up for anything.&lt;/p&gt;

&lt;p&gt;Have a look at what it takes away, though.&lt;/p&gt;

&lt;p&gt;In an ordinary week you'd wander into a repository, pull, and start work. That pull is doing two jobs, and fetching code is only one of them. The other is telling you how far behind you'd got. It's a little status report nobody asked for, delivered every time you sit down, and you stop noticing it's there.&lt;/p&gt;

&lt;p&gt;Work in worktrees and it simply stops arriving. You don't check the main clone out. There's no reason to go anywhere near it. So nothing pulls it, and it drifts a commit at a time, and the thing that used to tell you has been taken out of the room without anybody mentioning it.&lt;/p&gt;

&lt;p&gt;The rule was also only ever about the repo I was &lt;em&gt;changing&lt;/em&gt;. The sibling libraries I was &lt;em&gt;reading&lt;/em&gt;, to answer a question, all sat outside it completely. That's why twenty of them were behind at once, not twenty separate lapses. One gap nobody had thought about, me very much included.&lt;/p&gt;

&lt;h2&gt;
  
  
  What made it dangerous rather than annoying
&lt;/h2&gt;

&lt;p&gt;A stale checkout doesn't look stale, and that's the whole difficulty (it took the second mistake of the morning before I saw it). It doesn't warn you, or sulk, or leave a note. It sits there being perfectly agreeable and answering every question you put to it, accurately, about two weeks ago. Most other kinds of mistake announce themselves eventually: a typo fails, a bad merge goes red, a wrong assumption produces an answer someone queries. This one compiles. The signatures are sensible, the logic hangs together, and it hands you a conclusion that is confident, coherent and defensible about a version of the world that stopped existing a while back.&lt;/p&gt;

&lt;p&gt;The evidence is even real. It's just old...&lt;/p&gt;

&lt;p&gt;And it goes wrong in one particular direction, which is what makes it worth writing down. Stale source rarely misleads you about what code &lt;em&gt;does&lt;/em&gt;. Read a function from three weeks back and it almost certainly still does roughly that. What it misleads you about is what &lt;em&gt;isn't there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;"There's no API for it." "Upstream can't do that." "The library doesn't support it." Absence claims, the lot of them, and they're just what a stale source manufactures, because absence is the one thing you can't check by staring harder at what's in front of you. There's nothing there to stare at. That &lt;em&gt;is&lt;/em&gt; the claim.&lt;/p&gt;

&lt;p&gt;They're also the expensive ones, and that isn't a coincidence either. Decide a thing is missing and your very next move is to go and &lt;em&gt;build&lt;/em&gt; it: a workaround for a bug fixed two releases back, or a wrapper round a gap that closed in March, or a merge request for a feature that shipped yesterday afternoon (the one I was ninety seconds from). The second mistake that morning was reading &lt;code&gt;go/controls&lt;/code&gt; and concluding it had no HTTP support whatsoever, and that one would have had me build the thing rather than merely offer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;p&gt;Stable door, horse long gone. It's still the right door.&lt;/p&gt;

&lt;p&gt;The rule that came out of it isn't "remember to pull". It's the other half of the worktree rule, and it's about reading rather than writing: before you draw a conclusion out of some code, confirm you're looking at the current version of it. The line I typed into the global agent instructions that morning was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;always check that the code we investigate is the latest version and not stale otherwise that leads to bad assumption&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Global, rather than one project's memory, because it was never a &lt;a href="https://phpbotscout.phpboyscout.uk" rel="noopener noreferrer"&gt;phpbotscout&lt;/a&gt; problem. It's a working-pattern problem and the working pattern is everywhere.&lt;/p&gt;

&lt;p&gt;In practice it's four small things, all of them dull. Fetch, then count, because &lt;code&gt;git rev-list --count HEAD..@{u}&lt;/code&gt; gives you the distance to upstream as one number and if it isn't nought you don't know what you're looking at. Read &lt;code&gt;origin/main&lt;/code&gt; directly rather than the working tree, which costs nothing and doesn't need you fast-forwarding a checkout that might have your own mess in it. Remember that a module cache holds the &lt;em&gt;pinned&lt;/em&gt; version, which is a fact about your &lt;code&gt;go.mod&lt;/code&gt; and says nothing whatever about what the library can do this morning. And name the version you checked, whenever you tell someone their code can't do something, because it costs you a clause and it lets anyone prove you wrong in ten seconds. Including you, in four months.&lt;/p&gt;

&lt;p&gt;It went in at about six in the morning. By quarter past five that afternoon it had already caught one: another repo, ten behind, spotted before being read rather than after being concluded about.&lt;/p&gt;

&lt;p&gt;That's really all I wanted out of it, and there's nothing clever in it anywhere, which is the point, really. Just a cheap habit parked in front of a whole category of confident error, which is more or less the only way I know to keep delegated work honest at any scale, because you can't read every line (and if you can, you haven't delegated anything).&lt;/p&gt;

&lt;p&gt;A rule I'd still write again tomorrow had quietly taken something away without mentioning it, and the fix was never to tear it up. It was to work out what it had stopped doing for me, and go and write the other half.&lt;/p&gt;

&lt;p&gt;I checked, while putting this together. &lt;code&gt;go/controls&lt;/code&gt; on this machine is thirty commits behind.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/check-the-code-youre-reading-is-current/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 15 September 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>agents</category>
      <category>go</category>
    </item>
    <item>
      <title>Nobody's coming to clean your supply chain</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Mon, 14 Sep 2026 06:54:37 +0000</pubDate>
      <link>https://dev.to/phpboyscout/nobodys-coming-to-clean-your-supply-chain-3c9g</link>
      <guid>https://dev.to/phpboyscout/nobodys-coming-to-clean-your-supply-chain-3c9g</guid>
      <description>&lt;p&gt;Pick a week in May 2026 and there's a supply-chain attack in it. On the 11th someone owned TanStack's CI and pushed &lt;a href="https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised" rel="noopener noreferrer"&gt;84 poisoned package versions in six minutes&lt;/a&gt;. On the 14th, three malicious versions of &lt;a href="https://www.stepsecurity.io/blog/node-ipc-npm-supply-chain-attack" rel="noopener noreferrer"&gt;node-ipc&lt;/a&gt;, a library with ten million weekly downloads, shipped an identical credential-stealer. Days later it was &lt;a href="https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/" rel="noopener noreferrer"&gt;@antv&lt;/a&gt;, cascading down into a charting library a million projects depend on. Each one runs its payload the moment you install it, then quietly tries to publish itself from your machine.&lt;/p&gt;

&lt;p&gt;You've heard this story so many times the outrage has worn smooth. So let me point at the one detail that should still make you sit up: the TanStack packages carried &lt;em&gt;valid signing provenance&lt;/em&gt;. Real attestation, pointing at the real pipeline. The seal was genuine. The contents were poison.&lt;/p&gt;

&lt;h2&gt;
  
  
  A signature proves the sender, not the contents
&lt;/h2&gt;

&lt;p&gt;I've spent a fair while &lt;a href="https://phpboyscout.uk/verifying-your-own-downloads/" rel="noopener noreferrer"&gt;building integrity and signing into my own tools&lt;/a&gt;, so this one stings a little. Signing is a trust mechanism, and a good one. It's how I prove a binary you downloaded was built and published by me and nobody else, and in a world with this many ways to be impersonated, that matters more than ever.&lt;/p&gt;

&lt;p&gt;But TanStack shows the limit in neon. If the pipeline doing the signing is itself compromised, the signature is still perfectly valid. It just now certifies a lie. Provenance answers "did this come from where it claims?" It does not answer "is what's inside safe?", and we have spent a few years quietly letting people treat those as the same question.&lt;/p&gt;

&lt;p&gt;They aren't. A signature is a promise about the &lt;em&gt;sender&lt;/em&gt;. The thing we actually need is a promise about the &lt;em&gt;contents&lt;/em&gt;: that whoever signed has done the diligence, the testing, the vetting, to vouch for what's in the tin. A signature without that behind it isn't a safety certificate. It's a tamper-proof seal on a poisoned jar.&lt;/p&gt;

&lt;h2&gt;
  
  
  It was never just npm
&lt;/h2&gt;

&lt;p&gt;It's tempting to file all this under "npm being npm". Resist it, because it's a category error. The thing that makes these attacks work, a stranger's code running on your machine as a side effect of installing or building, is not an npm bug. It's a near-universal design choice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ecosystem&lt;/th&gt;
&lt;th&gt;Untrusted code on install/build?&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;npm&lt;/strong&gt; (JS)&lt;/td&gt;
&lt;td&gt;Yes, at install (dependencies too)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pre&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;PyPI&lt;/strong&gt; (Python)&lt;/td&gt;
&lt;td&gt;sdist yes, wheel no&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;setup.py&lt;/code&gt;; wheels forbid hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RubyGems&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, at install&lt;/td&gt;
&lt;td&gt;native-extension build (&lt;code&gt;extconf.rb&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;cargo&lt;/strong&gt; (Rust)&lt;/td&gt;
&lt;td&gt;Yes, at build&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;build.rs&lt;/code&gt; and proc-macros&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Composer&lt;/strong&gt; (PHP)&lt;/td&gt;
&lt;td&gt;Dependencies: no&lt;/td&gt;
&lt;td&gt;only the &lt;em&gt;root&lt;/em&gt; project's scripts run, by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Maven/Gradle&lt;/strong&gt; (JVM)&lt;/td&gt;
&lt;td&gt;Yes, at build&lt;/td&gt;
&lt;td&gt;build scripts and plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;NuGet&lt;/strong&gt; (.NET)&lt;/td&gt;
&lt;td&gt;Modern: no&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;install.ps1&lt;/code&gt;, legacy format only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Go&lt;/strong&gt; (modules)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;no install or build hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Lifecycle hooks across ecosystems are catalogued at &lt;a href="https://github.com/ecosyste-ms/package-manager-hooks" rel="noopener noreferrer"&gt;ecosyste.ms&lt;/a&gt; if you want the receipts.)&lt;/p&gt;

&lt;p&gt;Read that and the lesson isn't "npm is uniquely bad", it's "this was a choice, and several ecosystems chose differently". Go runs no install or build hooks at all. PHP's Composer flatly refuses to run a dependency's scripts, only your own project's. Python's wheel format forbids install hooks. The hook was never inevitable.&lt;/p&gt;

&lt;p&gt;And yes, that includes my own back yard. cargo's &lt;code&gt;build.rs&lt;/code&gt; is the same gun fired at build time instead of install time, and the &lt;a href="https://socket.dev/blog/trapdoor-crypto-stealer-npm-pypi-crates" rel="noopener noreferrer"&gt;TrapDoor campaign&lt;/a&gt; used exactly that to rifle through keystores on crates.io this year. Rust isn't safe here. It's a smaller, better-policed target, which is a different thing, and I'd rather say so than pretend one of my favourite languages is above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  No registry can hand you a clean package
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable core. Not one of these registries can guarantee the package you pull is clean. They can sign it, scan it, attest its origin and mandate 2FA on maintainers, and they should do all of that. But none of it is a guarantee, because the failure modes are endless and attackers keep finding new ones. A maintainer account gets phished. A CI token leaks. A trusted contributor turns. A dependency four levels down quietly changes hands.&lt;/p&gt;

&lt;p&gt;So the onus lands, and will keep landing for a good while yet, on the consuming engineer. That isn't a comfortable answer or a clever one. It's the true one.&lt;/p&gt;

&lt;p&gt;And it's a genuinely rotten spot to stand in, because the advice contradicts itself. Patch slowly and you're scolded for running known-vulnerable dependencies. Patch the instant a release drops and you've skipped the bedding-in that might have caught a poisoned one. There's no setting on that dial that's safe, only trade-offs you have to actually think about. Add CI that leaks credentials it never needed, and a dependency tree thousands of strangers deep, and you can see why there's no single villain to blame and no single switch to flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring discipline that actually helps
&lt;/h2&gt;

&lt;p&gt;What's left isn't heroic, it's hygiene, and it's the boring, necessary stuff I keep banging on about. &lt;a href="https://phpboyscout.uk/openssf-scorecard-graded-my-supply-chain/" rel="noopener noreferrer"&gt;Pin your CI actions to commit SHAs&lt;/a&gt; so a moved tag can't swap code under you. Commit your lockfiles. Run the auditors, &lt;code&gt;cargo-audit&lt;/code&gt;, &lt;code&gt;pip-audit&lt;/code&gt;, &lt;code&gt;govulncheck&lt;/code&gt;, &lt;code&gt;npm audit&lt;/code&gt;, or Google's cross-ecosystem &lt;a href="https://github.com/google/osv-scanner" rel="noopener noreferrer"&gt;OSV-Scanner&lt;/a&gt;, on every build. Gate the dependency tree and &lt;a href="https://phpboyscout.uk/waivers-with-an-expiry-date/" rel="noopener noreferrer"&gt;give every exception an expiry date&lt;/a&gt; so "we'll deal with it later" can't quietly become "never". Keep the tree small: every crate you don't add is a stranger you don't have to trust.&lt;/p&gt;

&lt;p&gt;None of that is a solution. All of it is diligence, and diligence is the only thing that was ever going to stand behind the signature. When I sign a release, the cryptography is the easy part. The promise underneath it, that I pinned, locked, audited, vetted and tested before I put my name on it, is the part worth anything. That's the contract. The signature is just how I countersign it.&lt;/p&gt;

&lt;p&gt;The encouraging note is that the structural defences exist and they work. Go's checksum database and its refusal to run hooks. Composer declining to trust a dependency's scripts. Python's wheels. &lt;code&gt;cargo-vet&lt;/code&gt; and &lt;code&gt;cargo-deny&lt;/code&gt; giving you somewhere to record human judgement at scale. More ecosystems should steal these shamelessly, because a registry that makes the safe path the &lt;em&gt;default&lt;/em&gt; does the working engineer a far bigger favour than one that leaves it all to discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, a third time
&lt;/h2&gt;

&lt;p&gt;If this feels familiar, it should. I wrote recently about &lt;a href="https://phpboyscout.uk/ai-didnt-kill-curls-bug-bounty/" rel="noopener noreferrer"&gt;a bug bounty that collapsed because the cost of slop was deferred&lt;/a&gt;, and about &lt;a href="https://phpboyscout.uk/the-greybeards-edge-was-never-typing/" rel="noopener noreferrer"&gt;a junior pipeline being cut because the bill lands years later&lt;/a&gt;. Supply-chain security is the same shape a third time. The convenience is now, the catastrophe is later, and the only thing standing in the gap is an engineer paying attention, doing the dull work, refusing to be rushed into trusting something they haven't checked.&lt;/p&gt;

&lt;p&gt;There is no clean package waiting to be found, no registry about to solve this for us, no signature that means "safe" all on its own. There's the diligence you do before you put your name to something, and the judgement to know when an install is asking you to trust more than you should. For a good while yet, that is the whole job. Boring, unfashionable, and the only thing that works.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/nobody-is-coming-to-clean-your-supply-chain/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 29 May 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>supplychain</category>
      <category>dependencies</category>
      <category>packagemanagers</category>
    </item>
    <item>
      <title>The off-switch was never a button</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Mon, 14 Sep 2026 06:54:21 +0000</pubDate>
      <link>https://dev.to/phpboyscout/the-off-switch-was-never-a-button-4lb</link>
      <guid>https://dev.to/phpboyscout/the-off-switch-was-never-a-button-4lb</guid>
      <description>&lt;p&gt;Last night, while I was asleep, an AI agent spent the better part of eight hours writing code in one of my repositories. It pulled a task off a spec, wrote the code, ran the tests, and left a merge request with my name on it, waiting for me to read over coffee.&lt;/p&gt;

&lt;p&gt;If that makes you reach for the word "reckless", I understand. Eighteen months ago I'd have been right there with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  I came to this a sceptic
&lt;/h2&gt;

&lt;p&gt;For a long time I didn't have the faith in these models that a lot of my peers did. Every time I went near AI-generated code it was a bit sketchy, or it looked like a StackOverflow copy-paste that had wandered in off the street, or it just plain didn't do what it said on the tin. So I filed it under "assistant", handy for the boilerplate I couldn't be bothered to type, and even then I usually reached for my own tooling instead (go-tool-base is just the latest version of that instinct). The one place I happily let it off the leash was my Dungeons &amp;amp; Dragons prep, because when there's a table of legendary heroes-in-the-making in front of you, facts and reality are already fairly negotiable.&lt;/p&gt;

&lt;p&gt;And then, somewhere in the last year, it changed. The models got better. Almost too good, to the untrained eye! I watched them improve, month on month, until the lure was enough to make me spend real time with a spread of tools and models from different providers. I was taken aback by how quickly they became part of how I actually work. I run an AI agent every day now, and there's always at least one thing brewing in the pot.&lt;/p&gt;

&lt;p&gt;So I'm not here as a sceptic. I'm an advocate who uses this stuff in anger. Which is exactly why the next bit needs saying.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Golden Retriever with a keyboard
&lt;/h2&gt;

&lt;p&gt;Even now, with all the progress, there are still moments where I look at what an agent has handed me and put my face in my hands. Sometimes it's copied the same block of code into fifteen files instead of reaching for the obvious abstraction. Sometimes it has started bang on the brief and then, for reasons known only to itself, wandered off and built something on a completely different tangent.&lt;/p&gt;

&lt;p&gt;Here's the most useful way I've found to think about it. An AI agent is a Golden Retriever playing fetch. It will bring the ball back all day long, joyfully, tirelessly, for exactly as long as there isn't a more interesting smell in the next field. It has no loyalty beyond what we've trained into it, and like any good dog it desperately wants to be told it's a good boy, even if being a good boy today means shredding the sofa cushions because yesterday I stubbed my toe on the sofa and swore at it. (The sofa, not the dog.)&lt;/p&gt;

&lt;p&gt;It is, in other words, fallible. Just like us. The Romans had a line for it: &lt;em&gt;cuiusvis hominis est errare; nullius nisi insipientis in errore perseverare&lt;/em&gt;. Anyone can make a mistake, but only a fool persists in it. It's the second clause an agent hasn't learned yet. It will make an error and then, with great enthusiasm, build on top of it, because nothing in it feels that anything is wrong. All it has is the input we gave it, usually some text, maybe the odd picture. It doesn't have the empathy to work out what we actually meant, and it doesn't know when it's gone too far, because we never told it where "too far" was.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Agents that work while you sleep"
&lt;/h2&gt;

&lt;p&gt;This is the part the brochure skips.&lt;/p&gt;

&lt;p&gt;Open any vendor deck in 2026 and you'll find the same promise: agents that work while you sleep, agents that merge while your team sleeps, autonomy as the headline feature. The industry's answer to the obvious worry is the kill switch. Okta now sells one that "instantly revokes an agent's access if it goes rogue", and its CEO says every agent needs one. &lt;a href="https://www.theregister.com/ai-ml/2026/05/29/okta-writes-its-own-license-to-kill-rogue-ai-agents/5248766" rel="noopener noreferrer"&gt;The Register put it plainly&lt;/a&gt;: Okta wrote its own licence to kill rogue AI agents. Gartner, meanwhile, &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;reckons more than 40% of agentic projects will be scrapped by the end of 2027&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, this might sound contrarian coming from someone who runs these things daily, but I don't think most of that is the agents going rogue. I think it's teething. Read Gartner's own reasons and there isn't a rebellious machine in sight: escalating cost, unclear value, inadequate risk controls. Read the horror stories and most of them are the same story, a powerful, eager tool handed to people who hadn't worked out how to fence it.&lt;/p&gt;

&lt;p&gt;I've made this argument in miniature before. When I built a little AI dungeon master and it kept refereeing its own dice rolls, &lt;a href="https://phpboyscout.uk/the-goblin-that-wouldnt-stay-dead/" rel="noopener noreferrer"&gt;the model never once misbehaved&lt;/a&gt;; every failure was a permission I'd handed it without meaning to. Scale that up from a toy at the gaming table to an agent holding your shell and your credit card, and the stakes change beyond recognition. The lesson doesn't.&lt;/p&gt;

&lt;p&gt;Look at OpenClaw. A weekend project by &lt;a href="https://venturebeat.com/security/openclaw-agentic-ai-security-risk-ciso-guide" rel="noopener noreferrer"&gt;Peter Steinberger&lt;/a&gt; that became the fastest-growing open-source project GitHub has ever seen: an autonomous agent that lives in your chat apps and runs shell commands on your behalf. People wired it into their systems, their code, in some cases their credit cards, then hosted it around the clock and walked away. The result was a security crisis you could see from space. A one-click exploit that worked even on a machine bound to localhost. A community plug-in marketplace where hundreds of "skills" turned out to be siphoning crypto wallets while their owners slept. Tens of thousands of instances left wide open on the public internet, leaking keys.&lt;/p&gt;

&lt;p&gt;The one that sticks with me is smaller and sharper. Summer Yue, a director of alignment at Meta's superintelligence lab, of all people, had told her OpenClaw agent to confirm before doing anything destructive. It started speed-running the deletion of her inbox anyway. She &lt;a href="https://techcrunch.com/2026/02/23/a-meta-ai-security-researcher-said-an-openclaw-agent-ran-amok-on-her-inbox/" rel="noopener noreferrer"&gt;typed STOP into her phone and it ignored her&lt;/a&gt;, so she had to physically run to her Mac mini, in her own words, "like I was defusing a bomb". And here's the forensic detail that matters: the agent hadn't defied her. Her "confirm first" rule had been sitting in the conversation's short-term memory, and when the context filled up, it got summarised away. It didn't rebel. It forgot.&lt;/p&gt;

&lt;p&gt;That is not a story about a rogue agent that needed a kill switch. It's a story about a guardrail that wasn't built to survive contact, on a tool that had been handed god-mode over someone's data. By the time she lunged for the off-button, the damage was already running. The off-button was never going to save her.&lt;/p&gt;

&lt;h2&gt;
  
  
  The off-switch was never a button
&lt;/h2&gt;

&lt;p&gt;Here's what the kill-switch crowd has the wrong way round. If you ever find yourself slamming the emergency stop, the failure has already happened, and it happened upstream, long before the agent started typing.&lt;/p&gt;

&lt;p&gt;So yes, I let my agents run unattended, sometimes for eight hours at a stretch if the task is meaty enough and I need to sleep. But never naked. Every agent I set loose runs inside a safety net I've put real effort into building, at every single touchpoint it can reach: my prompts, my local development environment, my CI stack, my version control. The agent that declared a job done before it had run the linter, which I &lt;a href="https://phpboyscout.uk/the-agent-said-success-the-linter-disagreed/" rel="noopener noreferrer"&gt;wrote about&lt;/a&gt;, is exactly the kind of gap those layers exist to catch. And it never, ever gets my host: an unattended agent works in an isolated tree, for the same reason I &lt;a href="https://phpboyscout.uk/the-interpreter-we-forgot-to-sandbox/" rel="noopener noreferrer"&gt;keep the interpreter sandboxed&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The work that actually keeps it safe happens before the leash ever comes off. Every unattended task starts as a full spec with detailed instructions, and before the agent goes anywhere I sit down with it and we walk the spec together. I get it to challenge my choices, poke at the open questions and the ambiguous bits, and I challenge its reading right back. The spec names the testing strategy it has to follow, TDD, BDD, UAT, whatever fits, and passing it is a precondition of the job being finished at all. Only when I'm satisfied there's enough real detail to keep it on the ball do I let go.&lt;/p&gt;

&lt;p&gt;And the end of the line is always the same: a merge request, with my name on it, waiting for me when I get back to my desk. I read it. Not perfectly, I'm only human, but enough to accept the state of the code and whatever support burden it lands me with later. That the review is mine, and the blame for whatever ships is mine and not the agent's, I've &lt;a href="https://phpboyscout.uk/bought-not-stolen/" rel="noopener noreferrer"&gt;argued at length elsewhere&lt;/a&gt; and won't go over it all again here. The point worth adding is this: that review, the off-button's respectable cousin, is the cheap part. By the time there's an MR to read, the safety has already been won or lost upstream, in the spec and the rails. The review is where you confirm it, not where you create it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It gets harder as it gets better, not easier
&lt;/h2&gt;

&lt;p&gt;My setup isn't perfect, and I'm still learning. Everyone is; the AI is going to be in obedience lessons for a good while yet. But the direction is clear, and there's a trap buried in it worth naming out loud.&lt;/p&gt;

&lt;p&gt;The danger doesn't shrink as the models improve. It grows. The better the output looks, the more tempting it is to stop reading it, and the untrained eye genuinely cannot tell the difference between code that is good and code that merely looks good. That gap, between looking right and being right, is precisely where a tired person at 1am stops checking. The discipline matters more the better these things get, not less.&lt;/p&gt;

&lt;p&gt;It's also why the kill switch is no answer. A button you smash in a panic assumes you're still watching closely enough to smash it, right at the point the agent's been good for long enough that you've stopped watching it that closely. The emergency stop asks the most of you at the exact moment you're least likely to be there for it.&lt;/p&gt;

&lt;p&gt;So no, I don't lie awake worrying that the thing working in my repo overnight is going to turn on me. A Golden Retriever doesn't go rogue. It does exactly what you trained it to do, in exactly the yard you fenced, and it brings back exactly the ball you threw. The off-switch was never a button. It's the spec you wrote before you let go of the leash, the rails you laid at every turn, and your name on what it carries home. If you're scrambling for the button, you already skipped the part that mattered.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/the-off-switch-was-never-a-button/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 2 July 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Building it yourself is the third thing I try</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Mon, 07 Sep 2026 01:43:00 +0000</pubDate>
      <link>https://dev.to/phpboyscout/building-it-yourself-is-the-third-thing-i-try-1n7</link>
      <guid>https://dev.to/phpboyscout/building-it-yourself-is-the-third-thing-i-try-1n7</guid>
      <description>&lt;p&gt;At ten to nine one Thursday morning I had a new skill working, built out of the strongest idea in somebody else's library. At three minutes past ten I deleted it.&lt;/p&gt;

&lt;p&gt;Seventy-one minutes. Long enough to build the thing properly and find out it was wrong, which is not the same as looking at it and deciding I didn't fancy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friction is the only thing that starts any of this
&lt;/h2&gt;

&lt;p&gt;Nothing here gets replaced because I woke up wanting to replace it. Every single time, it's friction. Something rubs, and keeps rubbing, in a way I can't talk myself out of noticing.&lt;/p&gt;

&lt;p&gt;That matters, because anyone with a repository full of their own modules gets accused of not-invented-here sooner or later, and it's a fair thing to accuse somebody of. So here is the order I actually work in.&lt;/p&gt;

&lt;h2&gt;
  
  
  One: use the thing
&lt;/h2&gt;

&lt;p&gt;If a tool does what I need, I use it. Most of them do, and most of these decisions end right there, as one more line in a &lt;code&gt;go.mod&lt;/code&gt; that nobody ever thinks about again. There's no post in that and nobody writes one, which is exactly right. But leave it out of the telling and you start to sound like a man who rebuilds everything on principle.&lt;/p&gt;

&lt;p&gt;The ideas matter, always. The implementation is kinda incidental, and mostly somebody else's implementation is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two: try to fix it
&lt;/h2&gt;

&lt;p&gt;When it nearly fits, the next move is upstream, and I've had both outcomes inside the same fortnight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/disgoorg/disgo" rel="noopener noreferrer"&gt;disgo&lt;/a&gt;, the Go Discord library, took &lt;a href="https://github.com/disgoorg/disgo/pull/594" rel="noopener noreferrer"&gt;a fix for RTP padding detection&lt;/a&gt; two days after I sent it. Then it &lt;em&gt;didn't&lt;/em&gt; take the next one: I proposed returning an error rather than dereferencing a nil UDP connection, and eight comments later I'd been argued out of my own approach and came back with &lt;a href="https://github.com/disgoorg/disgo/pull/604" rel="noopener noreferrer"&gt;a different patch&lt;/a&gt; that's still open. That's rung two working exactly as advertised. Nobody owes me a merge, and being talked out of a fix by somebody who knows the codebase better than I do is a good afternoon's work as far as I'm concerned.&lt;/p&gt;

&lt;p&gt;Losing an argument you started is fine. Losing it to someone who's right is better.&lt;/p&gt;

&lt;p&gt;The other outcome looks like this. I sent releaser-pleaser &lt;a href="https://github.com/apricote/releaser-pleaser/pull/462" rel="noopener noreferrer"&gt;a GitLab fix&lt;/a&gt; on the first of August, for release commits after an automatic rebase. It's still open, with no comment on it, on a repository that has merged fifteen commits since... all of them dependency bumps of the sort that merge themselves.&lt;/p&gt;

&lt;p&gt;I'm not going to draw a conclusion from that and neither should you. Maintainers have lives, bots merge themselves, and a drive-by fix for one forge from a stranger who turns up out of nowhere is a genuinely awkward thing to land on somebody. Nobody did anything wrong here.&lt;/p&gt;

&lt;p&gt;What it shows is that rung two has a failure mode you can't spot from the outside, and the only way you find out which one you've got is to wait. Which is a rotten thing to have to admit about a step you're recommending... and it's still the right step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three: build it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://colophon.phpboyscout.uk" rel="noopener noreferrer"&gt;colophon&lt;/a&gt; exists because of exactly that. Not because I reckoned I could do release tooling better than the people already doing it, but because the fix I needed had nowhere to go and I still needed the fix.&lt;/p&gt;

&lt;p&gt;Rung three is expensive and it is permanent. Everything you build is a thing you maintain until you die, and the estate is (quite typically) already stuffed with things I have signed myself up to maintain until I die.&lt;/p&gt;

&lt;p&gt;Nobody makes you do that. You just keep doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seventy-one minutes
&lt;/h2&gt;

&lt;p&gt;Back to that Thursday, because it's the case where all of this is visible.&lt;/p&gt;

&lt;p&gt;I'd been putting a lot of hours into researching skills, watching people's videos, hunting for anything that would narrow the gaps in my own workflow. That led me to &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's skills library&lt;/a&gt;, which is phenomenally good, and to &lt;code&gt;wayfinder&lt;/code&gt; in particular, which decomposes work into a tree of forge tickets. Genuinely clever design. I built our version of it at ten to nine.&lt;/p&gt;

&lt;p&gt;Three things killed it, and only two of them are defensible.&lt;/p&gt;

&lt;p&gt;The first is a product argument rather than a taste one. Wayfinder assumes your issue tracker is &lt;em&gt;yours&lt;/em&gt;. Mine aren't. They're a public front door, the place a stranger turns up to tell me something is broken, and filling that with hundreds of internal planning tickets wrecks it for the people it's actually there to serve.&lt;/p&gt;

&lt;p&gt;The second is money, and I'd rather say so than dress it up. The whole value of the thing is what it calls the frontier: the handful of tickets you could genuinely pick up today, because nothing else is standing in front of them. To work that out, the tracker has to know which tickets block which. On GitLab Free it doesn't, because those blocking relationships come back 403 without a paid licence. So the most useful view in the tool is precisely the one my tier won't draw.&lt;/p&gt;

&lt;p&gt;The third is that it didn't fit how I think about my own work. That's much harder to defend than the other two and I'm keeping it in, because a post that only gives you the defensible reasons is a post that's tidied itself up on the way out.&lt;/p&gt;

&lt;p&gt;The question underneath all three, and it's the one worth stealing: &lt;em&gt;will this improve my workflow, or just add to it?&lt;/em&gt; Four words, and the answer is more often "add to" than anybody wants to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning something down is not the same as throwing it away
&lt;/h2&gt;

&lt;p&gt;I didn't take wayfinder. I took five separate ideas out of it.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Destination&lt;/strong&gt; at the top of a spec, saying where the thing is meant to end up. A &lt;strong&gt;Not yet specified&lt;/strong&gt; section for the parts you already know are missing, rather than leaving a reader to notice the hole themselves. The idea of &lt;strong&gt;fog&lt;/strong&gt;, and the rather good test that tells fog apart from an ordinary open question: can you state the question precisely, right now? If you can, it isn't fog, it's a question, and you should go and answer it. &lt;strong&gt;Out of scope&lt;/strong&gt; as a permanent verdict rather than a to-do, so something you have decided not to build stays decided instead of creeping back onto the list six weeks later.&lt;/p&gt;

&lt;p&gt;And, best of all, naming a decision instead of numbering it, so a spec reads as words rather than a wall of &lt;code&gt;D1&lt;/code&gt; and &lt;code&gt;2c.2&lt;/code&gt; and &lt;code&gt;OQ1&lt;/code&gt;. When you're juggling a dozen projects and switching between them all day, an identifier you have to decode is a small tax you pay a few hundred times.&lt;/p&gt;

&lt;p&gt;All of that went into our own spec skill and none of it is wayfinder. The marketplace's credits file lists twelve of these now, across two sources.&lt;/p&gt;

&lt;p&gt;The bit I like best is what happened at 10:03. The revert and the replacement went in together. &lt;code&gt;programme-tracker&lt;/code&gt;, one wiki page per project saying what's in flight, landed in the same batch as the deletion, and I did not have it in my back pocket beforehand. It only became obvious once I could say out loud precisely what hadn't fitted. Rung three isn't a plan you keep in your back pocket. It turns up after you've done the work of saying exactly what was wrong with the thing you turned down... which is another good reason not to jump straight to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  And then it stops being about taste
&lt;/h2&gt;

&lt;p&gt;I could have written all of the above as preference, and for a long time that's how I'd have defended it. Then I wrote the credits file and noticed the actual argument.&lt;/p&gt;

&lt;p&gt;A skill file is not a library. It's instruction text that an agent reads and executes. Every one in our marketplace gets scanned for hidden characters and injection patterns before it lands, and a plugin installed from somewhere else walks straight past that gate, and then changes whenever its author changes a branch, with nothing in my repository recording that it moved.&lt;/p&gt;

&lt;p&gt;Adopting the idea and owning the text keeps the whole trust boundary inside one place I control. That's not a preference. Shared skills are not one size fits all anyway, so you're going to be adapting them regardless; you may as well own what you're running.&lt;/p&gt;

&lt;p&gt;The other half of that bargain is saying where it came from. Every derived skill carries an &lt;code&gt;Adapted from&lt;/code&gt; line, and the credits file records the source, the author, the licence and the exact commit it was reviewed at. Pocock's work is MIT, so none of that is required of me. Which makes it matter more, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it leaves me
&lt;/h2&gt;

&lt;p&gt;Three rungs, in order, and I climb them slowly because skipping to the top is how an estate fills up with things nobody else will ever fix for you.&lt;/p&gt;

&lt;p&gt;Wayfinder was a good tool that was wrong for me, and I only know that because I built it and lived with it for an hour rather than reading the README and having an opinion. Those seventy-one minutes were the cheapest thing in the whole exercise.&lt;/p&gt;

&lt;p&gt;The rubbing stopped, anyway. That's the only test I've got.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/building-it-yourself-is-the-third-thing-i-try/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 7 September 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>craft</category>
      <category>process</category>
    </item>
    <item>
      <title>Everyone wants Rust's safety, nobody wants Rust</title>
      <dc:creator>Matt Cockayne</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:53:28 +0000</pubDate>
      <link>https://dev.to/phpboyscout/everyone-wants-rusts-safety-nobody-wants-rust-2em9</link>
      <guid>https://dev.to/phpboyscout/everyone-wants-rusts-safety-nobody-wants-rust-2em9</guid>
      <description>&lt;p&gt;This spring, the better part of a million lines of Zig quietly became a million lines of Rust. Bun, the JavaScript runtime that was the showcase for "you don't need a borrow checker, you need good tools and a steady hand", looked at its own memory bugs and switched teams. Around &lt;a href="https://www.techzine.eu/news/devops/141364/bun-takes-a-surprising-step-from-zig-to-rust/" rel="noopener noreferrer"&gt;99.8% of its test suite passed&lt;/a&gt; on the rewritten code, a clutch of memory leaks closed in the move, and the maintainers said the quiet part out loud: the previous release would be the last one written in Zig.&lt;/p&gt;

&lt;p&gt;It's tempting to read that as Rust winning, hoist the flag, and move on. I don't think that's quite the story, and the more interesting one is happening everywhere else at the same time.&lt;/p&gt;

&lt;p&gt;Because Bun is the exception that went all the way. Everyone else is trying to get the &lt;em&gt;safety&lt;/em&gt; without the &lt;em&gt;Rust&lt;/em&gt;, and watching how they're going about it tells you more than one runtime's heroic rewrite does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What everyone's actually after
&lt;/h2&gt;

&lt;p&gt;A quick level-set, because not everyone reading this writes systems code daily. "Memory safety" is the property that a program can't read or write memory it has no business touching: no using a value after you've freed it, no running off the end of an array. It sounds niche. It is, by most counts, behind something like &lt;a href="https://www.kusari.dev/blog/rust-wont-fix-everything-moving-toward-a-memory-safe-future" rel="noopener noreferrer"&gt;70% of serious security vulnerabilities&lt;/a&gt;, which is why governments and trillion-dollar companies suddenly care a great deal.&lt;/p&gt;

&lt;p&gt;There are roughly three ways to get it. Rust uses a &lt;em&gt;borrow checker&lt;/em&gt;: a compiler that flatly refuses to build your program unless it can prove, before it ever runs, that you never touch memory after you're done with it. The price is that it argues with you the entire time you're writing. The product is that an entire category of bug becomes literally unwriteable. Go (and most managed languages) uses a &lt;em&gt;garbage collector&lt;/em&gt;: a runtime janitor that frees memory for you, so you mostly can't get it wrong, at the cost of some overhead and a little control. And then there's the old way, the one most code on Earth still uses: trust the developer to get it right, and add an &lt;em&gt;escape hatch&lt;/em&gt;, usually a keyword like &lt;code&gt;unsafe&lt;/code&gt;, for the bits where they promise they have.&lt;/p&gt;

&lt;p&gt;The retrofit trend is everyone in that third camp trying to inch toward the first two without rewriting the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust didn't invent any of this
&lt;/h2&gt;

&lt;p&gt;Worth saying plainly, because the fan club rarely does: Rust invented almost none of it. The borrow checker is, &lt;a href="https://doc.rust-lang.org/reference/influences.html" rel="noopener noreferrer"&gt;by Rust's own admission&lt;/a&gt;, Cyclone's region-based memory management, from a safe-C experiment in the early 2000s, welded to &lt;a href="https://borretti.me/article/type-systems-memory-safety" rel="noopener noreferrer"&gt;affine types out of linear logic&lt;/a&gt;, ideas that predate Rust by decades. And it goes beyond the borrow checker. Rust's exhaustive pattern matching came from ML and Haskell. Its "errors are values, and there is no null" approach, &lt;code&gt;Result&lt;/code&gt; and &lt;code&gt;Option&lt;/code&gt;, is Haskell's Maybe and Either in work boots.&lt;/p&gt;

&lt;p&gt;What Rust did, and did better than anyone before it, was taste and integration: it curated thirty-odd years of academic research into one coherent language and proved the ideas could carry real systems code rather than just research papers. That is the genuine USP, and it's why the rest of the industry is now shopping from the same shelf. Pattern matching has landed in Python and Java, with a proposal in flight for C++26. Swift 6 shipped &lt;a href="https://www.infoworld.com/article/3529619/swift-6-arrives-with-improved-concurrency-data-race-safety.html" rel="noopener noreferrer"&gt;compile-time data-race safety&lt;/a&gt;, its &lt;code&gt;Sendable&lt;/code&gt; machinery a close cousin of Rust's &lt;code&gt;Send&lt;/code&gt; and &lt;code&gt;Sync&lt;/code&gt;. The borrow checker just gets the headlines because it's the hardest bit to copy. Which makes the title almost too literal: everyone wants Rust's safety, and they are quietly adopting its mechanisms one feature at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit where it's due: C# is doing this properly
&lt;/h2&gt;

&lt;p&gt;The example that made me sit up is C#. In C# 16, Microsoft is &lt;a href="https://devblogs.microsoft.com/dotnet/improving-csharp-memory-safety/" rel="noopener noreferrer"&gt;redefining the &lt;code&gt;unsafe&lt;/code&gt; keyword&lt;/a&gt; that's been in the language since version one. Instead of &lt;code&gt;unsafe&lt;/code&gt; marking a lump of syntax, it now marks a &lt;em&gt;contract&lt;/em&gt;: a promise the compiler can't verify and a human has to read and uphold, with documentation and static analysers nudging you to take it seriously. They're even floating badges on NuGet packages to show which ones have opted in.&lt;/p&gt;

&lt;p&gt;My first instinct with any retrofit is suspicion, because bolting safety onto a language after the fact has a long and miserable history, and an escape hatch that's easy to reach is an escape hatch people will reach for the moment they're in a hurry. But this isn't a bolt-on. Taking the keyword that's already there and giving it real teeth is working &lt;em&gt;with&lt;/em&gt; the grain of the language instead of stapling a second safety system alongside the first. That's honest engineering, and it deserves the credit. It genuinely raises the floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A contract is not a guarantee
&lt;/h2&gt;

&lt;p&gt;Here's where my enthusiasm meets its limit, and it's a distinction I happen to have &lt;a href="https://phpboyscout.uk/forbid-means-forbid-until-linkme-needs-a-word/" rel="noopener noreferrer"&gt;a lot of skin in&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;C#'s redefined &lt;code&gt;unsafe&lt;/code&gt; makes dangerous code &lt;em&gt;visible and reviewable&lt;/em&gt;. That is a real improvement, and most teams would be better off for it. But visible and reviewable still means a human has to honour the promise. It's a sign on the door. Rust's equivalent is a wall: in &lt;a href="https://phpboyscout.uk/rust-tool-base-the-same-idea/" rel="noopener noreferrer"&gt;rust-tool-base&lt;/a&gt; I put &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt; at the top of all eleven shipping crates, and &lt;code&gt;forbid&lt;/code&gt; is not advice, it's a refusal. The compiler will not build a crate that contains &lt;code&gt;unsafe&lt;/code&gt;, full stop, and unlike its softer sibling &lt;code&gt;deny&lt;/code&gt;, you can't quietly switch it back off in a corner of the code where it's inconvenient. The whole reason I use &lt;code&gt;forbid&lt;/code&gt; and not &lt;code&gt;deny&lt;/code&gt; is that I don't trust future-me, in a hurry, not to reach for the hatch.&lt;/p&gt;

&lt;p&gt;So when I look at the C# work I think: good, genuinely good, and they should take it further. A contract a human upholds is not the same kind of thing as a proof a compiler enforces, and the trend, if it's serious, points at enforcement. Visible is better than invisible. Impossible is better than visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discipline never scaled, and that's not an insult
&lt;/h2&gt;

&lt;p&gt;The objection I keep hearing, and that a younger me would have made, is that any language can be memory-safe if you're just disciplined enough. And it's true, in the way that any house can be tidy if you never get busy. In the before times we shipped memory-safe C with code review and valgrind and sheer bloody-mindedness, and it worked, sort of, at small scale.&lt;/p&gt;

&lt;p&gt;It doesn't scale, and Bun is the proof sitting on the table. That wasn't a sloppy team learning the basics. It was a strong team, betting publicly on the discipline-and-good-tools model, and the memory bugs piled up anyway until the honest move was to let a compiler take the job. Discipline failing at scale isn't a moral failure of the engineers. It's just what happens when you ask humans to hold a thousand invariants in their heads across a million lines. Delegating that to a machine that never gets tired or rushed isn't laziness. It's the entire point of having compilers at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that changed my mind
&lt;/h2&gt;

&lt;p&gt;I learned most of my Rust by building rust-tool-base with an AI alongside me, leaning on it to explain the borrow checker, suggest the idiomatic shape, and check my work. And somewhere in that I noticed the thing I now can't unsee: &lt;strong&gt;the borrow checker is exactly as good a guardrail for the AI as it is for me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A model, like a tired human, will write a confident use-after-free without blinking. In Rust it simply doesn't compile, so the mistake never reaches me. What that does is move the whole error surface. The bugs that survive into review aren't memory bugs or lifetime bugs or data races, the language has eaten those, they're errors of &lt;em&gt;logic&lt;/em&gt;: the code is safe and wrong. And logic is precisely where I want my attention, and the AI's, because it's the part a human has to own and the part the models are getting better at every month. (I split my AI work across a few providers for their different strengths, so this is not a pitch for anyone's logo. The effect is the same whoever's doing the typing.)&lt;/p&gt;

&lt;p&gt;Which dissolves the one argument that ever really kept people out of Rust. "The borrow checker is too much friction" was always the case for the defence. But Bun's million-line rewrite was done largely &lt;em&gt;with&lt;/em&gt; an AI, because an AI is very good at paying a tax that is tedious and mechanical rather than creative. The friction is getting cheaper to pay at exactly the moment the guarantee is getting more valuable to have. In an AI-assisted world, a language that &lt;em&gt;proves&lt;/em&gt; safety is worth more, not less, because it fences in the machine's mistakes as firmly as your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  None of this means rewrite everything in Rust
&lt;/h2&gt;

&lt;p&gt;I want to be careful not to land somewhere smug, because most software does not need what Rust offers and pretending otherwise is how you end up rewriting a CRUD app nobody asked you to. Garbage collection is not a failure state. Go's collector keeps getting &lt;a href="https://go.dev/doc/go1.26" rel="noopener noreferrer"&gt;meaningfully better&lt;/a&gt;, my own go-tool-base is GC'd top to bottom and I have never once wished it weren't, and "safe-by-default with a GC" is the right answer for a vast amount of the work most of us do. The borrow checker is a price, and you should only pay it when the thing you're buying, that last class of guarantee with no runtime cost, is something your stakes actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it comes down to
&lt;/h2&gt;

&lt;p&gt;The question was never "is it as safe as Rust". That framing turns everything into a loss for everyone who isn't Rust, which is silly. The useful question is: &lt;em&gt;what does your language make the default, and how hard does it make the escape hatch to reach?&lt;/em&gt; Go makes safety the default and charges you a GC. Rust makes it the default and charges you the borrow checker. C# is moving its default in the right direction and, for now, leaves the hatch as a promise rather than a wall.&lt;/p&gt;

&lt;p&gt;Credit the retrofits, they are raising the floor for an enormous amount of code that was never going to be rewritten. Just don't mistake the floor for the ceiling, or a contract a human signs for a guarantee a compiler keeps. Everyone wants Rust's safety, and the interesting question, now that an AI will pay the toll for you, is who still has a reason not to want it.&lt;/p&gt;

&lt;p&gt;Widen the lens past Rust, though, because that's where the news gets genuinely good. We're at a turn in how languages evolve. Compile-time rigour is spreading rather than retreating: borrow checking is reaching the Python family through Mojo, static typing long since conquered JavaScript, and even the managed languages are turning their escape hatches into something you have to argue with. More of our safety is quietly moving from "remember to" into "can't not". And the one thing that always made the strict path hard to start down, the friction, is being absorbed by an AI that will happily learn the rules so you can lean on them. I've been at this long enough to distrust a rosy forecast, but I'll put my name to this one: the outlook for software that's safe and secure by default has never looked better.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://phpboyscout.uk/everyone-wants-rusts-safety-nobody-wants-rust/" rel="noopener noreferrer"&gt;phpboyscout.uk&lt;/a&gt; on 14 June 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>rust</category>
      <category>ai</category>
      <category>memorysafety</category>
    </item>
  </channel>
</rss>
