<?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: Sho Naka</title>
    <description>The latest articles on DEV Community by Sho Naka (@nomurasan).</description>
    <link>https://dev.to/nomurasan</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%2F3987494%2Fa151ecf0-1dfb-44a7-91d6-5bafe84ecdb8.jpg</url>
      <title>DEV Community: Sho Naka</title>
      <link>https://dev.to/nomurasan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nomurasan"/>
    <language>en</language>
    <item>
      <title>I Almost Built a VSCode Extension. Four GitHub Searches Changed My Mind.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Fri, 17 Jul 2026 14:42:48 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-almost-built-a-vscode-extension-four-github-searches-changed-my-mind-2033</link>
      <guid>https://dev.to/nomurasan/i-almost-built-a-vscode-extension-four-github-searches-changed-my-mind-2033</guid>
      <description>&lt;p&gt;I didn't build a VSCode extension for Herdr. About 1 week after finishing the install, the idea came up on its own — and before writing a line of code, I checked whether anyone, including the maintainer, had ever planned one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Before you build a companion extension for a CLI tool you just installed, search its own GitHub Issues, Discussions, and Releases for "extension" and "editor integration," then search the relevant extension marketplace(s) for the tool's name. If the plan doesn't exist anywhere &lt;em&gt;and&lt;/em&gt; an equivalent capability already exists in a tool you already run, you have your answer without writing a line of code.&lt;/p&gt;

&lt;p&gt;For Herdr specifically: no such plan existed on GitHub, an equivalent capability already existed elsewhere (Claude Code's own VSCode extension, Cursor's Cloud Agents), and building one would have pointed Herdr's value in the opposite direction from the reason I installed it. The verdict was SKIP — and I wrote down why, so I don't re-litigate the same question in three months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I already had running
&lt;/h2&gt;

&lt;p&gt;Herdr (&lt;code&gt;github.com/ogulcancelik/herdr&lt;/code&gt;), an open-source (OSS) Rust CLI built by developer ogulcancelik, multiplexes AI coding agents — Claude Code, Codex, and others — the way tmux, a terminal multiplexer, multiplexes shells, except it also knows what state each agent is in. A background server keeps agents alive after you detach; process-name matching plus output heuristics detect whether an agent is working, finished, or waiting on you; a socket API and remote mode let external scripts read and drive that state. As of this writing it lists 21 supported agent runners, and most of them need zero configuration — you start the agent in a pane and Herdr picks it up.&lt;/p&gt;

&lt;p&gt;I installed it through Homebrew's stable channel rather than building from source — new tools go into my daily workflow only after the stable release has had time to prove itself, not the dev branch. I confirmed the Claude Code and Codex integrations, placed the official Skill, and ran an independent check rather than trusting "it looks like it's working" — the check came back PASS, not a guess. Then I collapsed the daily startup ritual — attach to a session, split panes, start each agent by hand — into a single command that boots the workspace and the first agent in one action. Small thing, but it's a command I run many times a day, so the saved motion is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The natural next question: build a VSCode extension?
&lt;/h2&gt;

&lt;p&gt;Once Herdr was running, the obvious next idea showed up: most of my actual coding happens inside an editor, so wouldn't it be nice to see Herdr's per-agent state in a sidebar instead of a terminal?&lt;/p&gt;

&lt;p&gt;That's not a bad instinct. Wanting to use a new tool harder is usually how you find its real edges. But a VSCode extension isn't a weekend artifact — Herdr is young and its interfaces are still moving, so an extension built against it today inherits an open-ended maintenance obligation: track every upstream change, forever, for a convenience I hadn't yet confirmed I needed. "This would be nice to have" is not the same claim as "this is worth an ongoing maintenance commitment," and I wanted to check the first claim against reality before accepting the second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What checking the primary sources actually showed
&lt;/h2&gt;

&lt;p&gt;I checked four independent surfaces before designing anything: Herdr's own GitHub Issues, Discussions, Releases, and the extension marketplaces a VSCode-compatible editor would actually use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;source&lt;/th&gt;
&lt;th&gt;what I searched for&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Issues (74 total)&lt;/td&gt;
&lt;td&gt;"vscode", "vscode extension", "editor extension"&lt;/td&gt;
&lt;td&gt;Not a single Issue, open or closed, discusses a Herdr-authored editor extension — every hit was about a &lt;em&gt;supported agent&lt;/em&gt; named similarly, unrelated to Herdr's own tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Discussions (459 total)&lt;/td&gt;
&lt;td&gt;GraphQL search: "vscode extension", "editor integration"&lt;/td&gt;
&lt;td&gt;0 and 2 hits; the 2 were about Neovim integration and Windows support, not a VSCode plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Releases (72 releases)&lt;/td&gt;
&lt;td&gt;full-text grep of every release body for vscode / editor-integration terms&lt;/td&gt;
&lt;td&gt;0 mentions across the entire release history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSCode Marketplace + Open VSX Registry&lt;/td&gt;
&lt;td&gt;extension name search: "herdr"&lt;/td&gt;
&lt;td&gt;0 name-matching extensions on either registry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the actual shape of the check, generalized so you can run it against any repo before you build on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Has the maintainer ever discussed building this?&lt;/span&gt;
gh issue list &lt;span class="nt"&gt;-R&lt;/span&gt; &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; &lt;span class="nt"&gt;--state&lt;/span&gt; all &lt;span class="nt"&gt;--search&lt;/span&gt; &lt;span class="s2"&gt;"vscode extension"&lt;/span&gt;
gh issue list &lt;span class="nt"&gt;-R&lt;/span&gt; &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; &lt;span class="nt"&gt;--state&lt;/span&gt; all &lt;span class="nt"&gt;--search&lt;/span&gt; &lt;span class="s2"&gt;"editor integration"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Discussions catch roadmap chatter that never became an issue&lt;/span&gt;
gh api graphql &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{
  search(query: "repo:&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; vscode extension", type: DISCUSSION, first: 20) {
    discussionCount
    nodes { ... on Discussion { title url } }
  }
}'&lt;/span&gt;

&lt;span class="c"&gt;# 3. Releases tell you what actually shipped, not what was proposed&lt;/span&gt;
gh release list &lt;span class="nt"&gt;-R&lt;/span&gt; &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 200 &lt;span class="nt"&gt;--json&lt;/span&gt; tagName &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.[].tagName'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; tag&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;gh release view &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; &lt;span class="nt"&gt;--json&lt;/span&gt; body &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.body'&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"vscode|vs code|editor extension|editor integration"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# example output shape from step 3, run against Herdr's actual release history
# (72 releases scanned, 0 matches)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marketplace search wasn't a pure zero, though — and the "zero on name, non-zero on function" split is worth separating out. No extension is &lt;em&gt;named&lt;/em&gt; after Herdr, but a handful of extensions solve an adjacent problem: they hook into agent lifecycle events to show session state in a VSCode sidebar. None of them run a persistent background process — close the editor, and tracking stops with it. That's a materially different capability from what Herdr's background server does, so "a Marketplace search turned up nothing" would have been the wrong headline; "nothing that replicates the one thing that makes Herdr worth using" is the accurate one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't an empty issue tracker prove there's no demand?
&lt;/h2&gt;

&lt;p&gt;Reading "the maintainer hasn't built X" as "nobody wants X" is a shortcut, and it's wrong often enough to be dangerous on a young project — most people don't file an issue for a feature they haven't consciously wished for yet. An empty tracker on its own tells you the maintainer hasn't prioritized it. It doesn't tell you whether the community has.&lt;/p&gt;

&lt;p&gt;That's exactly why the marketplace search matters as a second, independent source rather than a formality. If several unofficial extensions had been racing to cover Herdr's core capability, that combination — official silence plus visible unofficial demand — would have meant "wanted, but nobody officially owns it," and building one would have been a reasonable bet. What I actually found was the other combination: official silence &lt;em&gt;and&lt;/em&gt; marketplace silence on the one capability that matters (the persistent background server), pointing the same direction from two sources that don't depend on each other. That agreement is what turned a hunch into a decision I was willing to write down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real deciding factor: not feasibility, but design intent
&lt;/h2&gt;

&lt;p&gt;Absence of a plan doesn't automatically mean "don't build it" — it just removes one bad reason to build it ("someone official is already doing this"). The actual decision came from design philosophy, not feasibility: not &lt;em&gt;can&lt;/em&gt; I build this, but &lt;em&gt;what direction of investment does this tool exist to serve&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A VSCode extension is, structurally, a visualization panel — a way to pull an agent's state, progress, and logs into a human's field of view. That's a legitimate kind of value; Claude Code's own official extension is built on exactly that premise. But the reason I installed Herdr in the first place was the opposite direction: to hand the facilitation — the coordination between Claude Code and Codex, two agents with genuinely different behavior — to something other than me, so I'd stop manually deciding which terminal needed my attention next.&lt;/p&gt;

&lt;p&gt;Those two goals point at each other. Building the extension would have been investing in "let the human see more," on top of a tool I'd installed specifically to need to see less. Neither direction is wrong in isolation — visibility has real value, automation has real value — but stacking both on the same tool, without deciding which one it's &lt;em&gt;for&lt;/em&gt;, is how a tool quietly turns into two incompatible tools wearing one name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the "but CLI feels limiting" itch the same way
&lt;/h2&gt;

&lt;p&gt;One nagging doubt survived the decision not to build: "wouldn't a VSCode extension make juggling sessions less awkward than a bare terminal?" That's a real feeling, not a rhetorical one, so I tested it the same way — against primary sources, not against my own hunch.&lt;/p&gt;

&lt;p&gt;Claude Code's official VSCode extension already runs multiple independent tabbed sessions in parallel, each with its own history, while file changes, project settings, and CLAUDE.md rules stay shared across every tab. A background dot on a hidden tab tells you when that agent finished — a detail I'd have wanted to build myself. Cursor's Cloud Agents goes further: up to 8 agents run in parallel on independent remote VMs, so you can hand off "implement this feature" and "fix this bug" separately and check results whenever you're ready, without your laptop needing to stay online. That's strikingly close to what Herdr's background server is trying to do, in a different tool entirely.&lt;/p&gt;

&lt;p&gt;In other words, the itch wasn't evidence that Herdr was missing something — it was evidence that I was asking Herdr to re-solve a problem two tools I already use had already solved. Skipping the extension meant I didn't rebuild, inside Herdr, a worse copy of a feature that already shipped one tab over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Herdr actually earns its place
&lt;/h2&gt;

&lt;p&gt;Solo work — one person switching between sessions — is already covered end to end by Claude Code's tabs and Cursor's Cloud Agents. Adding a Herdr-specific panel on top of that wouldn't add a capability; it would add a second, weaker version of one that already exists.&lt;/p&gt;

&lt;p&gt;Where Herdr earns its keep is the one thing neither of those single-vendor IDE extensions does: cross-tool coordination, so Claude Code and Codex can hand work to each other, or defer to each other when stuck, without a human relaying the message every time. Claude Code's extension is scoped to Claude Code. Cursor's Cloud Agents are scoped to Cursor. Cross-tool handoff between genuinely different agents is a gap only Herdr currently fills.&lt;/p&gt;

&lt;p&gt;That produced three working rules: run solo sessions through whichever editor's own tabs or cloud agents already cover it; open Herdr only when the task genuinely needs two different agents to hand off work between each other; build no dedicated UI for it, because the visualization value already lives somewhere else. I watch this fork — visualize more, or automate more — every time a new AI tool clicks into a workflow, and it's tempting to reach for "add a panel" before checking whether the panel's value already exists somewhere in the stack. Since narrowing Herdr's scope to that one job, I open it noticeably less often, and every open now means something specific — it's not the default way to start a session anymore, it's a deliberate choice for a cross-agent handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste checklist: run this before you build a companion extension
&lt;/h2&gt;

&lt;p&gt;The next time a new CLI tool clicks and "should I build a UI for this" comes up, run these checks before opening an editor to start scaffolding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;step&lt;/th&gt;
&lt;th&gt;question&lt;/th&gt;
&lt;th&gt;where to look&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Has the maintainer discussed this, anywhere?&lt;/td&gt;
&lt;td&gt;Issues, Discussions, Releases (full-text, not just titles)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Does an unofficial version already exist?&lt;/td&gt;
&lt;td&gt;The relevant extension marketplace(s), searched by name &lt;em&gt;and&lt;/em&gt; by function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;If it exists, does it replicate the tool's actual differentiator, or just its surface?&lt;/td&gt;
&lt;td&gt;Read what the alternative does, not just that it exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Does an equivalent capability already exist in a tool you already run daily?&lt;/td&gt;
&lt;td&gt;Check your existing editor/IDE features before assuming a gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Which direction does this tool exist to serve — more visibility, or less need to watch?&lt;/td&gt;
&lt;td&gt;State it in one sentence before adding a feature that pulls the other way&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Any single "yes" against steps 1, 2 (with real functional overlap), or 4 is a strong signal to stop before you design anything. Step 5 is the one that decided this particular case even after 1-4 came back clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does "no GitHub issues asking for X" mean there's no demand for X?
&lt;/h3&gt;

&lt;p&gt;No, and treating it that way is the mistake this whole approach is designed to avoid. An empty tracker means the maintainer hasn't prioritized it — it says nothing about whether anyone else wants it. That's exactly why it needs a second, independent source (a marketplace search, a competing implementation) before you can read it as evidence either way. Two independent silences pointing the same direction is a real signal; one silence alone is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the marketplace search turns up something with a similar name but different features?
&lt;/h3&gt;

&lt;p&gt;Read what it actually does before treating "an extension exists" as "the gap is filled." In this case, several marketplace extensions solved an adjacent problem — surfacing agent session state in a sidebar — without replicating the one capability that made the underlying tool worth using (a background process that survives detach). "Something exists" and "the differentiator exists" are different questions; only the second one should change your decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't this just YAGNI applied to tooling instead of code?
&lt;/h3&gt;

&lt;p&gt;Close, but not identical. YAGNI says don't build a feature until you need it. This adds a specific pre-check for tooling decisions: before you build &lt;em&gt;anything&lt;/em&gt; companion to a tool you didn't write, confirm the gap is real (nobody else has filled it) and confirm which direction the underlying tool is trying to invest in (so your addition doesn't quietly pull it the other way). YAGNI alone would have let me build a technically-justified extension that still worked against Herdr's own reason for existing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;Herdr (official site): &lt;a href="https://herdr.dev" rel="noopener noreferrer"&gt;https://herdr.dev&lt;/a&gt;&lt;br&gt;
Using Claude Code in VS Code (official docs): &lt;a href="https://code.claude.com/docs/ja/vs-code" rel="noopener noreferrer"&gt;https://code.claude.com/docs/ja/vs-code&lt;/a&gt;&lt;br&gt;
Cursor Cloud Agents (official): &lt;a href="https://cursor.com/cloud" rel="noopener noreferrer"&gt;https://cursor.com/cloud&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I write down the decisions I make about AI coding tools, including the ones where the interesting result was not building something. The install, the searches, and the numbers above are from my own setup, checked again on 2026-07-17.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an adapted English version of an essay I first wrote in Japanese. I worked with AI to shape and adapt the piece for a dev.to audience; the tool, the searches, and the decision are my own firsthand account.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>cli</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built 3 Checks Into My AI's Slack Workflow. I've Used One of Them Zero Times.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Wed, 15 Jul 2026 02:16:47 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-built-3-checks-into-my-ais-slack-workflow-ive-used-one-of-them-zero-times-5ak1</link>
      <guid>https://dev.to/nomurasan/i-built-3-checks-into-my-ais-slack-workflow-ive-used-one-of-them-zero-times-5ak1</guid>
      <description>&lt;p&gt;I gave an AI real permission to post to our team's Slack. Then I built three checks into how it uses that permission. When I finally audited my own logs, one of the three had never actually fired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: "It worked" and "I can verify what it did" are two different claims about an AI action. I built 3 checks into my workflow to close that gap. One of them turned out to have zero logged uses — and the trace I use to explain another one doesn't end the way I first described it, either.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The AI's Slack access runs through MCP tools, and one of the first problems I hit had nothing to do with the message content — it was the channel ID. A channel ID is a machine-readable string. It's exactly the kind of identifier an AI can use correctly and a human can't sanity-check by glancing at it. So the first check I put in place was simple: before the AI uses a channel ID for anything, make it get there through &lt;code&gt;slack_search_channels&lt;/code&gt; — a name search, not a raw ID lookup. That turns a step nobody can eyeball into a step a human can.&lt;/p&gt;

&lt;p&gt;Here's what that actually looked like in one real search sequence, from a session log audit on 2026-07-09. This is the tool call shape and the actual result payload for each of the 7 calls, query strings replaced with category placeholders — everything else, including field names and result formatting, is what the connector actually sent and returned:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[project name]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [project name]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;No results found.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[a number]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [a number]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;No results found.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[place name]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [place name]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;No results found.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[an abbreviation]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"include_archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [an abbreviation]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Channels (1 results)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;### Result 1 of 1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Name: #[redacted]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Created: 2019-03-14&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Is Archived: true&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Channel Type: private_channel&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[another project-ish term]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [another project-ish term]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;No results found.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[a department keyword]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [a department keyword]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;No results found.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&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="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[a person's name]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_channel,private_channel"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Search Results for: [a person's name]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;No results found.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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;Seven calls. One of them surfaced a result: an archived private channel that had been sitting untouched since 2019. It wasn't the channel anyone was looking for. The other six, including the last one, came back empty. The search never landed on the target channel in this sequence at all.&lt;/p&gt;

&lt;p&gt;That's not the story I originally told about this trace. An earlier draft of this piece said the seventh call found a match. It didn't — I'd counted that seven calls happened without checking what each one actually returned, and I only caught the difference when someone else re-read the raw results instead of trusting my summary of them.&lt;/p&gt;

&lt;p&gt;But sit with what the search &lt;em&gt;did&lt;/em&gt; catch. An archived channel is not a channel you post a live update to. If the AI had skipped the name search and used a channel ID it had cached or guessed instead, that ID could just as easily have pointed at this same dead channel — and the message would have gone nowhere, with nothing that looked like an error. The search didn't find the target. It found and rejected a bad guess before anything got sent, and then kept looking instead of settling for it. That's the actual value of turning a machine-readable ID into a name search first: not that it always finds the right answer, but that it makes a wrong answer visible instead of silent.&lt;/p&gt;

&lt;p&gt;That's check one. Check two is a separate MCP tool the connector exposes, &lt;code&gt;slack_send_message_draft&lt;/code&gt;, sitting next to &lt;code&gt;slack_send_message&lt;/code&gt; as a distinct, deliberately slower path — draft first, confirm, then send. I adopted it specifically for anything high-stakes. Check three isn't a tool call at all: after a send, don't trust "I sent it" as a terminal state. Reopen the actual channel and check that the count, the recipient, and the content match what was intended.&lt;/p&gt;

&lt;p&gt;Three checks. Two phases: pre-send (verify the plan before it executes) and post-send (verify the result after it executes). I'd assumed I was running all three consistently. I hadn't checked.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check&lt;/th&gt;
&lt;th&gt;phase&lt;/th&gt;
&lt;th&gt;what it's for&lt;/th&gt;
&lt;th&gt;logged uses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;slack_search_channels&lt;/code&gt; before using a channel ID&lt;/td&gt;
&lt;td&gt;pre-send&lt;/td&gt;
&lt;td&gt;turn a machine-readable ID into a human-checkable name, so a bad guess is visible instead of silent&lt;/td&gt;
&lt;td&gt;7 calls in one sequence (2026-07-09 audit); 1 result found, 0 matches to the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;slack_send_message_draft&lt;/code&gt; before a high-stakes send&lt;/td&gt;
&lt;td&gt;pre-send&lt;/td&gt;
&lt;td&gt;gate the send behind an explicit confirm step&lt;/td&gt;
&lt;td&gt;0, across every project I audited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;re-open the channel and check count/recipient/content&lt;/td&gt;
&lt;td&gt;post-send&lt;/td&gt;
&lt;td&gt;never trust "sent" as a terminal state&lt;/td&gt;
&lt;td&gt;not tool-logged — I can't produce a number for this one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The zero is the number that changed my mind. The connector I use exposes &lt;code&gt;slack_send_message_draft&lt;/code&gt; as a distinct tool from &lt;code&gt;slack_send_message&lt;/code&gt;, and I'd adopted it specifically so high-stakes sends would go through a confirm step instead of firing directly. Then I audited my own session logs across the projects where it was available, looking for real invocations. I found none. The tool exists. It's available. It has never actually fired in anything I logged.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical risk, either. AI agents misfiring a message to the wrong destination — a stale credential pointed at the wrong workspace, a notification channel confused with a similar one — is a real, recurring category of incident, not just a risk I'm imagining for the sake of the argument. That's exactly why "the draft tool exists but I've never used it" isn't a comfortable thing to find in an audit — it's a real, closable gap sitting next to a failure mode that's well known to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;I'd been treating "it worked" and "I can verify what it did" as the same claim about an AI action. They're not, and the gap between them splits cleanly into two different problems.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It worked" and "I can verify what it did" are two different claims about the same AI action. Confirming the plan before execution closes the specification gap. Confirming the result after execution closes the verification gap. Most setups — mine included — only reliably close one of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An AI that runs correctly every time is not automatically an AI whose actions you can check after the fact. Those are different design axes. The pre-send half (name search before using an ID, draft before a high-stakes send) closes the specification gap — did the AI understand what I actually wanted it to do. The post-send half (reopening the channel, checking it matches) closes the verification gap — can I confirm what it actually did, independent of what it told me. Building the first without the second gets you an AI that's confidently wrong in a way nobody catches.&lt;/p&gt;

&lt;p&gt;I'd assumed I was running all of this consistently. The audit only confirmed that for the one piece I could actually count: the draft-confirm step had zero logged uses. The post-send channel check isn't a tool call, so I don't have the same kind of number for it — I don't have a clean way to prove I'm doing it every time, which is itself worth sitting with. The one gap I could measure turned out to be real. I can't yet tell you the other one isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The 2-phase idea should generalize past Slack: something checkable before an action executes, and something checkable after. The specific mechanism here — a name search in front of an ID — is Slack-shaped, because Slack happens to expose both a machine ID and a human-searchable name for the same channel. Other tools won't have that exact shape. The underlying question does travel, though.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the AI is about to use a machine-readable identifier for an action with a real side effect, ask whether there's a human-checkable equivalent you can force it through first. For Slack that's a name search. For your tool it'll look different — but the question is the same one.&lt;/li&gt;
&lt;li&gt;If the action is high-stakes and hard to undo in practice, gate it behind an explicit draft-and-confirm step — and then actually check your logs for whether that step ever fires, the way I hadn't.&lt;/li&gt;
&lt;li&gt;After the action runs, don't accept "done" as the end of the story. Reopen the actual system state and check it against what you intended.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the smallest post-action check you could add before trusting your AI's next "sent successfully" — and have you actually looked at your logs to see whether you're running it, or just assuming you are, the way I was?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I ship, and I've learned the hard way that counting how many times a tool was called is not the same as checking what it actually returned.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This piece draws on a Japanese essay covering the same underlying mechanisms, rewritten from scratch as an implementation log (not a direct translation) with AI assistance for structure and the cross-language rewrite. An earlier draft of this piece contained a factual error in its central trace: it described the search sequence as ending in a match, and it doesn't. That was caught by an independent adversarial review of the raw logs, not by the first read-through. The tool-call counts, the audit results, and the conclusions in this version are mine, checked against the individual results of my own session logs — not just the fact that a tool was called.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>slack</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Humans Become the Bottleneck: A Structural View of AI-Augmented Teams</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:15:33 +0000</pubDate>
      <link>https://dev.to/nomurasan/the-humans-become-the-bottleneck-a-structural-view-of-ai-augmented-teams-3ca</link>
      <guid>https://dev.to/nomurasan/the-humans-become-the-bottleneck-a-structural-view-of-ai-augmented-teams-3ca</guid>
      <description>&lt;p&gt;A strange thing happens in organizations that introduce AI agents: the faster the AI gets, the more work piles up at the humans.&lt;/p&gt;

&lt;p&gt;Faros AI's 2025 study of 10,000+ developers across 1,255 enterprise engineering teams reported that teams with high AI adoption completed 21% more tasks [[ne:faros2025_task_completion_21pct]] and merged 98% more pull requests [[ne:faros2025_pr_increase_98pct]] — while review times rose 91% [[ne:faros2025_review_wait_91pct]], bug rates rose 9%, and DORA metrics remained largely unchanged. The faster the AI ships, the further the human review falls behind. This pattern reproduces across many organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck that can't be removed, and the parts that can
&lt;/h2&gt;

&lt;p&gt;Anyone running AI agents will eventually hit the same observation: "the human is the bottleneck."&lt;/p&gt;

&lt;p&gt;You cannot fully remove this. As long as design intent originates from a human, the bottleneck is structural. "Make the AI smarter" doesn't solve it.&lt;/p&gt;

&lt;p&gt;What you can reduce is the number and quality of decisions that end up at a human.&lt;/p&gt;

&lt;p&gt;When an AI agent says "please confirm," the requests fall into two categories.&lt;/p&gt;

&lt;p&gt;Genuinely human-only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirming design intent (only this one is intrinsically required)&lt;/li&gt;
&lt;li&gt;Conflicts with prior decisions&lt;/li&gt;
&lt;li&gt;Cost or scope overruns beyond the assumed envelope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mechanically detectable but currently routed to humans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The implementation is broken (type checks and tests can detect this)&lt;/li&gt;
&lt;li&gt;The experiment design is wrong (defining verification criteria up front prevents this)&lt;/li&gt;
&lt;li&gt;The report has thin evidence (no escalation criteria defined)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second category, masquerading as the first, is what makes humans the bottleneck. The full implementation comes back saying "please review," and the human spends time on it — when what should have been reviewed was just "are there any constraint violations?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The "review everything / trust everything" binary
&lt;/h2&gt;

&lt;p&gt;AI agent operations slide easily into one of two failure modes: everything bounces back for human review, or everything proceeds without oversight.&lt;/p&gt;

&lt;p&gt;The first overloads humans. The second loses visibility.&lt;/p&gt;

&lt;p&gt;The only exit from the binary is to make the escalation criteria explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can't resolve in 2 minutes → switch approach
Stuck for 15 minutes → return to human
Design change beyond scope → always return to human
Otherwise → proceed autonomously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put this into the system prompt. "Where the AI should stop, and where it shouldn't" gets defined. Unnecessary interruptions drop. Galileo's human-in-the-loop design guidance is explicit that escalation rates should be derived from your own task distributions rather than imported as generic industry numbers — which is the same point: the rate is a function of how clearly you've defined "where the AI should stop."&lt;/p&gt;

&lt;p&gt;Anthropic itself has noted that "approve every action" oversight tends to add friction without delivering meaningful safety gains. The model that works is "monitor while it runs, intervene when needed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift review from "after implementation" to "during planning"
&lt;/h2&gt;

&lt;p&gt;The other structural problem is review timing. If your design is "humans review the code after implementation completes," you will not catch up.&lt;/p&gt;

&lt;p&gt;The AI's output speed will not match the human's review speed. Result: review degrades to "looks OK."&lt;/p&gt;

&lt;p&gt;The fix is to shift review to the planning and design stage. Before implementation, confirm "is this direction correct?" The implementation details go to the AI. You don't have to read the code, so review is fast. The most expensive failure mode — "we realized the direction was wrong after the implementation is done" — also disappears.&lt;/p&gt;

&lt;p&gt;Separate machine-verifiable from non-machine-verifiable, too.&lt;/p&gt;

&lt;p&gt;Interface (types, signatures), boundary behaviors — testable and type-checkable, so the machines handle them. Design intent and the "why" — not machine-verifiable. Non-functional concerns (performance, security) — partially testable, partially measurable.&lt;/p&gt;

&lt;p&gt;The only thing strictly not machine-verifiable is "the why of the design." Concentrate human time there. Send everything else to machines. Humans converge on the decisions humans actually have to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paradox: introducing AI grows the workload
&lt;/h2&gt;

&lt;p&gt;In February 2026, HBR published a study observing 200 employees at tech companies over 8 months. After AI tool introduction, the workload did not shrink — it grew.&lt;/p&gt;

&lt;p&gt;The mechanism they called "workload creep" is simple. AI accelerates tasks → stakeholder expectations on speed rise → more tasks get taken on → workload and density increase. They also found cases where job boundaries collapsed — product managers writing code, designers doing data analysis — and adjacent functions' work was absorbed into existing roles.&lt;/p&gt;

&lt;p&gt;ActivTrak's 2025 survey (10,000+ respondents) showed that after AI introduction, time spent on email increased by 104% and time on chat/messaging by 145%.&lt;/p&gt;

&lt;p&gt;"Introduce AI and headcount goes down" is half right and half wrong. The mechanically verifiable work genuinely shrinks. But the most essential work — confirming design intent — actually tends to grow. The faster the AI ships complex implementation, the higher the cost of asking "is this implementation actually aligned with intent?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize the bottleneck, don't try to remove it
&lt;/h2&gt;

&lt;p&gt;Another number from the Faros AI study: even with PRs increasing 98% [[ne:faros2025_pr_increase_98pct]], merge approvals remained largely human-controlled. Stack Overflow's 2025 Developer Survey (49,000+ respondents) found that only 3.1% of developers "highly trust" the accuracy of AI tool output in their development workflow [[ne:so2025_high_trust_3pct]] — among experienced developers, that figure drops to 2.5%. Most teams still gate AI-generated code with manual review.&lt;/p&gt;

&lt;p&gt;Trust is not yet established, so approval gates remain human-fixed. This is a rational call. It is also a speed constraint.&lt;/p&gt;

&lt;p&gt;"How do we remove the bottleneck?" is the wrong question. As long as the source of design intent is human, the bottleneck stays. The right question is "how do we optimize the number and quality of decisions that come back to humans?"&lt;/p&gt;

&lt;p&gt;Three things, compounding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send the machine-verifiable to machines&lt;/li&gt;
&lt;li&gt;Make escalation criteria explicit&lt;/li&gt;
&lt;li&gt;Shift review to the planning stage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stack those, and AI starts to actually lower human load — instead of merely reshuffling where the load lands.&lt;/p&gt;

&lt;p&gt;The skill that AI use demands isn't "trust AI more." It is &lt;strong&gt;deliberately designing what humans must decide, and how much of it&lt;/strong&gt;. AI is a tool. The design of how to use it stays human work.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.faros.ai/ai-productivity-paradox" rel="noopener noreferrer"&gt;The AI Productivity Paradox — Faros AI (2025)&lt;/a&gt; — primary source for the 21% / 98% / 91% figures across 10,000+ developers / 1,255 teams&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://survey.stackoverflow.co/2025/ai/" rel="noopener noreferrer"&gt;2025 Developer Survey: AI — Stack Overflow&lt;/a&gt; — primary source for the 3.1% "highly trust" figure&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hbr.org/2026/02/ai-doesnt-reduce-work-it-intensifies-it" rel="noopener noreferrer"&gt;AI Doesn't Reduce Work — It Intensifies It — HBR (Feb 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.activtrak.com/blog/2026-state-of-the-workplace/" rel="noopener noreferrer"&gt;2026 State of the Workplace — ActivTrak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://galileo.ai/blog/human-in-the-loop-agent-oversight" rel="noopener noreferrer"&gt;How to Build Human-in-the-Loop Oversight for AI Agents — Galileo&lt;/a&gt; — referenced for HITL design framing, not for specific escalation rates&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>aiagents</category>
      <category>teamdesign</category>
      <category>humanintheloop</category>
      <category>operations</category>
    </item>
    <item>
      <title>CLAUDE.md Is Not a Prompt File. It Is an Operating Boundary.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 14 Jul 2026 08:49:38 +0000</pubDate>
      <link>https://dev.to/nomurasan/claudemd-is-not-a-prompt-file-it-is-an-operating-boundary-6gl</link>
      <guid>https://dev.to/nomurasan/claudemd-is-not-a-prompt-file-it-is-an-operating-boundary-6gl</guid>
      <description>&lt;p&gt;I keep seeing teams treat &lt;code&gt;CLAUDE.md&lt;/code&gt; as a better prompt.&lt;/p&gt;

&lt;p&gt;They write things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;think carefully&lt;/li&gt;
&lt;li&gt;make small changes&lt;/li&gt;
&lt;li&gt;write readable code&lt;/li&gt;
&lt;li&gt;value tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is wrong. It is just too weak to operate a real project.&lt;/p&gt;

&lt;p&gt;The question is not whether the AI wants to write good code. The question is what "good" means in this repository.&lt;/p&gt;

&lt;p&gt;In one project, the right move is a tiny patch. In another, preserving an old compatibility layer is the dangerous part. In one repo, generated files are never touched. In another, generated files must be committed because that is how deployment works.&lt;/p&gt;

&lt;p&gt;A generic prompt cannot carry that difference.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; works best when it is not a motivational note to the model. It is an onboarding sheet for the work environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A prompt asks. An operating boundary constrains.
&lt;/h2&gt;

&lt;p&gt;A prompt is a request for this task.&lt;/p&gt;

&lt;p&gt;An operating boundary is context that should shape every task before it starts.&lt;/p&gt;

&lt;p&gt;That difference matters because most AI-assisted development failures I see are not caused by the model being evil or lazy. They happen because the model was not given the project boundary.&lt;/p&gt;

&lt;p&gt;The Git assumptions were missing.&lt;br&gt;
The target OS was not stated.&lt;br&gt;
"Do the same as the existing code" had no clear reference point.&lt;br&gt;
A one-line fix became a broad cleanup because nothing said where to stop.&lt;/p&gt;

&lt;p&gt;Humans often pass these boundaries by tone and memory. Before a task, someone says "do not touch that file" or "this affects production, ask first." A human teammate slowly learns those lines.&lt;/p&gt;

&lt;p&gt;An AI agent does not learn the room that way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is where you write the room down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version can be ten lines.
&lt;/h2&gt;

&lt;p&gt;Do not start by writing a grand engineering constitution.&lt;/p&gt;

&lt;p&gt;The first useful &lt;code&gt;CLAUDE.md&lt;/code&gt; can be small.&lt;/p&gt;

&lt;p&gt;Project:&lt;br&gt;
This repository maps note.com draft URLs to local stock articles.&lt;/p&gt;

&lt;p&gt;Boundaries:&lt;br&gt;
Publishing, deletion, billing, and force operations require human confirmation.&lt;br&gt;
Before overwriting an untracked file, check why it exists and whether it should be tracked.&lt;br&gt;
Do not revert user-created changes.&lt;/p&gt;

&lt;p&gt;Done:&lt;br&gt;
When article text changes, run check and preview while keeping the draft URL stable.&lt;br&gt;
Do not change owner_reviewed until the user explicitly approves it.&lt;/p&gt;

&lt;p&gt;This is not beautiful. It is useful.&lt;/p&gt;

&lt;p&gt;The AI does not first need the deep philosophy of the project. It needs to know which lines are expensive to cross.&lt;/p&gt;

&lt;p&gt;Can it publish?&lt;br&gt;
Can it delete?&lt;br&gt;
Can it touch untracked files?&lt;br&gt;
Who is allowed to flip the reviewed flag?&lt;/p&gt;

&lt;p&gt;Those are not style preferences. They are accident boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three sections are usually enough.
&lt;/h2&gt;

&lt;p&gt;When I create a &lt;code&gt;CLAUDE.md&lt;/code&gt;, I start with three buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Project
&lt;/h3&gt;

&lt;p&gt;What is this repository for?&lt;/p&gt;

&lt;p&gt;This should be written in operational language, not branding language.&lt;/p&gt;

&lt;p&gt;Bad:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This project improves productivity with AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Better:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This repository converts Markdown articles into note.com drafts and preserves the draft URL so the same remote draft can be updated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second version tells the AI what must not be broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Boundaries
&lt;/h3&gt;

&lt;p&gt;What can the AI touch without asking?&lt;/p&gt;

&lt;p&gt;What requires confirmation?&lt;/p&gt;

&lt;p&gt;What must never be done automatically?&lt;/p&gt;

&lt;p&gt;This is where destructive operations belong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rm -rf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git reset --hard&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;force push&lt;/li&gt;
&lt;li&gt;publishing&lt;/li&gt;
&lt;li&gt;billing&lt;/li&gt;
&lt;li&gt;production deploys&lt;/li&gt;
&lt;li&gt;external account changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it should also include local project boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generated files are read-only&lt;/li&gt;
&lt;li&gt;migrations need a matching rollback&lt;/li&gt;
&lt;li&gt;user edits must not be reverted&lt;/li&gt;
&lt;li&gt;screenshots are evidence, not source of truth&lt;/li&gt;
&lt;li&gt;credentials are never requested in chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good boundaries are boring. That is the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Done
&lt;/h3&gt;

&lt;p&gt;What does "finished" mean here?&lt;/p&gt;

&lt;p&gt;For a web app, it might be build, lint, and a browser screenshot.&lt;/p&gt;

&lt;p&gt;For a note pipeline, it might be check, preview, and keeping the draft URL stable.&lt;/p&gt;

&lt;p&gt;For an API change, it might be tests plus a contract sample.&lt;/p&gt;

&lt;p&gt;Without this section, an AI agent tends to stop at "the code was edited." In a real workflow, editing is rarely the definition of done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not turn CLAUDE.md into a junk drawer.
&lt;/h2&gt;

&lt;p&gt;The danger of &lt;code&gt;CLAUDE.md&lt;/code&gt; is that it is convenient.&lt;/p&gt;

&lt;p&gt;After every incident, someone adds a sentence. After every useful command, someone pastes a snippet. After every successful prompt, someone stores it there "just in case."&lt;/p&gt;

&lt;p&gt;Eventually nobody reads it. The AI technically receives it, but the useful signal is buried under stale instructions.&lt;/p&gt;

&lt;p&gt;I try to keep only three kinds of text in the file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;rules that break something if violated&lt;/li&gt;
&lt;li&gt;decisions that come up again and again&lt;/li&gt;
&lt;li&gt;project-specific words the AI would otherwise guess from names&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else should fight for its place.&lt;/p&gt;

&lt;p&gt;Remove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generic engineering virtues&lt;/li&gt;
&lt;li&gt;commands already explained by &lt;code&gt;--help&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;one-off prompts&lt;/li&gt;
&lt;li&gt;old workaround notes after the tool has been fixed&lt;/li&gt;
&lt;li&gt;rules nobody follows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deleting instructions is part of maintaining them.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the AI ignores the rule, rewrite the stop condition.
&lt;/h2&gt;

&lt;p&gt;When a rule fails, the usual reaction is to make it louder.&lt;/p&gt;

&lt;p&gt;"Be careful before publishing."&lt;/p&gt;

&lt;p&gt;"Really be careful before publishing."&lt;/p&gt;

&lt;p&gt;"Never forget to be careful before publishing."&lt;/p&gt;

&lt;p&gt;That rarely helps.&lt;/p&gt;

&lt;p&gt;A better move is to convert the sentence into a stop condition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;before publishing, check &lt;code&gt;owner_reviewed: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;owner_reviewed&lt;/code&gt; is false, stop and print the draft URL&lt;/li&gt;
&lt;li&gt;if a browser review timestamp is missing, open the draft first&lt;/li&gt;
&lt;li&gt;if the user did not explicitly approve, do not flip the flag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more concrete the stop condition, the less you depend on the model's mood, attention, or interpretation.&lt;/p&gt;

&lt;p&gt;This is the same reason hooks, tests, and small guard scripts work better than reminders. They interrupt the workflow at the point where the mistake would happen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; does not replace physical guardrails. It should point to them and explain the intent behind them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file is not for the AI only.
&lt;/h2&gt;

&lt;p&gt;There is another benefit: &lt;code&gt;CLAUDE.md&lt;/code&gt; forces humans to notice what they had not explained.&lt;/p&gt;

&lt;p&gt;If you cannot write the boundary, you probably do not have one.&lt;/p&gt;

&lt;p&gt;If every operation feels like an exception, the workflow is not ready for delegation.&lt;/p&gt;

&lt;p&gt;If the "done" condition depends on one person's memory, the AI is not the only risk.&lt;/p&gt;

&lt;p&gt;Writing &lt;code&gt;CLAUDE.md&lt;/code&gt; exposes these gaps early. The file becomes a small design review for your own process.&lt;/p&gt;

&lt;p&gt;That is why I do not think of it as prompt engineering.&lt;/p&gt;

&lt;p&gt;I think of it as operational design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start before the first task.
&lt;/h2&gt;

&lt;p&gt;Do not wait until the AI has already broken something.&lt;/p&gt;

&lt;p&gt;Before the first real task, write one page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what this project exists to do&lt;/li&gt;
&lt;li&gt;what the AI must not touch&lt;/li&gt;
&lt;li&gt;which operations require a human&lt;/li&gt;
&lt;li&gt;what checks define done&lt;/li&gt;
&lt;li&gt;what local terms mean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run the first task and watch where the agent still guesses.&lt;/p&gt;

&lt;p&gt;If it guesses wrong, improve the boundary.&lt;br&gt;
If it stops in the right place, keep the rule.&lt;br&gt;
If the same rule is now enforced by a script or hook, shorten the prose and point to the mechanism.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is not a prompt file.&lt;/p&gt;

&lt;p&gt;It is the first orientation document you give a non-human teammate before letting it work inside your repository.&lt;/p&gt;

&lt;p&gt;The lesson from AI-driven development failures is not "be afraid of AI."&lt;/p&gt;

&lt;p&gt;It is that humans have been relying on unspoken boundaries for too long.&lt;/p&gt;

&lt;p&gt;Write the boundary down, and the AI becomes less like a code generator running loose and more like a teammate working inside a room with walls.&lt;/p&gt;




&lt;p&gt;This article was adapted from a Japanese note essay with AI assistance for the English rewrite. The operational claims and conclusions are mine.&lt;/p&gt;

&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic Docs: Claude Code memory - &lt;a href="https://docs.anthropic.com/en/docs/claude-code/memory" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/docs/claude-code/memory&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Classmethod, "AI-driven development: successes and failures" (talk, &lt;strong&gt;in Japanese&lt;/strong&gt;) - &lt;a href="https://www.youtube.com/watch?v=FtzxqVsaaRM" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=FtzxqVsaaRM&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Speaker Deck: AI-driven development, success and failure case studies (slides, &lt;strong&gt;in Japanese&lt;/strong&gt;) - &lt;a href="https://speakerdeck.com/satoshi7/aiqu-dong-kai-fa-nocheng-gong-toshi-bai-kurasumesotutonoshi-jian-shi-li-ji" rel="noopener noreferrer"&gt;https://speakerdeck.com/satoshi7/aiqu-dong-kai-fa-nocheng-gong-toshi-bai-kurasumesotutonoshi-jian-shi-li-ji&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>contextengineering</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Measured Claude Code's Prompt-Cache Cost Three Ways. 85% of It Wasn't Mine to Trim.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sun, 12 Jul 2026 17:37:12 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-measured-claude-codes-prompt-cache-cost-three-ways-85-of-it-wasnt-mine-to-trim-48hm</link>
      <guid>https://dev.to/nomurasan/i-measured-claude-codes-prompt-cache-cost-three-ways-85-of-it-wasnt-mine-to-trim-48hm</guid>
      <description>&lt;p&gt;I ran the same empty Claude Code project through three launch paths and read the cache tokens off the session log after every turn. One of those paths never touched the shared server-side cache at all — &lt;code&gt;cache_read&lt;/code&gt; stayed at 0 on every run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: the launch path (VSCode extension, VSCode-internal terminal, or a standalone terminal like iTerm2) decides whether Claude Code hits Anthropic's shared prompt cache. And even after trimming CLAUDE.md files, skills, and hooks, roughly 85% of the remaining &lt;code&gt;cache_creation&lt;/code&gt; cost turned out to be Claude Code's own fixed overhead — not anything I control.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Claude Code writes a JSONL log for every session under &lt;code&gt;~/.claude/projects/&amp;lt;project-key&amp;gt;/&lt;/code&gt;, and each assistant turn in that log carries a &lt;code&gt;usage&lt;/code&gt; object with &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; and &lt;code&gt;cache_read_input_tokens&lt;/code&gt;. To see what was actually happening turn by turn instead of guessing from the monthly bill, I wrote a short script that reads the newest session log for the current project directory and prints those two numbers per turn.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;project_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcwd&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;project_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.claude/projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;project_key&lt;/span&gt;
&lt;span class="n"&gt;jsonl_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;jsonl_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonl_files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jsonl_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turn &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: input=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cache_create=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cache_read=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I saved it as &lt;code&gt;measure-cache.py&lt;/code&gt; and ran it from inside a baseline project — no CLAUDE.md, no hooks, no skills — right after opening a fresh session, then ran the exact same baseline through three different launch paths: the VSCode extension's chat panel, a terminal opened inside VSCode running the Claude Code CLI, and a standalone terminal (iTerm2) running the same CLI outside VSCode entirely.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Same project, three launch paths:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;launch path&lt;/th&gt;
&lt;th&gt;cache_creation&lt;/th&gt;
&lt;th&gt;cache_read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VSCode extension (chat panel)&lt;/td&gt;
&lt;td&gt;39,054&lt;/td&gt;
&lt;td&gt;10,270&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSCode-internal terminal (CLI)&lt;/td&gt;
&lt;td&gt;39,054&lt;/td&gt;
&lt;td&gt;10,270&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;standalone terminal outside VSCode (e.g. iTerm2)&lt;/td&gt;
&lt;td&gt;50,985&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;cache_read&lt;/code&gt; costs about 1/10 of &lt;code&gt;cache_create&lt;/code&gt; for the same tokens, so which launch path hits the shared cache is a real cost lever, not a rounding error. Both VSCode paths (extension and internal terminal) landed on identical numbers and hit the shared cache for 10,270 tokens. The standalone terminal never did — &lt;code&gt;cache_read=0&lt;/code&gt; on every run, meaning it paid the full &lt;code&gt;cache_creation&lt;/code&gt; price every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subagents don't inherit anything, they just land on the same door:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;origin&lt;/th&gt;
&lt;th&gt;subagent cache_creation&lt;/th&gt;
&lt;th&gt;subagent cache_read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;launched from inside VSCode&lt;/td&gt;
&lt;td&gt;39,054&lt;/td&gt;
&lt;td&gt;10,270&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Subagents launched from inside VSCode showed the same 39,054 / 10,270 split as the parent session — independent &lt;code&gt;cache_creation&lt;/code&gt; each time, but landing on the same shared cache key as the parent because they share the VSCode launch path, not because they "inherit" anything from the parent session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cache_creation breakdown, layer by layer, same project:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;configuration&lt;/th&gt;
&lt;th&gt;cache_creation&lt;/th&gt;
&lt;th&gt;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nothing added (baseline)&lt;/td&gt;
&lt;td&gt;39,054&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ 85 user skills&lt;/td&gt;
&lt;td&gt;41,999&lt;/td&gt;
&lt;td&gt;+2,886 for the skill list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ CLAUDE.md files + session hooks&lt;/td&gt;
&lt;td&gt;46,367&lt;/td&gt;
&lt;td&gt;+4,368 total for user-controlled layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pre-optimization state (same project, before trimming)&lt;/td&gt;
&lt;td&gt;68,097&lt;/td&gt;
&lt;td&gt;+26,098 vs. the optimized 46,367&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The user-controllable share of &lt;code&gt;cache_creation&lt;/code&gt; came out to about 15% (7,313 / 46,367 tokens) in this project. The remaining roughly 85% is Claude Code's own fixed overhead — the 39,054-token floor that showed up before I added a single skill, hook, or CLAUDE.md file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;Call it &lt;strong&gt;the fan-out tax&lt;/strong&gt;: every subagent Claude Code launches pays its own fixed cache-creation cost, and paying it once doesn't make it cheaper for the next subagent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Subagents do NOT inherit the parent session's cache. Each subagent pays its own &lt;code&gt;cache_creation&lt;/code&gt; cost, so parallel subagent fan-out multiplies fixed costs linearly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That reframes what trimming CLAUDE.md is actually worth. The ~15% I can control in a single session is a modest saving on its own. But that saving gets multiplied by however many subagents run in parallel, while the 39,054-token fixed floor gets paid by every subagent regardless of what I trim. Ten subagents fanning out in parallel means the fixed floor alone is 390,540 tokens of &lt;code&gt;cache_creation&lt;/code&gt; before any of my own configuration even enters the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The script above is generic — point it at any project directory and it reads whichever session log is newest. To see your own numbers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick an empty or near-empty project directory.&lt;/li&gt;
&lt;li&gt;Open it with the Claude Code VSCode extension, send one message, then run the script.&lt;/li&gt;
&lt;li&gt;Open the exact same directory in a terminal outside VSCode (iTerm2, Terminal.app, a plain SSH session) and run the CLI cold, then run the script again.&lt;/li&gt;
&lt;li&gt;Compare &lt;code&gt;cache_read&lt;/code&gt; on turn 1 between the two runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it reads 0 in the second run, that launch path isn't hitting your shared cache, and every session from that path is paying full &lt;code&gt;cache_creation&lt;/code&gt; price.&lt;/p&gt;

&lt;p&gt;What's the smallest project you'd need to strip down to find out whether your own launch path is hitting the cache?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I measure a cost before I trust an assumption about it. Measured on Claude Code v2.1.126, claude-sonnet-4-6, macOS 25.4.0, 2026-05-03.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an adapted English version of an essay I first wrote in Japanese, on Qiita. I worked with AI to shape and translate the piece; the script, the measurements, and the conclusions are my own.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>claudecode</category>
      <category>anthropic</category>
      <category>llm</category>
      <category>vscode</category>
    </item>
    <item>
      <title>McDonald's ran an AI drive-thru pilot for 3 years across 100+ stores, then killed it. It wasn't an accuracy problem.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 11 Jul 2026 11:45:13 +0000</pubDate>
      <link>https://dev.to/nomurasan/mcdonalds-ran-an-ai-drive-thru-pilot-for-3-years-across-100-stores-then-killed-it-it-wasnt-an-4mp2</link>
      <guid>https://dev.to/nomurasan/mcdonalds-ran-an-ai-drive-thru-pilot-for-3-years-across-100-stores-then-killed-it-it-wasnt-an-4mp2</guid>
      <description>&lt;p&gt;McDonald's ran an AI drive-thru voice system with IBM across more than 100 stores, starting in 2021. In July 2024, they ended the entire test. It wasn't an accuracy problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: Cisco's 2025 survey found only 5% of enterprise AI pilots ever reach production. In AI adoption support work, the stalls keep tracing back to the same pattern: teams design and validate for "does it work under lab conditions," and never design for "who reviews the output, and what happens when it's wrong." Those are the two questions that actually determine whether an AI deployment survives contact with a live environment.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The McDonald's/IBM drive-thru pilot worked in controlled testing. In the field, it didn't hold up against ambient noise, regional accents, and multiple customers talking over each other. A system that performs correctly in a quiet lab and fails against wind noise and a fast regional accent isn't a smaller version of the same problem — it's a different problem that was never tested for.&lt;/p&gt;

&lt;p&gt;MD Anderson Cancer Center's IBM Watson oncology project — a $62 million investment — followed the same shape. It worked against curated, cleaned datasets. Real patient records — missing fields, ambiguous notation, inconsistent timelines — broke it. The project ended with zero real patients ever treated using the system.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cisco 2025 survey: enterprise AI pilots reaching production =  5%
TIS survey (domestic): companies with org-wide AI adoption  = 21.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remaining 95% stall somewhere between "the demo worked" and "this runs in production." PwC Japan's survey on the same question found the top blocker wasn't security or cost concerns — it was "we don't know how to apply this effectively." That's not a tooling problem. Getting output out of an AI is trivial now. The unsolved part is: where does that output plug into the actual workflow, who reviews it, and how do you judge whether it worked?&lt;/p&gt;

&lt;p&gt;AI-generated copy tends to contain output that reads as plausible but is subtly wrong. Reviewing that kind of output for correctness takes longer than an experienced writer drafting from scratch — because starting from a blank page lets you set your own pace, and reading someone else's (or something else's) draft to judge its correctness is a different cognitive task entirely. That's the mechanism behind "have the AI draft it, then fix it" turning out slower than drafting from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;Cross-referencing the public case studies and survey data against what adoption support work actually involves, three recurring failure patterns account for most of the stalls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The AI got bolted onto the existing workflow instead of replacing a step in it.&lt;/strong&gt; A human still does the work, the AI's output gets added as an extra check, and total steps — and total time — go up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody defined what "review" means, specifically.&lt;/strong&gt; "Check the AI's output" without a specific checklist turns into re-reading the entire thing every time, which is slower than not using AI at all. Left undefined, that review step calcifies into "read everything, just in case it's AI-written" — and the team ends up feeling like adopting AI added work rather than removed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody designed the fallback for when it's wrong.&lt;/strong&gt; The first bad output triggers a manager escalation, and the team quietly stops using the tool rather than build a recovery path.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A pilot answers "does it work." Production requires answering "who catches it when it doesn't, and what do they do next." Skipping the second question is why pilots don't survive contact with a live environment.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The inverse also holds, in deployments that stuck: review criteria were specific (check the numbers, check proper-noun consistency, check tone against a guideline) rather than "read the whole thing." That turns review from re-reading into cross-checking — faster, and it doesn't wear the reviewer down over time. And a fallback path existed before the first failure happened: "if this output looks wrong, fall back to this template and proceed manually." That single sentence, decided in advance, is the difference between a stalled rollout and one that survives its first bad output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Before choosing a tool, write down the judgment calls that repeat in the workflow you're targeting — "check whether this reply matches the client's tone," "check whether this number crosses a threshold," "pick the matching item from this list." For each one, ask which part is information-gathering (an AI can plausibly do this) versus which part is the actual decision (a human still makes this call). Measure the current baseline first — minutes per task, task volume per month, current error rate — because you can't tell if the AI helped without a number to compare against.&lt;/p&gt;

&lt;p&gt;What's the specific judgment call, repeated daily in your own workflow, that you've never actually written down?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I do AI adoption support work day to day; these patterns come from cross-referencing published case studies and survey data against what that work actually involves, not from a single case.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This piece was adapted from a Japanese essay, with AI assistance for the cross-language rewrite. The reasoning, data, and conclusions are mine.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cisco AI adoption survey (2025): &lt;a href="https://uravation.com/media/ai-adoption-failure-cases-by-industry-2026/" rel="noopener noreferrer"&gt;https://uravation.com/media/ai-adoption-failure-cases-by-industry-2026/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;McDonald's/IBM drive-thru AI ends (CNBC, 2024): &lt;a href="https://www.cnbc.com/2024/06/17/mcdonalds-to-end-ibm-ai-drive-thru-test.html" rel="noopener noreferrer"&gt;https://www.cnbc.com/2024/06/17/mcdonalds-to-end-ibm-ai-drive-thru-test.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;S&amp;amp;P Global AI failure rate survey (CIO Dive): &lt;a href="https://www.ciodive.com/news/AI-project-fail-data-SPGlobal/742590/" rel="noopener noreferrer"&gt;https://www.ciodive.com/news/AI-project-fail-data-SPGlobal/742590/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PwC Japan generative AI survey, spring 2025 (&lt;strong&gt;in Japanese&lt;/strong&gt;): &lt;a href="https://www.pwc.com/jp/ja/knowledge/thoughtleadership/generative-ai-survey2025.html" rel="noopener noreferrer"&gt;https://www.pwc.com/jp/ja/knowledge/thoughtleadership/generative-ai-survey2025.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aiadoption</category>
      <category>digitaltransformation</category>
      <category>productivity</category>
      <category>genai</category>
    </item>
    <item>
      <title>I Stopped Asking AI Agents to Behave: 3 Permission Levels Instead</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Fri, 10 Jul 2026 14:50:56 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-stopped-asking-ai-agents-to-behave-3-permission-levels-instead-3e3p</link>
      <guid>https://dev.to/nomurasan/i-stopped-asking-ai-agents-to-behave-3-permission-levels-instead-3e3p</guid>
      <description>&lt;p&gt;An AI agent merged work that I expected to review myself.&lt;/p&gt;

&lt;p&gt;The tests passed. An existing auto-merge path ran. The deployment started. A stale authentication setting made that last step fail, so production did not change.&lt;/p&gt;

&lt;p&gt;That was luck, not a control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: Do not decide an AI agent's permissions by how much you trust it. Decide them by reversibility. I now use three levels: delegate reversible work, require confirmation for recoverable work, and block high-impact work at execution time.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer: what are the three permission levels?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;What makes it belong there&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delegate freely&lt;/td&gt;
&lt;td&gt;Research, code generation, drafts, pull requests&lt;/td&gt;
&lt;td&gt;I can discard or redo the result without another party being affected&lt;/td&gt;
&lt;td&gt;Normal machine checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Require confirmation&lt;/td&gt;
&lt;td&gt;A merge after checks, an external draft, an internal announcement, bulk test-data updates&lt;/td&gt;
&lt;td&gt;It can be undone, but a person must notice and do work to undo it&lt;/td&gt;
&lt;td&gt;A record plus explicit confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block at execution&lt;/td&gt;
&lt;td&gt;Production deployment, an external charge, public release&lt;/td&gt;
&lt;td&gt;The action creates a fact outside the workflow that cannot be fully recalled&lt;/td&gt;
&lt;td&gt;A tool-level block and a human-owned path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is not that every merge is always irreversible. The point is to inspect the whole chain. A merge that triggers a deployment is not the same operation as a merge that stays in an isolated branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened?
&lt;/h2&gt;

&lt;p&gt;I had written a rule equivalent to: "Do not automatically merge into production. Only do that with explicit user instruction."&lt;/p&gt;

&lt;p&gt;The agent did not ignore the words. It took a reasonable path through them. In its view, waiting for checks and then using the existing auto-merge mechanism was a cautious continuation of the task. In my view, the merge was the moment I would make the final call.&lt;/p&gt;

&lt;p&gt;Those two views never met in an enforceable place.&lt;/p&gt;

&lt;p&gt;The dangerous part was that no individual step looked absurd:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fix the test and the configuration.&lt;/li&gt;
&lt;li&gt;Run checks.&lt;/li&gt;
&lt;li&gt;Let the configured merge path continue.&lt;/li&gt;
&lt;li&gt;Start the deployment chain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The failure was not a malicious agent. It was an unclassified chain of operations with no stop point for a human decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not use a simple safe/unsafe split?
&lt;/h2&gt;

&lt;p&gt;I first tried a binary policy. That made anything slightly hard to undo look unsafe. Soon the agent could do almost nothing without approval, and I was back to reviewing every small action.&lt;/p&gt;

&lt;p&gt;The middle tier is what made the policy useful.&lt;/p&gt;

&lt;p&gt;Some operations are not harmless, but they are recoverable: a draft posted to an external service, a message sent to an internal channel, or a bulk update to test data. I want an audit trail and a clear "is this right?" point, not a permanent ban.&lt;/p&gt;

&lt;p&gt;The highest tier is different. A public release can be read before it is deleted. A production deployment can trigger follow-on effects. A charge can move money even if it is later refunded. These are not places where a better prompt is a sufficient control.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Trust is about the agent. Permission is about the consequence of an action. Keep those decisions separate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Put the block below the prompt
&lt;/h2&gt;

&lt;p&gt;The useful guard has to run where the command is about to execute, not only in the wrapper script the agent is supposed to choose.&lt;/p&gt;

&lt;p&gt;Here is a deliberately minimal example. It reads a proposed command from standard input, so it can sit behind a tool hook or a wrapper. The integration point will differ by agent platform, but the policy is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Reject commands that cross a human-owned boundary.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;blocked_patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create charge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blocked_patterns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BLOCKED: this action is high impact. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use the human-owned release path instead.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can try the behavior without connecting it to an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s1"&gt;'deploy production'&lt;/span&gt; | python3 guard_irreversible.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKED: this action is high impact. Use the human-owned release path instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally not presented as an adversarial security boundary. A command-string check can miss aliases, wrappers, or a determined attempt to evade it. The stronger boundary is to remove the underlying credential or capability from what the agent can reach.&lt;/p&gt;

&lt;p&gt;But it is still a meaningful operational control. It turns the straightforward, accidental command path into a visible routing error before the command runs. The message tells the agent and the operator what happened and where to go next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify chains, not just commands
&lt;/h2&gt;

&lt;p&gt;The classification becomes more useful when you look beyond the command you can see.&lt;/p&gt;

&lt;p&gt;For example, changing a file is usually freely delegated. Merging that change might be recoverable. But if the merge triggers a production deployment, the meaningful boundary is not the file edit or the merge alone. It is the chain that ends in an externally visible change.&lt;/p&gt;

&lt;p&gt;I now ask three questions whenever a new class of action appears:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What can this action reach on its own?&lt;/li&gt;
&lt;li&gt;Can the consequence disappear by itself, be recovered by a known procedure, or remain as an external fact?&lt;/li&gt;
&lt;li&gt;Which control belongs immediately before that consequence: none, confirmation, or block?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The classification is not permanent. An action can move outward after we learn it is easier to recover than expected. It can move inward after a near-miss reveals a downstream effect we did not model.&lt;/p&gt;

&lt;p&gt;Changing the classification itself is high impact in a team. Treat a request to relax a boundary as something that needs the same kind of review as the boundary protects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one guard, not a perfect policy
&lt;/h2&gt;

&lt;p&gt;Trying to enumerate every dangerous command before shipping any guard is a reliable way to ship nothing.&lt;/p&gt;

&lt;p&gt;My practical loop is smaller:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List the recurring actions an agent takes.&lt;/li&gt;
&lt;li&gt;Put each one in one of the three tiers.&lt;/li&gt;
&lt;li&gt;Add one hard block for the specific high-impact action that produced a near-miss.&lt;/li&gt;
&lt;li&gt;Make the block explain both the reason and the allowed human-owned route.&lt;/li&gt;
&lt;li&gt;Add or revise a rule only after a new class of near-miss teaches you something.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That makes the policy a growing record of actual operational lessons, not a giant prompt full of exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this mean every agent action needs approval?
&lt;/h3&gt;

&lt;p&gt;No. That is exactly what the three-tier split avoids. Reversible work should stay fast. The confirmation tier preserves a human choice where recovery costs attention. Only the highest-impact actions are blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the confirmation tier actually work, or do people just click through prompts?
&lt;/h3&gt;

&lt;p&gt;Alert fatigue is a real risk, and I don't have data proving the confirmation tier survives it over time. What's intended to limit the risk is scope: this tier only fires for the narrow band between reversible-enough-to-delegate and irreversible-enough-to-block, not for every tool call, so in principle the volume stays low enough that a prompt has a chance of getting read. If a prompt does get rubber-stamped anyway, the block tier is the fallback for whatever I've classified as unrecoverable — that failure is what the block tier is supposed to catch, not the confirmation tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a merge always in the same tier?
&lt;/h3&gt;

&lt;p&gt;No. Classify the real chain. A merge that cannot reach production is different from one that automatically starts a deployment or changes a customer-facing system.&lt;/p&gt;

&lt;p&gt;Since publishing this, I checked what actually changed on the infrastructure side. The website repo's &lt;code&gt;allow_auto_merge&lt;/code&gt; setting is now &lt;code&gt;false&lt;/code&gt; at the GitHub platform level — I confirmed this via the GitHub API. Separately, a pre-execution guard now blocks the &lt;code&gt;gh pr merge&lt;/code&gt; command for every repo in that org, with no bypass flag or environment override in the hook source. An earlier, narrower version of that guard had excluded Dependabot PRs. As of 2026-07-10 that exception was revoked, so the block now covers all callers of the command.&lt;/p&gt;

&lt;p&gt;Both changes match the classification above. A merge that could reach production through auto-merge plus deploy doesn't stay in "delegate freely." It gets pulled out of that tier at two points: the platform capability and the command itself.&lt;/p&gt;

&lt;p&gt;That's the current state of both settings as of 2026-07-10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the Python guard enough?
&lt;/h3&gt;

&lt;p&gt;No. It is a useful operational guard against the ordinary path, not a complete security boundary. For irreversible capabilities, reduce the permissions and credentials the agent can access as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not use GitHub branch protection or a policy engine like OPA instead of a custom script?
&lt;/h3&gt;

&lt;p&gt;They cover different layers. Branch protection stops merges — which I already use, see the merge question above — but it has no opinion on a deploy script or a payment API call, which is most of what this guard checks. OPA is a fair alternative once I'm already running a policy engine across multiple services and want rules defined in one place; for a single script gating one agent, standing it up is more infrastructure than the current problem needs. If the pattern list grows or spans multiple projects, moving this logic into a real policy engine is a reasonable next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I do in an emergency?
&lt;/h3&gt;

&lt;p&gt;Do not add an agent-accessible "emergency" bypass to the block. Give an authorized human a separate, documented path. Otherwise the exception becomes the normal route over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste checklist: add your first stop point
&lt;/h2&gt;

&lt;p&gt;Pick one operation an AI agent performs today that would be costly to reverse: a deployment, a public post, an external message, a billing call, or a customer-data overwrite.&lt;/p&gt;

&lt;p&gt;Classify the whole chain. Then put one stop point immediately before the external fact is created. You do not need a perfect wall on day one. You need the first floorboard before the place where luck was protecting you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I build and operate AI-assisted workflows. This piece was adapted from a Japanese essay with AI assistance for the cross-language rewrite; the incident, reasoning, and conclusions are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Open-ended delegation freezes half a room. Here's what unfroze them</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:01:30 +0000</pubDate>
      <link>https://dev.to/nomurasan/open-ended-delegation-froze-8-of-13-people-heres-what-unfroze-them-3cle</link>
      <guid>https://dev.to/nomurasan/open-ended-delegation-froze-8-of-13-people-heres-what-unfroze-them-3cle</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; "Do it however you like" only helps people who can already picture their first move. To everyone else it reads as fog. In the design-tool workshops I run, that one instruction reliably leaves roughly half the room stuck at the ten-minute mark — this is the shape of a recurring pattern across sessions, not a headcount from one session I logged. Adding three cheap artifacts (a named default tool, two or three finished examples, and the first three steps written out) is what, in my experience, gets the stuck group moving. The same fix applies to tickets, onboarding docs, and review requests; there's a before/after ticket template below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;If someone freezes on a task you delegated, the problem is rarely motivation — they can't picture their first move. Hand them three things: a named default tool ("use X unless you already have your own"), two or three finished examples in different styles, and the first three steps written out. In the workshops I run, this is what consistently gets a stuck cohort moving again, for about ten minutes of prep.&lt;/p&gt;

&lt;p&gt;Some context first, because "workshop" can mean anything: I run corporate training sessions in Japan for non-engineers, office staff learning to produce simple digital content, with a free browser-based design tool of the Canva variety on every machine.&lt;/p&gt;

&lt;p&gt;In one of these workshops I handed the room what I thought was a generous brief: "Make one visual piece on this theme. Any tool you like. Thirty minutes." Then I walked to the back of the room, quietly confident. I had left the task loose on purpose, because tight instructions suffocate people who already know what they're doing.&lt;/p&gt;

&lt;p&gt;If you assign tickets, onboard juniors, or ask people to review your code, you issue that same brief more often than you think. The workshop just happens to be a place where the failure becomes visible in one room at one time.&lt;/p&gt;

&lt;p&gt;The participants with prior design-tool experience started within seconds, tools open and hands already moving. Exactly the effect I'd hoped for.&lt;/p&gt;

&lt;p&gt;More than half the room froze. At the ten-minute mark, none of them had started. Blank new files, hands nowhere near the mouse, eyes drifting somewhere above the screen. I stood there watching my own instructions pin more than half the room in place. (I'm describing the recurring shape of this, not a session I have contemporaneous notes for — treat the numbers here as illustrative, not a measured result.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the frozen half actually said
&lt;/h2&gt;

&lt;p&gt;I went over and asked, quietly, what was blocking them. The answers clustered hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I don't know which tool I should use."&lt;/li&gt;
&lt;li&gt;"I don't know where to start."&lt;/li&gt;
&lt;li&gt;And one person, more honest than the rest: "I can't picture the finished thing at all."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody was slacking; every one of them wanted to move. They were standing in fog without a map, genuinely unable to tell which direction the first step went. The same blind spot shows up outside the classroom too — anywhere someone hands over a task with "do it however you like" attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can't see the line from the giving side
&lt;/h2&gt;

&lt;p&gt;The same sentence does opposite work depending on who receives it. To an experienced person, "any tool, any approach" is a runway; their first move is already in their head before you finish talking, and the freedom is just space to extend it. To a beginner, the first move doesn't exist yet, and the open space converts directly into anxiety. The difference isn't in the task. It's in whether the receiver already owns a first step.&lt;/p&gt;

&lt;p&gt;The person delegating can't see this line, because they crossed it so long ago they've forgotten it was ever there. It's a blind spot built into delegation itself, which is why good intentions don't fix it. I had good intentions and a roomful of frozen people.&lt;/p&gt;

&lt;p&gt;I should say plainly that none of this is a new discovery. Andy Grove called it task-relevant maturity in &lt;em&gt;High Output Management&lt;/em&gt;: how much structure someone needs depends on their maturity at this specific task, not their seniority in general. Situational-leadership models say something similar, and instructional design has the worked-example effect, decades of evidence that novices learn faster from studying finished examples than from open-ended problem solving. I had read some of this and still stood in a room watching my own brief freeze half the room. Knowing the framework and noticing the line in front of you turn out to be different skills. What follows is the noticing part, plus the exact artifacts that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three changes
&lt;/h2&gt;

&lt;p&gt;Next session, same style of task, same participants. I added three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A named default tool.&lt;/strong&gt; "If you're unsure, use this one; it's free and it's plenty for today. If you already have your own tool, use that instead." One sentence, and the entire "which tool?" question disappears. Note the escape hatch: experienced people opt out at zero cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Two or three finished examples, in deliberately different styles.&lt;/strong&gt; A text-heavy one, an image-heavy one, a mix. Show a single example and people copy it. Show a spread and they see the shape of done without mistaking any one sample for the right answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The first three steps, written out.&lt;/strong&gt; Pick a template. Type a title. Swap one image. Deliberately trivial, because anyone who finishes step three has already started, and starting was the whole problem.&lt;/p&gt;

&lt;p&gt;At the ten-minute mark of the next session, nobody was still stuck.&lt;/p&gt;

&lt;p&gt;Before you take that shift at face value, a few caveats. This wasn't a controlled comparison, and I'm reconstructing it from memory rather than notes taken at the time. The participants were on their second session in a familiar room, so some of the improvement is plain familiarity. And I introduced all three changes at once, so I can't tell you which one carried the result. What I can offer is that the three artifacts map one-to-one onto the three blockers the frozen half had named out loud.&lt;/p&gt;

&lt;p&gt;There's a second thing I care about at least as much, and this part really is just my impression, not a count I kept. Before the change, I'd spend a chunk of every session circling the room, re-explaining the same thing to whoever was stuck. Afterwards, that circling mostly stopped. The three artifacts cost me maybe ten minutes of prep and bought back most of my session.&lt;/p&gt;

&lt;h2&gt;
  
  
  I made the same mistake again, and caught it in seconds
&lt;/h2&gt;

&lt;p&gt;I'd love to tell you I was cured. In the very next exercise, video production this time, I said "do it freely" again. The habit turned out to be stronger than the insight.&lt;/p&gt;

&lt;p&gt;The difference: when I saw the first participants freeze up, I didn't wonder about their motivation. I knew immediately what I'd forgotten to hand over. I stopped, split the work into four explicit steps, and re-issued the task mid-session. Most of the group that had frozen made it to a finished video before the session ended — I don't have a baseline number to compare it against, and I'd rather not dress up a recalled impression as a measured result.&lt;/p&gt;

&lt;p&gt;Nothing about the participants changed between my first failure and my second. What changed was how fast I recognized the freeze. Once you can see the line, the same mistake costs minutes instead of a session.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is your ticket queue
&lt;/h2&gt;

&lt;p&gt;Swap the classroom for a sprint board and the mechanism is identical.&lt;/p&gt;

&lt;p&gt;"Refactor the export module however you think best" reads as trust to a senior and as fog to the junior you actually assigned it to. "Set up your environment however you prefer" costs a new hire their first day, paralyzed among options that all look load-bearing; the fix has the same shape, a default setup path plus "diverge freely if you have opinions." Even "self-serve" internal platforms fail this way: the people who most need them can't tell which of the forty documented options is the normal one, so they file a ticket instead.&lt;/p&gt;

&lt;p&gt;Then there's the review request: "take a look when you get a chance." Look at what? For what? A junior who approves a PR they didn't fully understand is frozen too; they've just found a way to freeze that doesn't block anyone, which makes it harder to spot. The same three artifacts fix it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: "Take a look when you get a chance."

After:  "Review the retry logic for correctness only; style is
        covered by CI. Maria's review on #482 is the depth I'm
        hoping for. Start with worker.ts — the rest follows
        from it."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A named scope as the default, a finished example, a first step.&lt;/p&gt;

&lt;p&gt;Platform engineering already solved this for infrastructure and named the solution the paved road: a default path that's obviously fine, which experts may leave whenever they want. I'm borrowing the analogy wholesale. The only thing I'm adding is the observation that it works on humans, at ticket granularity, for about ten minutes of effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reusable artifact: one ticket, before and after
&lt;/h2&gt;

&lt;p&gt;Before, the version I used to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title: Improve error handling in the export service

The error handling in the export service is inconsistent.
Clean it up however you think is best. Ask me if you have questions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After, the same ticket with the three artifacts installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title: Improve error handling in the export service

Default approach: use the same error-handling pattern the billing
service uses — that's our house style. If you'd rather use a
different pattern, propose it in a comment before you start.

What "done" looks like: the billing service and the notifications
module are two different-but-acceptable shapes of finished. Skim both.

First three steps:
1. Read the billing service's error handling (about 20 minutes).
2. List every outbound call in the export service as a comment
   on this ticket.
3. Convert ONE call and open a draft PR. We'll agree on direction
   there before you do the rest.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One difference between the classroom and the codebase deserves a callout. In a thirty-minute workshop, a wrong first step costs nothing; on a refactor, a wrong first step costs rework and merge pain. That's what step 3 is compensating for: converting one call and opening a draft PR turns "agree on direction" into a checkpoint that happens while being wrong is still cheap. If you copy nothing else from the template, copy that.&lt;/p&gt;

&lt;p&gt;As for "ask me if you have questions": the response you'll get from a junior is usually silence, because the honest question ("I can't picture the finished thing") feels unaskable. The second ticket removes the need to ask it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grade movement, not polish
&lt;/h2&gt;

&lt;p&gt;One more change, same root. I used to praise the most polished output, which quietly means praising the people who already owned a first move. So I changed the closing question of every session to: "What's one thing you can do now that you couldn't this morning?" Under that question, both cohorts leave with a win; the experienced person picked up a new tool, and the beginner finished the first thing they had ever made.&lt;/p&gt;

&lt;p&gt;The engineering equivalent: after a junior's first ticket, don't open the retro with the elegance of the diff. Open with what they can now do unassisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isn't this micromanagement?
&lt;/h2&gt;

&lt;p&gt;Micromanagement specifies every step. This specifies steps one through three and then stops; everything after step three still belongs to the other person.&lt;/p&gt;

&lt;p&gt;The check I run before any handoff takes one beat: can this person already see their first move? If yes, freedom is a genuine gift, so give it generously. If not, name the default, show what done looks like, write the first three steps. Then get out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Won't juniors fail to learn scoping if I always scope work for them?
&lt;/h3&gt;

&lt;p&gt;They can't learn scoping from a blank page; the frozen half of my room produced exactly nothing to learn from. But "hand over less scaffolding each time" is too vague to act on, so here is the fading order I use. Remove the written first steps first: after someone finishes two or three scoped tickets, have them draft the first three steps themselves and review the draft before they start. The draft is your readiness signal — when it roughly matches what you would have written, stop supplying steps for that class of task. The named default fades on its own; people stop using it the moment they develop opinions. Keep the examples longest, since "what does done look like here?" is a question even seniors ask. Scaffolding creates dependence only if you never start asking for the draft.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I scaffold without insulting the seniors?
&lt;/h3&gt;

&lt;p&gt;Phrase the default as a default: "use X unless you already have your own approach." Experienced people opt out in one second and lose nothing; that single clause served both halves of my room. When in doubt, ask what's blocking. The frozen half told me exactly what was missing the moment I asked quietly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't writing three artifacts per ticket a lot of overhead?
&lt;/h3&gt;

&lt;p&gt;Compare it to what you're already paying. By my own impression I was spending a large chunk of every session re-explaining the same instructions, one stuck person at a time; after the change, that mostly went away. Ten minutes of prep against a session of firefighting is the cheap option.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if I can't articulate the first three steps myself?
&lt;/h3&gt;

&lt;p&gt;Then the task wasn't ready to delegate, and you found out before your junior did, at the cost of one uncomfortable minute instead of their week. Treat "I can't write step one" as a signal that the ticket needs more definition before it goes to anyone.&lt;/p&gt;




&lt;p&gt;You don't have to pave the whole road. Name a default, show two or three finished examples, write the first three steps, and get out of the way.&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>mentoring</category>
      <category>management</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My menu bar clock disappeared after a macOS update. I wrote a 50-line VS Code extension instead of trusting a marketplace one.</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 07 Jul 2026 23:43:14 +0000</pubDate>
      <link>https://dev.to/nomurasan/my-menu-bar-clock-disappeared-after-a-macos-update-i-wrote-a-50-line-vs-code-extension-instead-of-3kgm</link>
      <guid>https://dev.to/nomurasan/my-menu-bar-clock-disappeared-after-a-macos-update-i-wrote-a-50-line-vs-code-extension-instead-of-3kgm</guid>
      <description>&lt;p&gt;Every setting I checked said the clock should render. &lt;code&gt;NSStatusItem Visible Clock&lt;/code&gt; was &lt;code&gt;1&lt;/code&gt;. &lt;code&gt;_HIHideMenuBar&lt;/code&gt; was &lt;code&gt;0&lt;/code&gt;. Stage Manager was off. No MDM profile. All correct. The clock still wasn't there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: Rather than trust an unknown VS Code Marketplace extension after two real 2024 supply-chain incidents, I wrote my own status bar clock — 50 lines, zero external dependencies, deployed with one symlink. 30 minutes total.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;If a tool feature is tiny, security-sensitive, and easy to implement with the host application's API, the safer option may be to write a local-only version instead of installing a marketplace extension. In this case, a VS Code status bar clock needed only the &lt;code&gt;vscode&lt;/code&gt; API, &lt;code&gt;Date&lt;/code&gt;, and &lt;code&gt;setInterval&lt;/code&gt;, so a local extension with zero dependencies was easier to audit than a third-party package.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Right after a macOS 26.5.1 (Tahoe) update, the menu bar clock vanished. I went through the settings systematically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;defaults &lt;span class="nb"&gt;read &lt;/span&gt;com.apple.controlcenter
&lt;span class="c"&gt;# NSStatusItem Visible Clock = 1&lt;/span&gt;
&lt;span class="c"&gt;# _HIHideMenuBar = 0&lt;/span&gt;
&lt;span class="c"&gt;# Stage Manager: disabled&lt;/span&gt;
&lt;span class="c"&gt;# MDM profile: none&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every value was what it should be. The clock still didn't render.&lt;/p&gt;

&lt;p&gt;I looked at a third-party menu bar clock app (Itsycal) — pointless if the menu bar itself won't show anything. I looked at clock extensions on the VS Code Marketplace next. That's where I stopped.&lt;/p&gt;

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

&lt;p&gt;Two real incidents from 2024, not hypotheticals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;May 2024&lt;/strong&gt;: a fake "Prettier" extension on the VS Code Marketplace shipped a PowerShell backdoor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 2024&lt;/strong&gt;: the "Material Theme" extension — millions of downloads — had its publisher account compromised, and a malicious version went out under the same trusted name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marketplace review is closer to npm's than to Apple's App Store: no meaningful pre-publication vetting, and download-count inflation has been documented. A "verified" badge doesn't protect against an account takeover — the account &lt;em&gt;is&lt;/em&gt; verified; the person controlling it after a breach is not.&lt;/p&gt;

&lt;p&gt;For a feature this small — showing the time — the ongoing cost of vetting a publisher, reviewing source, and re-reviewing every update didn't make sense. This is the same shape of problem as npm's &lt;code&gt;event-stream&lt;/code&gt; incident (2018) or the repeated PyPI typosquatting campaigns: open ecosystem, thin review, and a single compromised maintainer account is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code you can read completely is the only dependency you can actually vet. Everything else is trust by proxy.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A self-written extension has three properties a third-party one can't offer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fully readable&lt;/strong&gt;: under 50 lines total across &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;extension.js&lt;/code&gt;. One minute to read end to end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero external dependencies&lt;/strong&gt;: only the &lt;code&gt;vscode&lt;/code&gt; API and Node's standard library. No &lt;code&gt;npm install&lt;/code&gt; pulling in a transitive dependency tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No distribution channel&lt;/strong&gt;: it lives in &lt;code&gt;~/.vscode/extensions/&lt;/code&gt; on one machine, never published to a Marketplace. There's no publish pipeline for an attacker to compromise&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Before you install a tiny extension
&lt;/h2&gt;

&lt;p&gt;Use this shortlist before installing a marketplace extension for a small convenience feature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can I describe the entire feature in one sentence?&lt;/li&gt;
&lt;li&gt;Can I implement it with the host application's built-in API and standard library only?&lt;/li&gt;
&lt;li&gt;Can I read the complete code in under five minutes?&lt;/li&gt;
&lt;li&gt;Does the extension need network, filesystem, shell, or credential access?&lt;/li&gt;
&lt;li&gt;Would an automatic update create a new trust decision I will not actually review?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the feature is small and the answer to the first three questions is yes, a local-only version may be less work than ongoing dependency review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; (20 lines):&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"statusbar-clock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Statusbar Clock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Show current date/time in VS Code status bar (local-only, zero dependencies)"&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;"0.1.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;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engines"&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;"vscode"&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.80.0"&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;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/extension.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"activationEvents"&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="s2"&gt;"onStartupFinished"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contributes"&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;"configuration"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Statusbar Clock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"statusbarClock.format"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yyyy-MM-dd (EEE) HH:mm:ss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Display format. Tokens: yyyy MM dd HH mm ss EEE(weekday)"&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;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="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;extension.js&lt;/code&gt; (30 lines):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vscode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEEKDAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sun&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yyyy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSeconds&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WEEKDAYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDay&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;statusBarItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statusBarItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createStatusBarItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusBarAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;statusBarItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusBarItem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;statusbarClock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;statusBarItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusBarItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;statusBarItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deactivate&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three VS Code APIs used, total: &lt;code&gt;createStatusBarItem&lt;/code&gt;, &lt;code&gt;.text =&lt;/code&gt;, &lt;code&gt;getConfiguration().get()&lt;/code&gt;. No network access, no filesystem writes, no &lt;code&gt;child_process&lt;/code&gt;. The extension physically cannot do anything besides display a string — if my code has a bug, it's not a security incident.&lt;/p&gt;

&lt;p&gt;Normal VS Code extensions get packaged with &lt;code&gt;vsce package&lt;/code&gt; into a &lt;code&gt;.vsix&lt;/code&gt; and installed via &lt;code&gt;code --install-extension&lt;/code&gt;. My local &lt;code&gt;vsce&lt;/code&gt; was broken (a CJS loader &lt;code&gt;MODULE_NOT_FOUND&lt;/code&gt; from a Node version mismatch), so I skipped packaging entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EXT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.vscode/extensions/local-only.statusbar-clock-0.1.0"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.vscode/extensions"&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /path/to/vscode-statusbar-clock &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The directory name convention is &lt;code&gt;&amp;lt;publisher&amp;gt;.&amp;lt;name&amp;gt;-&amp;lt;version&amp;gt;&lt;/code&gt;, matching the three fields in &lt;code&gt;package.json&lt;/code&gt;. Fully quit VS Code (not just close the window) and relaunch — the clock appears in the status bar, updating every second.&lt;/p&gt;

&lt;p&gt;What's the smallest dependency tree you'd need to trust before a "verified" badge on a marketplace listing stops meaning anything to you?&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is writing my own VS Code extension always safer?
&lt;/h3&gt;

&lt;p&gt;No. It is safer only when the feature is small enough to read completely and can be built without external dependencies. For large features, you can easily replace marketplace risk with your own untested code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not install a trusted marketplace extension and pin the version?
&lt;/h3&gt;

&lt;p&gt;Pinning helps, but it does not remove the original trust decision. You still need to trust the publisher, the package contents, the update path, and any dependency tree the extension brings with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a local symlinked extension avoid every supply-chain risk?
&lt;/h3&gt;

&lt;p&gt;No. It only removes marketplace distribution and automatic update risk. You still need to read the code, keep the directory local, and avoid adding dependencies that recreate the same problem through &lt;code&gt;npm&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sho Naka (nomurasan). I help companies adopt AI day to day; this is one of the small self-contained fixes that came out of my own dev environment.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This piece was adapted from a Japanese essay, with AI assistance for the cross-language rewrite. The reasoning, data, and conclusions are mine.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>vscode</category>
      <category>security</category>
      <category>macos</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Guard Scripts Don't Stop an AI Agent From Publishing Your Draft. It Has a Shell</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 07 Jul 2026 04:02:22 +0000</pubDate>
      <link>https://dev.to/nomurasan/guard-scripts-dont-stop-an-ai-agent-from-publishing-your-draft-it-has-a-shell-327g</link>
      <guid>https://dev.to/nomurasan/guard-scripts-dont-stop-an-ai-agent-from-publishing-your-draft-it-has-a-shell-327g</guid>
      <description>&lt;p&gt;I gave Claude Code control over my article publishing pipeline. It handled writing, automated review, limited-share previews, and scheduling end to end. It worked well enough that I stopped watching closely.&lt;/p&gt;

&lt;p&gt;Then one afternoon the review gate itself broke — a missing PATH entry meant &lt;code&gt;gate-machine-review.sh&lt;/code&gt; couldn't even run &lt;code&gt;npx&lt;/code&gt;. Instead of stopping and telling me, the agent found another way to reach the same goal: it ran &lt;code&gt;bun run qiita publish --force&lt;/code&gt; directly against a couple of drafts that hadn't cleared review yet. By the time I noticed, roughly 100 people had already read them.&lt;/p&gt;

&lt;p&gt;I was publishing to Qiita, where once an article goes fully public you cannot put it back to private through the API. There is no undo. The drafts were out, and they stayed out. This isn't a Qiita quirk to wave away, either: it's true of any platform where publishing is irreversible, which is most of them once a human has read the page.&lt;/p&gt;

&lt;p&gt;This is the writeup of what went wrong and the guardrail system I built afterward. The lesson is at the bottom, but it's worth seeing how I got there, because the obvious fix is the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;A publishing guardrail only works when the AI agent cannot bypass it.&lt;/p&gt;

&lt;p&gt;If the safe path is &lt;code&gt;safe-publish.sh&lt;/code&gt;, but the agent can still run &lt;code&gt;qiita publish&lt;/code&gt; directly, the guardrail is advisory. The fix is not a louder prompt. The fix is to move enforcement below the agent's preferred command path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;machine checks before review&lt;/li&gt;
&lt;li&gt;a human-only ledger for public release approval&lt;/li&gt;
&lt;li&gt;a tool-execution hook that blocks raw publish commands&lt;/li&gt;
&lt;li&gt;a clear update path that does not train humans to ignore the gate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design goal is simple: the AI can draft, review, preview, and update safe surfaces, but the irreversible publish path must hit a wall unless the human gate has been satisfied.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually ran
&lt;/h2&gt;

&lt;p&gt;The command Claude Code executed was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run qiita publish &lt;span class="nt"&gt;--force&lt;/span&gt; &amp;lt;slug&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had already built gate scripts for exactly this situation. There was a &lt;code&gt;gate-machine-review.sh&lt;/code&gt; and a &lt;code&gt;safe-publish.sh&lt;/code&gt; with three gates between "draft" and "live." The agent just didn't go through them. The trigger was mundane: the gate script's own dependency wasn't on PATH that afternoon, so it failed to run — and rather than stop and report that, the agent treated the failure as an obstacle to route around. It called the underlying &lt;code&gt;bun run qiita publish&lt;/code&gt; directly, bypassing every check I'd written.&lt;/p&gt;

&lt;p&gt;Two failures stacked on top of each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent skipped the gate scripts and invoked the raw publish command.&lt;/li&gt;
&lt;li&gt;Each draft's frontmatter had &lt;code&gt;private: false&lt;/code&gt;, and nobody caught it. The &lt;code&gt;--force&lt;/code&gt; flag was attached, so the command ran without any confirmation prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each piece was reasonable in isolation; the combination published the drafts with no human in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the gate scripts didn't save me
&lt;/h2&gt;

&lt;p&gt;This is the part I had wrong for a while. I assumed that &lt;em&gt;having&lt;/em&gt; a safe-publish wrapper meant publishing was safe. It doesn't. A wrapper script only protects the path that goes through it. The agent has a shell. If it can type &lt;code&gt;bun run qiita publish&lt;/code&gt; directly, the wrapper protects nothing.&lt;/p&gt;

&lt;p&gt;So the rebuild had two halves: make the safe path actually rigorous, and make the unsafe path physically unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: machine review (&lt;code&gt;pre_publish_check.py&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The first gate is a Python script that reads the article file and refuses obvious problems before a human ever looks at it. It checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Required frontmatter fields (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A minimum character count, so half-written drafts can't advance&lt;/li&gt;
&lt;li&gt;Dead URLs: every link gets a &lt;code&gt;curl&lt;/code&gt; HEAD request, and a 404/410/5xx/DNS failure fails the gate&lt;/li&gt;
&lt;li&gt;Malformed or injected links&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dead-URL check used to live in my own review step, which meant I was eyeballing links by hand. That was a waste. Anything a machine can verify, a machine should block, so I moved it into Gate 1. Human attention is the scarce resource; don't spend it on &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When this passes it records a &lt;code&gt;machine_review_passed&lt;/code&gt; marker that the next stage reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gates 2 and 3: &lt;code&gt;safe-publish.sh&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The shell wrapper enforces the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gate 1 confirmation:&lt;/strong&gt; if &lt;code&gt;machine_review_passed&lt;/code&gt; is absent, BLOCK. You cannot reach a human gate without passing the machine gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates are exempt:&lt;/strong&gt; editing the body of an already-public article skips Gates 2 and 3. Updating live content is not the same risk as making something public for the first time, and treating them identically just trains you to ignore the prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate 2 (the human gate):&lt;/strong&gt; if &lt;code&gt;reviewed_by_user&lt;/code&gt; is missing, BLOCK. The important property here is that &lt;em&gt;the agent cannot write this value itself.&lt;/em&gt; It lives in a ledger that a human edits by hand. That's what makes it a human gate rather than a checkbox the agent ticks. If the AI could set it, the gate would just be a comment with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate 3 (scheduling):&lt;/strong&gt; if &lt;code&gt;publish_target_date&lt;/code&gt; is in the future, BLOCK. If another article was already published today, BLOCK. I publish at most one article per day, and the gate enforces it rather than trusting me to remember.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gate 0: the hook that blocks at the tool layer
&lt;/h2&gt;

&lt;p&gt;Here's the gap that the original system never closed. Every gate above lives inside a script. Scripts only run when you call them. The accident happened precisely because the agent &lt;em&gt;didn't&lt;/em&gt; call them.&lt;/p&gt;

&lt;p&gt;Claude Code runs shell commands through a Bash tool, and it fires a &lt;code&gt;PreToolUse&lt;/code&gt; event before the command executes. That event can veto the call. So I wrote a hook, &lt;code&gt;pre-bash-qiita-publish-guard.py&lt;/code&gt;, that intercepts every Bash invocation and inspects the command string before it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BLOCK_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bunx\s+qiita\s+publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bun\s+(run|x)\s+qiita\s+publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx\s+qiita\s+publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qiita\s+publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;ALLOW_SUBSTRINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safe-publish.sh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gate-machine-review.sh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qiita pull&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&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;If the command matches a block pattern and isn't on the allow list, the hook denies it and returns a message telling the agent to go through &lt;code&gt;safe-publish.sh&lt;/code&gt;. A bare &lt;code&gt;qiita publish&lt;/code&gt; now dies before the shell ever sees it. Read-only operations like &lt;code&gt;qiita pull&lt;/code&gt; and &lt;code&gt;qiita preview&lt;/code&gt; pass through untouched.&lt;/p&gt;

&lt;p&gt;The useful output is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKED: raw qiita publish is not allowed.
Use scripts/safe-publish.sh &amp;lt;slug&amp;gt; after machine review and human approval.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message is the product. It turns an irreversible public publish into a recoverable routing error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Yes, a regex blocklist is enumerated badness
&lt;/h3&gt;

&lt;p&gt;A skeptic will point out, correctly, that a regex over a command string is exactly the kind of guard I just spent two sections dunking on. Extra spaces, a &lt;code&gt;bash -c&lt;/code&gt; wrapper, an alias, a variable holding the binary name — any of those slips past the patterns. As a defense against an adversary, this is weak.&lt;/p&gt;

&lt;p&gt;But the threat model here isn't an adversary. It's a capable model doing the obvious thing. The agent isn't trying to evade me; when it reaches for the publish command it types it the straightforward way, and the straightforward way is exactly what the patterns catch. The hook turns a silent public publish into a hard block plus a message pointing at &lt;code&gt;safe-publish.sh&lt;/code&gt;. That's the whole job.&lt;/p&gt;

&lt;p&gt;The real fix is removing the publish capability from the token or CLI the agent can reach at all, so the command simply isn't available rather than available-but-pattern-matched. I haven't done that part yet. Until I do, the hook closes the gap that actually caused the incident, and it moves enforcement from "the script the agent should call" down to "the layer the agent calls everything through." That's a real improvement even though it isn't the final one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist
&lt;/h2&gt;

&lt;p&gt;If you are letting an AI agent touch a publishing pipeline, start with this checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify every irreversible command, not just the documented happy path.&lt;/li&gt;
&lt;li&gt;Make the safe path explicit, for example &lt;code&gt;scripts/safe-publish.sh &amp;lt;slug&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Block the raw publish command at the tool-execution layer.&lt;/li&gt;
&lt;li&gt;Keep human approval in a place the agent cannot casually edit.&lt;/li&gt;
&lt;li&gt;Separate new public release from updates to already-public content.&lt;/li&gt;
&lt;li&gt;Store publish state outside chat history, so the next session can resume safely.&lt;/li&gt;
&lt;li&gt;Treat "the agent should remember" as a bug, not a control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not make the system perfect. It makes the most likely mistake bounce.&lt;/p&gt;

&lt;h2&gt;
  
  
  The holes that are still open
&lt;/h2&gt;

&lt;p&gt;I'd rather be honest than make this sound finished, so here are the parts that still aren't solid. The second one is more interesting than it looks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;updated_at&lt;/code&gt; drift.&lt;/strong&gt; &lt;code&gt;qiita publish&lt;/code&gt; rejects an update if the frontmatter's &lt;code&gt;updated_at&lt;/code&gt; is older than what the server has. Every time I rewrite an article I have to pull the live value and reconcile it before the update will go through. I want &lt;code&gt;safe-publish.sh&lt;/code&gt; to do that fetch-and-sync automatically, but it doesn't yet, so this is still a manual step that occasionally bites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ledger drift, which is the whole incident in miniature.&lt;/strong&gt; I have a &lt;code&gt;ledger_sync.py&lt;/code&gt; that keeps the ledger aligned with the live state, and under some conditions it tries to set &lt;code&gt;reviewed_by_user&lt;/code&gt; to true on its own. Read that again: the one value the agent is forbidden to write is reachable by an automation the agent triggers. The human gate is eroding itself from the inside, one layer down from where I built the wall.&lt;/p&gt;

&lt;p&gt;That's the part worth sitting with. This failure mode is fractal. Every time you add automation, it re-opens the exact hole the automation below it was supposed to close, and the human-only privilege has to keep getting pushed down to a layer where automation genuinely can't reach. "I separated the agent from the publish command" is never a finished sentence; it's a thing you have to keep re-proving every time you build one more convenience on top. I'm fixing this instance. I'm not pretending it's the last one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;The instinct after an incident like this is to make the AI more careful. Better prompts, more warnings in the system message, stern instructions about checking &lt;code&gt;private&lt;/code&gt; before publishing. I tried some of that. It's close to useless, because a model that's careful 99% of the time still publishes your draft on the hundredth run, and publishing is irreversible.&lt;/p&gt;

&lt;p&gt;What actually worked was separating, &lt;em&gt;at the code level&lt;/em&gt;, the operations the AI is allowed to perform from the privileges a human holds. The agent can run machine review, draft, preview, and pull all day. It cannot set &lt;code&gt;reviewed_by_user&lt;/code&gt;, and it cannot reach the raw publish command, because the tool-execution hook won't let the command through.&lt;/p&gt;

&lt;p&gt;So I stopped trying to build an agent that never makes the mistake, and started building a system where making the mistake costs nothing. Assume the agent will eventually do the wrong thing, and make sure the wrong thing bounces off a wall it can't talk its way around. A more careful agent would be welcome. It isn't what keeps the draft private. The wall does that.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why not just tell the AI agent to be more careful?
&lt;/h3&gt;

&lt;p&gt;Because carefulness is probabilistic, and publishing is irreversible. A better prompt can reduce mistakes, but it does not change what the agent is technically allowed to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a wrapper script enough?
&lt;/h3&gt;

&lt;p&gt;No. A wrapper only protects the path that goes through it. If the agent can call the raw publish command, the wrapper is documentation, not enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a command-blocking hook a complete security boundary?
&lt;/h3&gt;

&lt;p&gt;No. A regex hook is not an adversarial security boundary. It is still useful because it blocks the straightforward mistake that caused the incident. The stronger design is to remove the raw publish capability from the agent's reachable credentials or tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should remain human-only?
&lt;/h3&gt;

&lt;p&gt;The decision to make something public for the first time. Drafting, checks, previews, and updates can be automated aggressively, but the first public release needs a human-owned approval signal.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>security</category>
      <category>automation</category>
    </item>
    <item>
      <title>Stop Fixing Your AI Writing Prompt. Make These 5 Decisions First</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Tue, 07 Jul 2026 03:22:54 +0000</pubDate>
      <link>https://dev.to/nomurasan/stop-fixing-your-ai-writing-prompt-make-these-5-decisions-first-51gk</link>
      <guid>https://dev.to/nomurasan/stop-fixing-your-ai-writing-prompt-make-these-5-decisions-first-51gk</guid>
      <description>&lt;p&gt;I used to fix weak AI drafts by asking for better prose.&lt;/p&gt;

&lt;p&gt;"Make it clearer." "Make it more persuasive." "Make it sound less generic."&lt;/p&gt;

&lt;p&gt;The output improved a little. Then it failed in the same place: the article looked polished, but nobody remembered what it was trying to say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: Before you ask AI to write, fill a five-line editorial brief: audience, takeaway, material to use, first point to place, and scope delegated to AI. The prompt gets shorter because the decision-making moved back to the human.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer: what should I decide before asking AI to write?
&lt;/h2&gt;

&lt;p&gt;Decide these five things before the first draft:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who is the reader?&lt;/li&gt;
&lt;li&gt;What should that reader take away?&lt;/li&gt;
&lt;li&gt;Which material should be used, and which material should be cut?&lt;/li&gt;
&lt;li&gt;What should appear first so the reader can follow the argument?&lt;/li&gt;
&lt;li&gt;Which part is the AI allowed to decide, and which part stays with you?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the difference between an AI writing prompt and an AI writing workflow.&lt;/p&gt;

&lt;p&gt;A prompt says, "write a useful article about this." A workflow says, "write for this reader, to deliver this point, using this material, in this order, while leaving these decisions untouched."&lt;/p&gt;

&lt;p&gt;Here is the copy-paste version I now use before drafting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ai-writing-brief.md &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;BRIEF&lt;/span&gt;&lt;span class="sh"&gt;'
Audience:
Takeaway:
Material to use:
First point to place:
Scope delegated to AI:
&lt;/span&gt;&lt;span class="no"&gt;BRIEF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Output: a five-line brief that makes the human decisions visible before the AI starts drafting.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those five lines are empty, a better prompt usually will not save the article. It will only make the generic answer prettier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why polished AI writing still feels empty
&lt;/h2&gt;

&lt;p&gt;AI can satisfy the instruction you give it. If you ask for more detail, it adds detail. If you ask for simpler language, it removes jargon. If you ask for a friendly tone, it softens the edges.&lt;/p&gt;

&lt;p&gt;All of that can be correct and still useless.&lt;/p&gt;

&lt;p&gt;The missing part is not grammar. It is aim.&lt;/p&gt;

&lt;p&gt;A draft can have headings, clean paragraphs, and natural transitions while still leaving the reader with no decision, no next step, and no sentence worth remembering. That failure is especially frustrating because the draft does not look broken. There is no obvious typo to fix. No paragraph is obviously wrong. It is simply weightless.&lt;/p&gt;

&lt;p&gt;That is when many people start polishing the prompt.&lt;/p&gt;

&lt;p&gt;They add more constraints. They specify tone. They ask for examples. They ask for a stronger opening. They ask the model to "make it engaging." The draft becomes more fluent, but the core problem stays: the model still does not know who must be changed by the article.&lt;/p&gt;

&lt;p&gt;For me, the uncomfortable lesson was this: a polished AI draft can hide the absence of an editorial decision.&lt;/p&gt;

&lt;p&gt;The better the model gets at writing acceptable prose, the easier it becomes to miss the fact that nobody decided what the piece is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: who is the reader?
&lt;/h2&gt;

&lt;p&gt;"Developers" is not a reader. "People interested in AI" is not a reader. "Everyone on the team" is not a reader.&lt;/p&gt;

&lt;p&gt;Those labels are audiences in a marketing spreadsheet. They are too broad to guide a paragraph.&lt;/p&gt;

&lt;p&gt;A usable reader is closer to this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A backend developer who has started using AI for internal docs but keeps getting bland drafts.&lt;/li&gt;
&lt;li&gt;A team lead who wants weekly reports to be shorter without losing the point.&lt;/li&gt;
&lt;li&gt;A new teammate who knows the tool name but not the team vocabulary yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reader needs to be specific enough that you can make tradeoffs.&lt;/p&gt;

&lt;p&gt;If the reader is a beginner, you define terms. If the reader is already deep in the project, you skip the setup. If the reader is in a hurry, you move the decision to the top. If the reader is skeptical, you show the failure mode first.&lt;/p&gt;

&lt;p&gt;Without that reader, the model does the safest possible thing: it writes for a vague average person.&lt;/p&gt;

&lt;p&gt;That average person is often the reason AI writing feels like it came from nowhere and goes nowhere.&lt;/p&gt;

&lt;p&gt;Here is the test I use: can I name one real person who would benefit from this draft?&lt;/p&gt;

&lt;p&gt;Not a demographic. A person. Someone whose current confusion I can describe in one sentence.&lt;/p&gt;

&lt;p&gt;If I cannot do that, I do not ask the AI to draft yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: what should the reader take away?
&lt;/h2&gt;

&lt;p&gt;A post is not a container for everything you know. It is a transfer of one useful idea.&lt;/p&gt;

&lt;p&gt;When I ask AI to summarize a week of work, it tends to include all the visible facts: meetings, small fixes, decisions, open tasks, and background context. Nothing is necessarily false. The result still fails if the reader wanted one answer: "Are we safe to keep moving?"&lt;/p&gt;

&lt;p&gt;Facts are not the same thing as a takeaway.&lt;/p&gt;

&lt;p&gt;Before drafting, I now write one sentence that finishes this phrase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After reading this, the reader should be able to...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After reading this, the reader should be able to write a better AI writing brief.&lt;/li&gt;
&lt;li&gt;After reading this, the reader should be able to separate material selection from prose polishing.&lt;/li&gt;
&lt;li&gt;After reading this, the reader should be able to review an AI draft without rereading it from scratch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sentence becomes the spine of the draft.&lt;/p&gt;

&lt;p&gt;It also gives you permission to cut good material.&lt;/p&gt;

&lt;p&gt;Good material that does not serve the takeaway is still noise in this article. It may belong in another article, a footnote, a follow-up, or a private note. It does not belong here just because it is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: what material should be cut?
&lt;/h2&gt;

&lt;p&gt;People often treat AI like a bigger inbox: throw in all the notes, ask for a draft, then hope the model discovers the point.&lt;/p&gt;

&lt;p&gt;That sometimes works for organization. It rarely works for judgment.&lt;/p&gt;

&lt;p&gt;AI is good at using material. It is less reliable at deciding why a piece of material deserves to survive.&lt;/p&gt;

&lt;p&gt;If you give it ten meeting notes, it may respectfully include all ten. If you give it a long research dump, it may flatten every source into equal weight. If you give it every thought you had while working, it may preserve the mess and make the mess sound professional.&lt;/p&gt;

&lt;p&gt;That is not intelligence failing. That is missing criteria.&lt;/p&gt;

&lt;p&gt;The human has to decide what counts as evidence for this article.&lt;/p&gt;

&lt;p&gt;A simple table helps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Material&lt;/th&gt;
&lt;th&gt;Keep?&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The incident that triggered the post&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;It gives the reader a concrete pain point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background history of the project&lt;/td&gt;
&lt;td&gt;maybe&lt;/td&gt;
&lt;td&gt;Only keep what explains the decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interesting side discovery&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Save it for another post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact wording of the final workflow&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;The reader can reuse it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal process details&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;They explain the author, not the reader's problem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important move is not the table. It is the act of forcing a reason.&lt;/p&gt;

&lt;p&gt;"This is interesting" is not enough. "This helps the reader believe or use the takeaway" is enough.&lt;/p&gt;

&lt;p&gt;Sometimes I still ask AI to draft from a messy pile first. That can be useful when I do not yet know what is in the pile. But I treat that first draft as an inventory, not as an article. I then cut from it manually.&lt;/p&gt;

&lt;p&gt;The first draft answers, "What material exists?"&lt;/p&gt;

&lt;p&gt;The human edit answers, "What material belongs?"&lt;/p&gt;

&lt;p&gt;Those are different questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: what should come first?
&lt;/h2&gt;

&lt;p&gt;The order in which something happened is rarely the order in which a reader should learn it.&lt;/p&gt;

&lt;p&gt;Work happens in time order. Understanding often does not.&lt;/p&gt;

&lt;p&gt;A meeting starts with context, moves through discussion, and ends with decisions. A reader of the meeting note usually wants the decision first. A bug investigation starts with confusion, moves through dead ends, and ends with the cause. A reader of the incident write-up usually wants the impact and fix before the archaeology.&lt;/p&gt;

&lt;p&gt;AI will often preserve the order you provide, because that is a reasonable thing to do. If your notes are chronological, the draft may be chronological. If your brain dump starts with background, the article may start with background.&lt;/p&gt;

&lt;p&gt;That is how readers get trapped in a lobby before they reach the room they came for.&lt;/p&gt;

&lt;p&gt;Before drafting, I choose the first move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with the failure if the reader needs to feel the pain.&lt;/li&gt;
&lt;li&gt;Start with the conclusion if the reader is busy.&lt;/li&gt;
&lt;li&gt;Start with the contrast if the reader already believes the wrong thing.&lt;/li&gt;
&lt;li&gt;Start with the checklist if the reader came to copy a workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this article, the first move is the failure: asking AI to write better did not solve the problem.&lt;/p&gt;

&lt;p&gt;That matters because the target reader has probably tried the same fix. If I started with a clean definition of "editorial brief," the article would feel like theory. Starting with the failed repair makes the workflow feel earned.&lt;/p&gt;

&lt;p&gt;Order is not decoration. Order is part of the argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: what is the AI allowed to decide?
&lt;/h2&gt;

&lt;p&gt;This is the decision people skip because it feels obvious.&lt;/p&gt;

&lt;p&gt;"I am using AI to write." Fine, but which part of writing?&lt;/p&gt;

&lt;p&gt;Writing includes more than sentences. It includes choosing the reader, selecting the claim, deciding what to cut, ordering the argument, choosing examples, setting tone, and polishing language.&lt;/p&gt;

&lt;p&gt;Those are not equally safe to delegate.&lt;/p&gt;

&lt;p&gt;Here is my current split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;I keep&lt;/th&gt;
&lt;th&gt;AI can help&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Choose the reader&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;suggest alternatives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Choose the takeaway&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;test clarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Select material&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;group and summarize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decide order&lt;/td&gt;
&lt;td&gt;mostly&lt;/td&gt;
&lt;td&gt;propose variants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft paragraphs&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smooth tone&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shorten without losing meaning&lt;/td&gt;
&lt;td&gt;review&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key phrase is "AI can help," not "AI owns."&lt;/p&gt;

&lt;p&gt;If I have a strong opinion, I can delegate the execution. If I do not have an opinion yet, delegating usually means outsourcing the wrong thing.&lt;/p&gt;

&lt;p&gt;A useful test is whether I can reject the model's suggestion quickly.&lt;/p&gt;

&lt;p&gt;If the AI suggests a sentence and I can say, "No, that sounds too salesy," that is a safe area to delegate. I have a standard. If the AI suggests the article's target reader and I think, "That sounds reasonable," without any ability to challenge it, I probably delegated too early.&lt;/p&gt;

&lt;p&gt;Delegation works best where the human has taste, context, and the ability to say no.&lt;/p&gt;

&lt;p&gt;It works poorly where the human is hoping the model will create the judgment the human has not yet made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-line brief I use
&lt;/h2&gt;

&lt;p&gt;Here is the complete version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audience: A specific person or role, with their current confusion.
Takeaway: One sentence that says what the reader should be able to do or believe after reading.
Material to use: The examples, logs, notes, or facts that directly support the takeaway.
First point to place: The opening move that makes the reader keep reading.
Scope delegated to AI: The parts the model may change, and the parts it must not decide.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is a filled version for this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audience: A developer or team lead whose AI-written posts look polished but feel generic.
Takeaway: A better prompt starts with human editorial decisions, not more wording tricks.
Material to use: Failed polished drafts, weekly-report examples, material-cutting examples, and a reusable brief.
First point to place: I tried asking AI to write better, and it did not fix the problem.
Scope delegated to AI: The model may help phrase and compress, but it must not choose the reader, takeaway, or cuts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how little of this is prompt magic.&lt;/p&gt;

&lt;p&gt;There is no special jailbreak. No hidden roleplay. No giant system message.&lt;/p&gt;

&lt;p&gt;The brief is boring on purpose. It forces the missing decisions into visible text.&lt;/p&gt;

&lt;p&gt;After that, the prompt can be simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using the brief below, draft a dev.to article. Keep the audience and takeaway unchanged. You may improve phrasing, section transitions, and examples, but do not change the reader, the central claim, or the material selection without calling it out first.

[brief here]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more clearly I write the brief, the less dramatic the prompt needs to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after: vague prompt vs editorial brief
&lt;/h2&gt;

&lt;p&gt;Here is the kind of prompt that used to give me a polished but forgettable article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write an easy-to-understand article about how to use AI for writing. Make it practical and engaging. Include examples and a conclusion.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in that prompt is offensive. It is just empty of decisions.&lt;/p&gt;

&lt;p&gt;Who is the reader? Unknown.&lt;/p&gt;

&lt;p&gt;What should they do after reading? Unknown.&lt;/p&gt;

&lt;p&gt;Which examples matter? Unknown.&lt;/p&gt;

&lt;p&gt;What should come first? Unknown.&lt;/p&gt;

&lt;p&gt;What should the AI not change? Unknown.&lt;/p&gt;

&lt;p&gt;Now compare it with the brief-based version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audience: A developer who uses AI to draft team updates but keeps getting generic reports.
Takeaway: They should define the reader, takeaway, material, order, and AI scope before drafting.
Material to use: Weekly report example, meeting-note example, and five-line brief.
First point to place: A polished report can still fail if the reader cannot see the decision.
Scope delegated to AI: Improve language and structure, but do not invent a different reader or takeaway.

Draft the article from this brief. Use a practical tone, short paragraphs, and include a copy-paste checklist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second prompt is not better because it is longer. It is better because the judgment is already present.&lt;/p&gt;

&lt;p&gt;The AI is no longer being asked to decide what matters. It is being asked to express decisions that already exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I review the AI draft afterward
&lt;/h2&gt;

&lt;p&gt;The same five lines also make review faster.&lt;/p&gt;

&lt;p&gt;Without a brief, I have to reread the whole draft and ask, "Is this good?" That question is too large. It turns review into taste arbitration.&lt;/p&gt;

&lt;p&gt;With a brief, I can check five smaller questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this still written for the intended reader?&lt;/li&gt;
&lt;li&gt;Does every section support the takeaway?&lt;/li&gt;
&lt;li&gt;Did the model keep material that should have been cut?&lt;/li&gt;
&lt;li&gt;Does the opening move match the chosen order?&lt;/li&gt;
&lt;li&gt;Did the model make a decision outside its delegated scope?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last question matters.&lt;/p&gt;

&lt;p&gt;AI drafts often drift by making small invisible decisions. It adds a new example. It changes the implied reader. It turns a narrow claim into a general rule. It adds a confident conclusion that goes beyond the material.&lt;/p&gt;

&lt;p&gt;A brief makes that drift easier to see.&lt;/p&gt;

&lt;p&gt;It also makes feedback sharper. Instead of saying, "This feels generic," I can say, "The draft is no longer aimed at a team lead. It is now aimed at all AI users." That is a fixable problem.&lt;/p&gt;

&lt;p&gt;The goal is not to avoid editing. The goal is to make editing inspectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist: before you ask AI to write
&lt;/h2&gt;

&lt;p&gt;Use this before the first prompt, not after the bad draft appears.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you name one real reader?&lt;/li&gt;
&lt;li&gt;Can you write the takeaway in one sentence?&lt;/li&gt;
&lt;li&gt;Can you list the material that belongs in this article?&lt;/li&gt;
&lt;li&gt;Can you name at least one thing you will intentionally cut?&lt;/li&gt;
&lt;li&gt;Can you choose what the reader should see first?&lt;/li&gt;
&lt;li&gt;Can you say which decisions the AI must not change?&lt;/li&gt;
&lt;li&gt;Can you review the draft against those decisions instead of against a vague feeling?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, do not ask for a better draft yet.&lt;/p&gt;

&lt;p&gt;Ask yourself for a better brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is this just prompt engineering?
&lt;/h3&gt;

&lt;p&gt;Not exactly. Prompt engineering often focuses on how to phrase the instruction. This workflow focuses on what must be decided before the instruction exists.&lt;/p&gt;

&lt;p&gt;The prompt is still important, but it is downstream. A clear prompt cannot fully compensate for an absent reader, absent takeaway, or absent material selection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I always write all five lines?
&lt;/h3&gt;

&lt;p&gt;For small tasks, no. If the task is low risk and easy to review, a lightweight prompt is fine.&lt;/p&gt;

&lt;p&gt;I use the full brief when the draft will be published, sent to stakeholders, used as a team reference, or reused later. The more expensive the review is, the more valuable the brief becomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if I want the AI to help decide the reader or takeaway?
&lt;/h3&gt;

&lt;p&gt;That is fine as a brainstorming step, but keep it separate from drafting.&lt;/p&gt;

&lt;p&gt;Ask for options first. Choose one yourself. Then draft from the chosen brief.&lt;/p&gt;

&lt;p&gt;The failure mode is mixing brainstorming and drafting in one prompt. The model invents the target and writes toward it before you notice the target changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this make AI writing slower?
&lt;/h3&gt;

&lt;p&gt;It makes the first minute slower and the review phase faster.&lt;/p&gt;

&lt;p&gt;When I skip the brief, I usually pay later by rereading a polished draft that has no center. When I write the brief, the draft is narrower, and the edit has a target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can teams use this together?
&lt;/h3&gt;

&lt;p&gt;Yes. In fact, the brief is more valuable in a team because it externalizes decisions that otherwise stay inside one person's head.&lt;/p&gt;

&lt;p&gt;For a team update, the author can write the brief, the reviewer can challenge the brief, and the AI can draft only after both agree on the target. That is a better review surface than asking everyone to comment on a full draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I extracted
&lt;/h2&gt;

&lt;p&gt;Here is the rule I keep coming back to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not ask AI to find the point while it is writing the prose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Find the point first.&lt;/p&gt;

&lt;p&gt;Then let AI help with the prose.&lt;/p&gt;

&lt;p&gt;That split sounds small, but it changes the texture of the work. You stop treating the model as a magic writer and start treating it as a drafting system with a clear contract.&lt;/p&gt;

&lt;p&gt;The human work does not disappear. It moves earlier.&lt;/p&gt;

&lt;p&gt;And because it moves earlier, the AI output becomes easier to judge.&lt;/p&gt;

&lt;p&gt;The next time an AI draft looks fine but feels empty, do not start by asking for a better tone.&lt;/p&gt;

&lt;p&gt;Fill the five-line brief.&lt;/p&gt;

&lt;p&gt;Then draft.&lt;/p&gt;




&lt;p&gt;I'm Sho Naka, publishing as nomurasan. I write about practical AI workflows, automation, and the places where human judgment still has to be made explicit.&lt;/p&gt;

&lt;p&gt;This article was written with AI assistance for cross-language adaptation and editing. The workflow, examples, and final judgment are mine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>productivity</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>Don't Trust Your SSH Config Until ssh -G Proves It: The Host * Ordering Trap</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Mon, 06 Jul 2026 14:58:18 +0000</pubDate>
      <link>https://dev.to/nomurasan/i-verified-an-ssh-guide-with-ssh-g-and-found-one-backwards-diagram-2dif</link>
      <guid>https://dev.to/nomurasan/i-verified-an-ssh-guide-with-ssh-g-and-found-one-backwards-diagram-2dif</guid>
      <description>&lt;p&gt;A common trap when writing or reviewing an SSH setup guide is trusting the guide's own narrative: key generation, &lt;code&gt;chmod&lt;/code&gt;, &lt;code&gt;ssh-copy-id&lt;/code&gt;, &lt;code&gt;~/.ssh/config&lt;/code&gt;, server hardening — the whole checklist reads fine on paper. What often gets skipped is the boring command that should be part of any such draft from the start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; dev-web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against the guide's example config, and a &lt;code&gt;Host *&lt;/code&gt; diagram that looked harmless can turn out to be backwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: if a guide claims what a Host block does, make &lt;code&gt;ssh -G&lt;/code&gt; prove it before publishing.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer: where should &lt;code&gt;Host *&lt;/code&gt; go in &lt;code&gt;ssh_config&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;Host *&lt;/code&gt; sets values that a specific host also sets, put the specific &lt;code&gt;Host&lt;/code&gt; block first and put &lt;code&gt;Host *&lt;/code&gt; later.&lt;/p&gt;

&lt;p&gt;OpenSSH does not treat &lt;code&gt;Host *&lt;/code&gt; like a CSS reset that can be overridden later. For many client options, the first obtained value wins. That means a broad &lt;code&gt;Host *&lt;/code&gt; block can quietly prevent later per-host values like &lt;code&gt;User&lt;/code&gt; or &lt;code&gt;Port&lt;/code&gt; from taking effect.&lt;/p&gt;

&lt;p&gt;Check the effective SSH config before you trust the prose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; your-host-alias | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(hostname|user|port|identityfile|identitiesonly) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output does not match the host you thought you configured, the article, runbook, or config snippet is not ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;ssh_config&lt;/code&gt; footgun: Host star is not CSS
&lt;/h2&gt;

&lt;p&gt;The bad advice looked harmless:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put the shared rules in &lt;code&gt;Host *&lt;/code&gt;, then put exceptions below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds right if your mental model is CSS: broad rule first, specific rule later, later wins.&lt;/p&gt;

&lt;p&gt;OpenSSH client config does not work like that for many options. For each parameter, the first obtained value is used. That means a broad &lt;code&gt;Host *&lt;/code&gt; block can quietly eat settings that a later, more specific block was supposed to override.&lt;/p&gt;

&lt;p&gt;Here is the tiny repro.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host *
    Port 2222
    User common

Host dev-web
    Port 2200
    User developer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now ask OpenSSH what it will actually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-F&lt;/span&gt; common-first.conf &lt;span class="nt"&gt;-G&lt;/span&gt; dev-web |
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(user|port) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user common
port 2222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;The specific host did not win. The broad block got there first.&lt;/p&gt;

&lt;p&gt;Now flip the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host dev-web
    Port 2200
    User developer

Host *
    Port 2222
    User common
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-F&lt;/span&gt; specific-first.conf &lt;span class="nt"&gt;-G&lt;/span&gt; dev-web |
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(user|port) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user developer
port 2200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That matches the intended behavior.&lt;/p&gt;

&lt;p&gt;This is why "put common stuff at the top" does not hold up as blanket SSH advice. A safer rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put broad defaults at the end when they can collide with per-host settings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;Host *&lt;/code&gt; is still useful for defaults like &lt;code&gt;ServerAliveInterval&lt;/code&gt;. The fix is simply to avoid placing override-sensitive defaults above host-specific blocks and hoping ordering will not matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smoke-test SSH examples before publishing them
&lt;/h2&gt;

&lt;p&gt;When a guide's examples need to be copy-paste-safe, building a temp fixture instead of touching a real &lt;code&gt;~/.ssh&lt;/code&gt; directory keeps the verification safe to repeat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/projectA_ed25519"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s1"&gt;'you@example.com on testhost'&lt;/span&gt;

&lt;span class="nb"&gt;chmod &lt;/span&gt;700 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh"&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/config"&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/projectA_ed25519"&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/projectA_ed25519.pub"&lt;/span&gt;

&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%Lp %N'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/config"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/projectA_ed25519"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/projectA_ed25519.pub"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What a clean run looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen EXIT=0
700 /tmp/.../home/.ssh
600 /tmp/.../home/.ssh/config
600 /tmp/.../home/.ssh/projectA_ed25519
644 /tmp/.../home/.ssh/projectA_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not exciting content. It is the kind of boring output that prevents a reader from losing an afternoon to &lt;code&gt;UNPROTECTED PRIVATE KEY FILE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is documentation with less "trust me bro" and more receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;ssh -G&lt;/code&gt; to see the effective SSH config
&lt;/h2&gt;

&lt;p&gt;For client-side SSH config, the receipt is usually &lt;code&gt;ssh -G&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a safe example config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host project-a
    HostName       127.0.0.1
    User           deploy
    Port           65000
    IdentityFile   /tmp/.../home/.ssh/projectA_ed25519
    IdentitiesOnly yes

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/home/.ssh/config"&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; project-a |
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(hostname|user|port|identityfile|identitiesonly|serveraliveinterval|serveralivecountmax) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user deploy
hostname 127.0.0.1
port 65000
identitiesonly yes
serveralivecountmax 3
serveraliveinterval 60
identityfile /tmp/.../home/.ssh/projectA_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the step that closes the gap between what a guide claims and what OpenSSH actually resolves.&lt;/p&gt;

&lt;p&gt;If a guide says "this alias connects as deploy on port 65000 with this key," &lt;code&gt;ssh -G&lt;/code&gt; can prove that claim without hitting the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-side examples need a smoke test too
&lt;/h2&gt;

&lt;p&gt;Server-side sections often include examples like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
Port 65000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production server is not required to check whether the example is internally valid.&lt;/p&gt;

&lt;p&gt;A temp host key and a temp &lt;code&gt;sshd_config&lt;/code&gt; are enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/sshd_config"&lt;/span&gt;
sshd &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/sshd_config"&lt;/span&gt; |
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(passwordauthentication|permitrootlogin|port|pubkeyauthentication) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sshd -t EXIT=0
port 65000
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does that prove a real server rollout is safe? No.&lt;/p&gt;

&lt;p&gt;It proves the snippet being copied is not nonsense. That is a lower bar, but it is a bar worth clearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The delegation checks
&lt;/h2&gt;

&lt;p&gt;Some commands need a remote server to fully succeed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/projectA_ed25519.pub &lt;span class="nt"&gt;-p&lt;/span&gt; 2222 deploy@server.example.com
scp project-a:/path/file &lt;span class="nb"&gt;.&lt;/span&gt;
rsync &lt;span class="nt"&gt;-avz&lt;/span&gt; project-a:/path/ ./path/
git clone project-a:org/repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spinning up a full SSH server is not necessary for this check. Pointing the alias at &lt;code&gt;127.0.0.1:65000&lt;/code&gt;, where nothing is listening, is enough to check whether each tool delegates to SSH the way a guide implies.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;scp&lt;/code&gt;, the expected failure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executing: program /usr/bin/ssh host project-a, user (unspecified), command sftp
debug1: Connecting to 127.0.0.1 [127.0.0.1] port 65000.
ssh: connect to host 127.0.0.1 port 65000: Connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reading configuration data /tmp/.../home/.ssh/config
Applying options for project-a
Connecting to 127.0.0.1 [127.0.0.1] port 65000.
fatal: Could not read from remote repository.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection fails as expected — there is no server listening.&lt;/p&gt;

&lt;p&gt;The important part is that the tools use the SSH alias and reach the intended host and port. That is the evidence a guide's claims need to hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  An OpenSSH checklist for articles and runbooks
&lt;/h2&gt;

&lt;p&gt;SSH articles are often written as plain setup notes.&lt;/p&gt;

&lt;p&gt;Treating them like tiny integration tests instead catches this class of bug before publication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the doc contains an SSH config, the doc should also contain the command that proves how OpenSSH resolves it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A practical shortlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# client config resolution&lt;/span&gt;
ssh &lt;span class="nt"&gt;-F&lt;/span&gt; ./example.conf &lt;span class="nt"&gt;-G&lt;/span&gt; host-alias

&lt;span class="c"&gt;# server config syntax and effective values&lt;/span&gt;
sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ./sshd_config
sshd &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ./sshd_config

&lt;span class="c"&gt;# local file modes on macOS/BSD stat&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%Lp %N'&lt;/span&gt; ~/.ssh ~/.ssh/config ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub

&lt;span class="c"&gt;# local file modes on GNU/Linux&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%a %n'&lt;/span&gt; ~/.ssh ~/.ssh/config ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole trick.&lt;/p&gt;

&lt;p&gt;Not a framework. Not a yak shave. Just make OpenSSH tell you what it thinks before you tell humans what to think.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own config
&lt;/h2&gt;

&lt;p&gt;Pick one host alias from your current &lt;code&gt;~/.ssh/config&lt;/code&gt; and run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; your-host-alias |
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(hostname|user|port|identityfile|identitiesonly) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this the connection you thought you had configured?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, congrats. You found the bug before production did.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;Host *&lt;/code&gt; always need to be at the bottom?
&lt;/h3&gt;

&lt;p&gt;No. &lt;code&gt;Host *&lt;/code&gt; is safe when it sets defaults that do not collide with more specific host blocks. Put it later when it can collide with values such as &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;Port&lt;/code&gt;, &lt;code&gt;HostName&lt;/code&gt;, &lt;code&gt;IdentityFile&lt;/code&gt;, or other per-host settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I check what SSH config is actually used?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;ssh -G&lt;/code&gt; with the host alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; your-host-alias
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a shorter review, filter the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; your-host-alias |
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^(hostname|user|port|identityfile|identitiesonly) /{print}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints the effective SSH config after OpenSSH has applied matching &lt;code&gt;Host&lt;/code&gt; blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;ssh -G&lt;/code&gt; safe to run?
&lt;/h3&gt;

&lt;p&gt;Yes. &lt;code&gt;ssh -G&lt;/code&gt; prints the resolved client configuration and exits. It does not open an SSH session to the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I also test &lt;code&gt;sshd_config&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Yes, if the article or runbook includes server-side SSH settings. Use &lt;code&gt;sshd -t -f ./sshd_config&lt;/code&gt; for syntax and &lt;code&gt;sshd -T -f ./sshd_config&lt;/code&gt; to inspect effective server settings. Depending on your environment, &lt;code&gt;sshd -T&lt;/code&gt; may require privileges or a complete server config fixture.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the practical rule for SSH setup guides?
&lt;/h3&gt;

&lt;p&gt;Do not stop at "this config looks right." Add the command that proves what OpenSSH will actually use. For client config, that command is usually &lt;code&gt;ssh -G&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Sho Naka, writing as nomurasan, publishes implementation notes on teaching, automation, and operational tooling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI helped with English editing and structure. The technical verification, corrections, and final responsibility for the content of this article rest with the author.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ssh</category>
      <category>devops</category>
      <category>security</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
