<?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: hyuga</title>
    <description>The latest articles on DEV Community by hyuga (@hyuga611).</description>
    <link>https://dev.to/hyuga611</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%2F4069679%2F58392e9f-7cbb-4566-8fed-b1de5f5d09f3.png</url>
      <title>DEV Community: hyuga</title>
      <link>https://dev.to/hyuga611</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hyuga611"/>
    <language>en</language>
    <item>
      <title>80% of my Claude Code bill was output that never left the context</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Sat, 12 Sep 2026 09:53:07 +0000</pubDate>
      <link>https://dev.to/hyuga611/80-of-my-claude-code-bill-was-output-that-never-left-the-context-3nno</link>
      <guid>https://dev.to/hyuga611/80-of-my-claude-code-bill-was-output-that-never-left-the-context-3nno</guid>
      <description>&lt;p&gt;I had a number I could not explain.&lt;/p&gt;

&lt;p&gt;ccusage told me what I spent. Good tool, does its job. But the biggest line in it was &lt;code&gt;cache_read&lt;/code&gt; — the charge for re-reading the whole conversation on every single turn — and nothing told me &lt;em&gt;what&lt;/em&gt; I kept re-reading.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;strong&gt;nenpi&lt;/strong&gt; (燃費, Japanese for "fuel economy") to find out. Zero dependencies, reads the JSONL transcripts already sitting in &lt;code&gt;~/.claude/projects/&lt;/code&gt;, uploads nothing.&lt;/p&gt;

&lt;p&gt;The first run reframed the whole problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Residency cost
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Residency cost (tokens injected x turns that followed) = what cache_read really is
    resid  share  calls injected     avg  imgs  tool
    1669M  81.4%   5586     2.4M     432     0  Bash
     231M  11.3%    229     0.3M    1474   163  Read
      43M   2.1%     64     0.1M     950    34  mcp__claude-in-chrome__computer
      17M   0.8%    372     0.0M      55     0  Write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash output was 2.4M tokens of actual text. Its residency cost was 1669M — 81% of everything I spent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Residency cost = tokens injected × turns that came after.&lt;/strong&gt; A tool result is not charged once. It is charged once per turn that follows it. Dumping a log file on turn 5 of a 300-turn session is a completely different purchase from doing it on turn 299, and no cost-per-day chart will ever tell you that.&lt;/p&gt;

&lt;p&gt;Once you can sort by that column, the thing worth fixing stops being a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counting bug that makes most of these numbers wrong
&lt;/h2&gt;

&lt;p&gt;This is the part I would want to know if I were writing my own token tool.&lt;/p&gt;

&lt;p&gt;Claude Code splits one API response into a separate JSONL line per &lt;code&gt;thinking&lt;/code&gt; / &lt;code&gt;text&lt;/code&gt; / &lt;code&gt;tool_use&lt;/code&gt; block — and copies the same &lt;code&gt;usage&lt;/code&gt; object onto every one of them.&lt;/p&gt;

&lt;p&gt;Sum per line and you count the same charge several times over. Measured on my machine: &lt;strong&gt;+82%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The same split breaks parallel-call detection. From a single line you cannot tell how many tools one response called, so a per-line count pins your bundling rate at 0.0% forever, no matter what the model does.&lt;/p&gt;

&lt;p&gt;nenpi deduplicates by &lt;code&gt;message.id&lt;/code&gt; and regroups by &lt;code&gt;requestId&lt;/code&gt; before it counts anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it told me my own hook was useless
&lt;/h2&gt;

&lt;p&gt;nenpi is also three Claude Code hooks — the point where measuring turns into a smaller bill.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nenpi hook pre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PreToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A full-file &lt;code&gt;Read&lt;/code&gt; of a large file gets cut to the first 400 lines, with a note to &lt;code&gt;Grep&lt;/code&gt; first. Full text would sit in the context and be re-sent every turn.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nenpi hook prompt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UserPromptSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows what re-reading the current context costs per turn. It never says "your context is too long" — that claim did not hold up in the measurements.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nenpi hook post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PostToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the same tool has run one call at a time for several turns straight, says so once, at the moment it happens.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And &lt;code&gt;nenpi effect&lt;/code&gt; exists to check whether a nudge like that does anything at all.&lt;/p&gt;

&lt;p&gt;Week-to-week comparison confounds — spend fell, but was that your change or an easier week? So &lt;code&gt;effect&lt;/code&gt; compares, &lt;strong&gt;inside the same session&lt;/strong&gt;, the turns immediately after a nudge fired against ordinary turns. Whatever the work was, it applies to both sides and cancels out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## "bundle them" (hook post) — did the next turn actually bundle?
  fired                             80 times
  bundled on the next turn           8 / 79   10.1%
  normal turns, same sessions      201 / 6401   3.1%
  difference                    +7.0pt
  → It does not work. Reword it, or take it out.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My own tool, telling me to rewrite my own nudge.&lt;/p&gt;

&lt;p&gt;That turned out to be the feature I use most. Not "how much did I spend" — "the thing I was sure about is not true."&lt;/p&gt;

&lt;h2&gt;
  
  
  What is measured and what is estimated
&lt;/h2&gt;

&lt;p&gt;Being straight about this matters more than the numbers looking precise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured, from &lt;code&gt;usage&lt;/code&gt;:&lt;/strong&gt; token counts, tool call counts, timestamps, hook durations, &lt;code&gt;is_error&lt;/code&gt; flags, compaction events. Dollar figures come from Claude Code's own &lt;code&gt;costUSD&lt;/code&gt;, not from a model of mine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated:&lt;/strong&gt; &lt;code&gt;BYTES_PER_TOK = 3.5&lt;/code&gt; for the byte→token conversion, &lt;code&gt;IMG_TOK = 1600&lt;/code&gt; per image block, and the weights that turn token classes into input-token equivalents. &lt;code&gt;quality&lt;/code&gt; prints a &lt;strong&gt;calibration residual&lt;/strong&gt; against the real &lt;code&gt;costUSD&lt;/code&gt; so you can see how far off the weights are on your plan. Mine runs about 1%.&lt;/p&gt;

&lt;p&gt;So: charges are measured, attribution is estimated. Residency cost sits on the estimated side. It is still the most useful number here, because nothing else points at &lt;em&gt;which&lt;/em&gt; output is the expensive one — but read it as an estimate.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx @hyuga/nenpi report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 18 or newer. MIT. Source: &lt;a href="https://github.com/hyuga611/nenpi" rel="noopener noreferrer"&gt;https://github.com/hyuga611/nenpi&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are two more commands I lean on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;quality&lt;/code&gt;&lt;/strong&gt; — cutting tokens is easy if you are allowed to make the agent worse. It puts fuel, intelligence and speed on one screen (rework rate, tool failure rate, user correction rate, wasted-&lt;code&gt;Read&lt;/code&gt; rate, turns per prompt, wall time per prompt) so a "win" has to hold on all three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;errors&lt;/code&gt;&lt;/strong&gt; — a rising tool failure rate is not evidence the model regressed. Permission denials, &lt;code&gt;EPERM&lt;/code&gt;, a human pressing stop all raise it. This splits environment-caused failures from model-caused ones, broken down by model × effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat worth repeating: the transcript format is undocumented and it changes. nenpi pins its counting to &lt;code&gt;message.id&lt;/code&gt; and &lt;code&gt;requestId&lt;/code&gt;, which have been stable, but a format change can silently make a metric wrong. If a number looks impossible, it probably is — please open an issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>node</category>
      <category>productivity</category>
    </item>
    <item>
      <title>572 tests green, and three packages were one command from shipping broken</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Sun, 30 Aug 2026 23:05:31 +0000</pubDate>
      <link>https://dev.to/hyuga611/572-tests-green-and-three-packages-were-one-command-from-shipping-broken-4713</link>
      <guid>https://dev.to/hyuga611/572-tests-green-and-three-packages-were-one-command-from-shipping-broken-4713</guid>
      <description>&lt;p&gt;I publish seven packages out of one monorepo. Last week, with &lt;strong&gt;572 tests passing, typecheck clean, lint clean and the tarball-contents check green&lt;/strong&gt;, three of them were one command away from being published in a state that would fail on install.&lt;/p&gt;

&lt;p&gt;The bug is boring. The reason nothing caught it is not.&lt;/p&gt;

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

&lt;p&gt;I added one subpath export to the shared package, &lt;code&gt;@hyuga/spar&lt;/code&gt;:&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;"exports"&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;"."&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/spar.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./cli"&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/cli.mjs"&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;Then I changed the three packages that depend on it to use that subpath:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runDirectly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readStdin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hyuga/spar/cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I forgot to raise the dependency floor:&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="nl"&gt;"dependencies"&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;"@hyuga/spar"&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="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;^0.1.0&lt;/code&gt; resolves to the &lt;code&gt;0.1.0&lt;/code&gt; already on the registry, whose exports are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm view @hyuga/spar@0.1.0 exports
{ '.': './src/spar.mjs' }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;./cli&lt;/code&gt;. Three lines to reproduce:&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;mkdir &lt;/span&gt;t &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;t &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm i @hyuga/spar@0.1.0
node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"import('@hyuga/spar/cli')"&lt;/span&gt;
&lt;span class="c"&gt;# ERR_PACKAGE_PATH_NOT_EXPORTED - Package subpath './cli' is not defined by "exports"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It cannot reproduce in the workspace.&lt;/strong&gt; &lt;code&gt;npm install&lt;/code&gt; symlinks all seven packages to each other, so &lt;code&gt;@hyuga/spar&lt;/code&gt; always resolves to the working tree. Of course the tests passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing surfaced it
&lt;/h2&gt;

&lt;p&gt;These three run as hooks. Throwing inside a hook takes the user's session with it, so they are all written like this:&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="k"&gt;try&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;sub&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PreToolUse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// a limiter that breaks the session is worse than no limiter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I still think that is the right call. It also swallows &lt;code&gt;ERR_PACKAGE_PATH_NOT_EXPORTED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From outside: no error, no warning, exit 0, a counter that stays at zero forever, and a part meant to keep a copy of your draft before it is overwritten that keeps nothing. It reads as "a quiet day." You find out when you go looking for a draft that was never saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was green
&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;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;node --test&lt;/code&gt;, 7 packages, 572 tests&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tsc --noEmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;biome lint&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 18 / 20 / 22 / 24 matrix&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;integration against real MySQL 8.4 and PostgreSQL 16&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;npm pack --dry-run&lt;/code&gt; contents (no config / dump / env)&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tag vs package.json vs &lt;code&gt;src/version.ts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of them runs inside the working tree. That is the whole problem. It was not that I had too few checks — it was that &lt;strong&gt;all of them were checking the wrong place.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The check I added
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;scripts/smoke-install.mjs&lt;/code&gt;, and it is exactly what it sounds like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;npm pack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;make an empty directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm i ./the-tarball.tgz&lt;/code&gt; there — &lt;strong&gt;letting npm resolve dependencies from the registry&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;import()&lt;/code&gt; every subpath the package declares in &lt;code&gt;exports&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run every command in &lt;code&gt;bin&lt;/code&gt; with &lt;code&gt;--help&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3's parenthesis is the entire point: no workspace links.&lt;/p&gt;

&lt;p&gt;It failed immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@hyuga/redline@0.2.0
  ✗ npm install from the tarball
      npm error code ETARGET
      npm error notarget No matching version found for @hyuga/spar@^0.2.0.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A side effect worth having: &lt;strong&gt;the publish order is now enforced rather than documented.&lt;/strong&gt; Publishing &lt;code&gt;redline&lt;/code&gt; before &lt;code&gt;spar&lt;/code&gt; reaches the registry fails with ETARGET. It used to be a sentence in RELEASING.md.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the check was wrong. Twice.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It stopped a release and explained nothing
&lt;/h3&gt;

&lt;p&gt;First run in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ npm pack
    undefined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had written the destination path already-quoted, because npm is a &lt;code&gt;.cmd&lt;/code&gt; on Windows — and since Node 20, spawning a &lt;code&gt;.cmd&lt;/code&gt; without a shell fails with &lt;code&gt;EINVAL&lt;/code&gt;, so it needs &lt;code&gt;shell: true&lt;/code&gt;, which needs the quotes. Linux runs it with &lt;code&gt;shell: false&lt;/code&gt;, where those quote marks are just characters in a directory name.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;a check whose entire purpose is "what works in here is not what happens out there" failed for exactly that reason.&lt;/strong&gt; Written on Windows, first run on Linux.&lt;/p&gt;

&lt;p&gt;The fix moves quoting inside the runner, which is the only place that knows whether a shell is involved:&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;WINDOWS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win32&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;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&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="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&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;runNpm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&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="nx"&gt;WINDOWS&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm.cmd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also fixed the &lt;code&gt;undefined&lt;/code&gt;. &lt;strong&gt;A gate that halts a release and says nothing is worse than the bug it caught&lt;/strong&gt; — the next person's first move is not to read it, it is to delete it.&lt;/p&gt;

&lt;h3&gt;
  
  
  It called correct behaviour a defect
&lt;/h3&gt;

&lt;p&gt;Last package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ import '@hyuga/llm-safe-sql/mysql'
    Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'mysql2'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing wrong with the package. &lt;code&gt;mysql2&lt;/code&gt; is an &lt;strong&gt;optional peer&lt;/strong&gt;: you install it if you want the MySQL adapter, and you never touch that subpath if you don't.&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="nl"&gt;"peerDependencies"&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;"mysql2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=3.9.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;"pg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=8.11.0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"peerDependenciesMeta"&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;"mysql2"&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;"optional"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pg"&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;"optional"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span 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;My check imported every declared subpath against a bare install, so it was calling documented behaviour a bug. Peers now get installed alongside:&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;peers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;peerDependencies&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="nf"&gt;runNpm&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`./&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tarball&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;peers&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;This is the failure mode that actually matters. &lt;strong&gt;A gate that cries wolf gets switched off&lt;/strong&gt;, and after it is switched off, the one call that should have stopped you looks exactly like the nineteen that should not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it ended up
&lt;/h2&gt;

&lt;p&gt;Twelve subpaths and eight bins, all checked against a registry install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@hyuga/llm-safe-sql@0.10.1
  · peers installed: mysql2, pg
  ✓ import @hyuga/llm-safe-sql
  ✓ import @hyuga/llm-safe-sql/mysql
  ✓ import @hyuga/llm-safe-sql/postgres
  ✓ import @hyuga/llm-safe-sql/sqlite
  ✓ import @hyuga/llm-safe-sql/mcp
  ✓ llm-safe-sql --help
  ✓ llm-safe-sql-mcp --help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All seven went out through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Green means "it worked under the conditions I built," not "it works."&lt;/strong&gt; If you have workspaces, a monorepo, or path mapping, those conditions differ from your users' by construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;catch&lt;/code&gt; you added for a good reason still makes everything that dies inside it invisible.&lt;/strong&gt; Being right about the &lt;code&gt;catch&lt;/code&gt; does not help. Keep a list of the places your system is designed to stay quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that can stop a release must say why it stopped.&lt;/strong&gt; A check that does not is a check somebody deletes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A false positive can cost more than a miss.&lt;/strong&gt; A miss loses one case. A false positive loses the check, and therefore every case after it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is likely to hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;npm / pnpm / yarn workspaces monorepos&lt;/li&gt;
&lt;li&gt;packages with subpath &lt;code&gt;exports&lt;/code&gt; consumed by a sibling package in the same repo&lt;/li&gt;
&lt;li&gt;TypeScript &lt;code&gt;paths&lt;/code&gt; or project references doing the resolving&lt;/li&gt;
&lt;li&gt;anything with optional peer dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are not reinstalling the tarball in CI, you are probably not checking this. I wasn't.&lt;/p&gt;

&lt;p&gt;The script is under 100 lines: &lt;a href="https://github.com/hyuga611/airframe/blob/main/scripts/smoke-install.mjs" rel="noopener noreferrer"&gt;smoke-install.mjs&lt;/a&gt;. MIT — take it.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>node</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
    <item>
      <title>You are in the cockpit now. Nobody gave you instruments.</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Sun, 30 Aug 2026 11:36:38 +0000</pubDate>
      <link>https://dev.to/hyuga611/you-are-in-the-cockpit-now-nobody-gave-you-instruments-3315</link>
      <guid>https://dev.to/hyuga611/you-are-in-the-cockpit-now-nobody-gave-you-instruments-3315</guid>
      <description>&lt;p&gt;I noticed recently that I have stopped typing.&lt;/p&gt;

&lt;p&gt;The agent writes the code. What I do is look at what comes back and press &lt;code&gt;y&lt;/code&gt;. Dozens of times a day.&lt;/p&gt;

&lt;p&gt;And — you probably recognize this — &lt;strong&gt;somewhere around the tenth one, I stopped reading.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Same as the last one." It usually is. But if one of those fifty had touched production, would you have caught it? I wouldn't have.&lt;/p&gt;

&lt;p&gt;That is not supervision. That is a rubber stamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I flying this, or am I a passenger?
&lt;/h2&gt;

&lt;p&gt;Here is the thing I keep coming back to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pilot does not see the outside world directly.&lt;/strong&gt; They see instruments: fuel, damage, remaining ordnance, whether the limiter is engaged. Reaction time matters, but knowing &lt;em&gt;what is happening right now&lt;/em&gt; comes first.&lt;/p&gt;

&lt;p&gt;An aircraft with broken instruments is not the same aircraft with a small problem. It is a different aircraft.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;everyone running an AI agent is already sitting in a cockpit.&lt;/strong&gt; You don't move the controls yourself; the machine acts and you decide at the moments that matter. That is a pilot's job description.&lt;/p&gt;

&lt;p&gt;You are just sitting there &lt;strong&gt;without an instrument panel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What you can see is one approval dialog at a time. What you cannot see: what this session has done in total, how far into dangerous territory it has gone, what it got wrong last time.&lt;/p&gt;

&lt;p&gt;I wanted that panel, so I built it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hyuga/airframe &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The panel
&lt;/h2&gt;

&lt;p&gt;It says nothing most of the time. Silence is the correct state. You look when you want to look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ airframe status

sortie   2026-08-30T07-07-43-412Z-18aa80
form     strike / fire
limiter  0
wingmen  0
fuel     0 (no budget set)
mounted
  + airframe      the vessel
  + redline       limiter — counts the sortie, not the call
  + habit         learns from the corrections you make by hand
  + carbon        keeps the draft about to be written over — cruise only
  + groundtruth   completion gate — called from your code, not from a hook
  + llm-safe-sql  runs the write, measures it, rolls back — from your code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vocabulary is aviation, partly because I like it and partly because it turned out to be the right shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sortie&lt;/strong&gt; — one run of work. What the last one left unfinished survives into the next&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;limiter&lt;/strong&gt; — how much irreversible ground this sortie has covered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;wingmen&lt;/strong&gt; — how many subagents were launched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fuel&lt;/strong&gt; — how many moves are left&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;limiter 0&lt;/code&gt; means nothing dangerous has happened yet. When it climbs, the machine is pushing into something.&lt;/p&gt;

&lt;h2&gt;
  
  
  A limiter is not there to slow you down
&lt;/h2&gt;

&lt;p&gt;This is the part I most want to land.&lt;/p&gt;

&lt;p&gt;"Guardrails" sounds like friction. It is the opposite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The limiter is what lets you open the throttle.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without instruments there is exactly one safe way to operate: go slow, always. And that is what everyone does — confirm every step, never look away. Sitting in a fast machine, driving at walking pace forever.&lt;/p&gt;

&lt;p&gt;With a number on the panel, &lt;strong&gt;"safe to let it run" and "hold the reins" become different situations.&lt;/strong&gt; Zero? Leave it alone. Climbing? Pay attention. It is a part that makes you faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  My own instrument lied to me
&lt;/h2&gt;

&lt;p&gt;Then the instrument lied to me.&lt;/p&gt;

&lt;p&gt;While publishing this repository, the limiter — threshold 3 — reached &lt;strong&gt;20&lt;/strong&gt;. More than six times over.&lt;/p&gt;

&lt;p&gt;I looked at what it had charged for. &lt;strong&gt;Every single one was a search.&lt;/strong&gt;&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"npm publish"&lt;/span&gt; .github/workflows/release.yml    &lt;span class="c"&gt;# +3&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"rm -rf"&lt;/span&gt; packages/                             &lt;span class="c"&gt;# +3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Looking for the string&lt;/strong&gt; &lt;code&gt;npm publish&lt;/code&gt; scored the same as actually publishing. The limiter was matching a regex against the whole command as one blob of text.&lt;/p&gt;

&lt;p&gt;The problem is not that it was too strict. The problem is that &lt;strong&gt;once you learn 19 of 20 charges are noise, you skip the 20th too.&lt;/strong&gt; And the one you skip is the one that mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A broken instrument is worse than no instrument&lt;/strong&gt;, because it lets you feel watched while nothing is watching.&lt;/p&gt;

&lt;p&gt;Fixed the same day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep "npm publish" README.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep "npm publish" README.md &amp;amp;&amp;amp; npm publish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm publish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The arguments to &lt;code&gt;grep&lt;/code&gt; are text, not actions. But &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;xargs&lt;/code&gt; and &lt;code&gt;node -e&lt;/code&gt; are still charged on what they contain — their arguments &lt;em&gt;are&lt;/em&gt; commands, and nothing here can tell whether that string is about to run or about to be printed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Charging when the answer is genuinely unknown is design. Charging when the answer was obvious was the bug.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Past the limit, it does not stop you
&lt;/h2&gt;

&lt;p&gt;When the score goes past the threshold, the machine &lt;strong&gt;does not stop.&lt;/strong&gt; It says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This sortie has spent 20 against a limit of 3. You are flying it, so it is your call — but say out loud that you are past the edge before going on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not stop because &lt;strong&gt;there is no guarantee the machine is more right than the person in the seat.&lt;/strong&gt; The call belongs to the pilot.&lt;/p&gt;

&lt;p&gt;One exception: set &lt;code&gt;AIRFRAME_AUTONOMY&lt;/code&gt; to the reason you are running unattended — a loop, a timer — and then it &lt;em&gt;will&lt;/em&gt; halt. With nobody in the seat there is nobody to advise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advise when someone is aboard. Halt when nobody is.&lt;/strong&gt; The condition for releasing the limiter is decided by the seat, not by the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converging and diverging are opposite jobs
&lt;/h2&gt;

&lt;p&gt;One more thing you switch by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;airframe mode strike   &lt;span class="c"&gt;# implement it, fix it, ship it&lt;/span&gt;
airframe mode cruise   &lt;span class="c"&gt;# draft, design, decide what to build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you are implementing, you want the gate: &lt;em&gt;did those rows actually land?&lt;/em&gt; Stop me if not.&lt;/p&gt;

&lt;p&gt;While you are &lt;strong&gt;drafting&lt;/strong&gt;, the same gate is a wrist being grabbed mid-sentence. An idea that has not taken shape yet gets reported as "unmet." The better the idea, the earlier it closes.&lt;/p&gt;

&lt;p&gt;Opposite jobs. Tune for one and you break the other.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;the machine never guesses which one you are in.&lt;/strong&gt; Guessing wrong is the same bug as not having the two. You type it. It takes a second.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;cruise&lt;/code&gt;, the draft keeper runs: when the agent is about to overwrite a file git does not have, the previous version is kept.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ carbon list
2026-08-30T07-32-32-255Z-0bef6e1d.md    1832 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Files git already tracks are never copied — &lt;code&gt;git show&lt;/code&gt; gets those back. &lt;strong&gt;Only what git is not keeping.&lt;/strong&gt; For anyone who has had an agent overwrite a draft and found &lt;code&gt;git diff&lt;/code&gt; empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three CI files that had never run once
&lt;/h2&gt;

&lt;p&gt;One more thing that actually happened.&lt;/p&gt;

&lt;p&gt;When I merged the parts into one repository, I moved each part's &lt;code&gt;.github/workflows/&lt;/code&gt; along with it, into the package folders. GitHub only reads &lt;code&gt;.github/&lt;/code&gt; at the repository root.&lt;/p&gt;

&lt;p&gt;Three CI files. Three release files. After the merge, &lt;strong&gt;not one of them had run.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And nothing says so. Nothing is failing. On the GitHub UI, &lt;strong&gt;"zero failures" and "zero runs" look exactly the same.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which is the general form of what the completion gate does: at the moment something claims "done", go re-fetch the actual state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;groundtruth verify &lt;span class="nt"&gt;--probe&lt;/span&gt; &lt;span class="s2"&gt;"psql -tAc 'select count(*) from t where batch=123'"&lt;/span&gt; &lt;span class="nt"&gt;--count&lt;/span&gt; 45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Told 45 rows landed? Go count them. Zero? Stop there. No LLM, no API key — the raw output of the command you gave it, shown as evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually pays off
&lt;/h2&gt;

&lt;p&gt;Honestly: this thing helps in a narrow set of situations, and it is worth naming them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Late in a long day.&lt;/strong&gt; Work with an agent from morning to evening and your judgment degrades. You cannot feel it happen. The approval dialog asks you from scratch every time, so the tenth one looks exactly like the first. The limiter is the opposite: &lt;strong&gt;a number that does not go down.&lt;/strong&gt; Name your production paths and the day's total is right there.&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;"production"&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;"/var/www/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/srv/client-sites/"&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;strong&gt;When it runs unattended.&lt;/strong&gt; Set &lt;code&gt;AIRFRAME_AUTONOMY&lt;/code&gt; to the reason — a loop, a timer — and the machine really does halt. With nobody in the seat, "advise" means nothing. I don't know of another brake that works for unattended runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you are writing prose with an agent.&lt;/strong&gt; Articles, proposals, design notes. Overwriting a file git does not track, in &lt;code&gt;cruise&lt;/code&gt;, keeps the previous version. And the completion gate goes quiet, so a draft never gets reported as "unmet."&lt;/p&gt;

&lt;p&gt;Just as clearly, &lt;strong&gt;where it does not help:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short one-off tasks&lt;/strong&gt; — it's over before you'd look at the panel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local, reversible experiments&lt;/strong&gt; — nothing to count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overwrites via &lt;code&gt;sed -i&lt;/code&gt; or a formatter&lt;/strong&gt; — those never pass through a hook, so the draft keeper cannot see them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judgment calls&lt;/strong&gt; — "is this design good" has no probe, so the gate has nothing to check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Long sessions, a lot delegated, hard to undo.&lt;/strong&gt; The more of those three you have, the more this earns its place. None of them? Don't install it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I think this goes
&lt;/h2&gt;

&lt;p&gt;Soon enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agents will run &lt;strong&gt;while you sleep&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;you will fly &lt;strong&gt;several at once&lt;/strong&gt;, not one&lt;/li&gt;
&lt;li&gt;a single run will last &lt;strong&gt;hours&lt;/strong&gt;, not minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When that happens, I don't think the bottleneck is model quality. It is &lt;strong&gt;whether a human can stay in the loop at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You wake up to eight agents' worth of work. Can you reconstruct what happened? Which of them touched production? Did any of them quietly revert the fix you made by hand yesterday?&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;status&lt;/code&gt; has a &lt;code&gt;wingmen&lt;/code&gt; row. How many were launched is &lt;strong&gt;a number a pilot should be able to see afterwards&lt;/strong&gt;, and there is no other way to know it.&lt;/p&gt;

&lt;p&gt;Whether "AI does the work" means &lt;em&gt;you are in command&lt;/em&gt; or &lt;em&gt;you are being carried&lt;/em&gt; is decided, I think, by the cockpit rather than by the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of this buys
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It does not make the agent correct.&lt;/strong&gt; None of these parts read your code. They watch what happens around the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The limiter is a gauge, not a guard.&lt;/strong&gt; Its number is summed out of a ledger the agent can write to. It is built for the agent that has &lt;strong&gt;lost count&lt;/strong&gt;, not the one trying to get past you — anything that can run commands on your behalf is already inside the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The draft keeper matches names, not contents.&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt; is never copied; a draft with a key pasted into the third paragraph is copied like any other draft. A part that opens every draft to judge it is a part that opens every draft. I liked that less.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hyuga/airframe &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 18+. No dependencies, no daemon, no network, no LLM. MIT.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hyuga611/airframe" rel="noopener noreferrer"&gt;github.com/hyuga611/airframe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are sitting in the seat, look at the instruments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>node</category>
    </item>
    <item>
      <title>cd, type go, and your workspace builds itself — tsumugi 0.3</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Thu, 27 Aug 2026 13:24:11 +0000</pubDate>
      <link>https://dev.to/hyuga611/cd-type-go-and-your-workspace-builds-itself-tsumugi-03-46g2</link>
      <guid>https://dev.to/hyuga611/cd-type-go-and-your-workspace-builds-itself-tsumugi-03-46g2</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href="https://dev.to/hyuga611/i-built-a-terminal-because-i-could-never-remember-the-keybindings-13k1"&gt;building a terminal because I could never remember the keybindings&lt;/a&gt;. That terminal is &lt;strong&gt;tsumugi&lt;/strong&gt;, and I just shipped 0.3. This post is about the three things that went in — a workspace command, screenshot paste, and self-update — and why each one is shaped the way it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;github.com/hyuga611/tsumugi&lt;/strong&gt; — Rust, MIT OR Apache-2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise
&lt;/h2&gt;

&lt;p&gt;Short version, since the first post covered it. tsumugi is built on one claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The grid is a document. The process is appending to its end.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your scrollback is not a log, it is &lt;em&gt;what you just did&lt;/em&gt;, so it should be readable, selectable and reusable. Vim motions over scrollback, &lt;code&gt;ac&lt;/code&gt; to select a command with its output, &lt;code&gt;:e&lt;/code&gt; to turn the pane into an editor — they all fall out of that one line.&lt;/p&gt;

&lt;p&gt;And one rule on top: &lt;strong&gt;every command has a mouse path, enforced in CI.&lt;/strong&gt; A test walks the command registry and fails if any command lacks one.&lt;/p&gt;

&lt;p&gt;Those two decided the shape of almost everything below.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;go&lt;/code&gt; builds a workspace in one message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dev/tsumugi
go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pane you are in becomes the middle one, a &lt;strong&gt;directory tree opens on the left and an AI agent on the right&lt;/strong&gt;, and &lt;strong&gt;the tab is renamed after the directory&lt;/strong&gt;. 20 / 50 / 30. One tab per repository, and the tab names tell you which is which.&lt;/p&gt;

&lt;h3&gt;
  
  
  One message, not four
&lt;/h3&gt;

&lt;p&gt;Sending "split", "open the tree", "start the agent", "rename the tab" separately costs you twice: intermediate layouts get broadcast and &lt;strong&gt;the screen jumps&lt;/strong&gt;, and you end up needing "the id of the pane that was just created" in the next message — an async hole. So it is a single &lt;code&gt;ClientMsg::Workspace&lt;/code&gt;. The server assembles it and broadcasts once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tree has no motions of its own
&lt;/h3&gt;

&lt;p&gt;The tree implements &lt;code&gt;Buffer&lt;/code&gt;. That is the whole trick: &lt;code&gt;j&lt;/code&gt; &lt;code&gt;k&lt;/code&gt; &lt;code&gt;gg&lt;/code&gt; &lt;code&gt;G&lt;/code&gt; &lt;code&gt;/&lt;/code&gt;, visual selection and the mouse &lt;strong&gt;already work&lt;/strong&gt;, because they are the same ones you use on the terminal. I did not write a single tree-specific motion.&lt;/p&gt;

&lt;p&gt;Only a handful of keys are added inside the tree: Enter opens, &lt;code&gt;l&lt;/code&gt; / &lt;code&gt;h&lt;/code&gt; fold a branch, &lt;code&gt;a&lt;/code&gt; / &lt;code&gt;A&lt;/code&gt; create, &lt;code&gt;r&lt;/code&gt; renames, &lt;code&gt;R&lt;/code&gt; reloads. They are &lt;strong&gt;not in the default keymap&lt;/strong&gt; — &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;r&lt;/code&gt; meaning something else in a terminal would be wrong. Double-click opens, drag and drop moves. &lt;code&gt;:q&lt;/code&gt; closes the tree and the pane goes back to being a shell, with the shell underneath still running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no delete.&lt;/strong&gt; An operation you cannot undo does not belong on a list your finger slides across. Delete from the shell in the middle pane.&lt;/p&gt;

&lt;h3&gt;
  
  
  The listing is read server-side
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;[domains]&lt;/code&gt;, the files are on the far machine, so the tree is read there. Only branches you have opened are walked, so a directory with &lt;code&gt;node_modules&lt;/code&gt; or &lt;code&gt;target&lt;/code&gt; in it stays fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  It coexists with the Go toolchain
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;go&lt;/code&gt; is a function the shell integration installs; it calls &lt;code&gt;tsg --workspace&lt;/code&gt;. &lt;strong&gt;Anything with arguments is forwarded to the real &lt;code&gt;go&lt;/code&gt;&lt;/strong&gt;, so &lt;code&gt;go build&lt;/code&gt; still works, and a bare &lt;code&gt;go&lt;/code&gt; outside tsumugi still runs Go. &lt;code&gt;TSUMUGI_NO_GO=1&lt;/code&gt; gives the name back. From the keyboard it is Space then &lt;code&gt;w&lt;/code&gt;; from the palette, &lt;code&gt;:go&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Paste a screenshot into what you are telling the agent
&lt;/h2&gt;

&lt;p&gt;Ctrl+Shift+V writes the clipboard image out as a PNG and &lt;strong&gt;types its path into the prompt&lt;/strong&gt; — it does not press Enter, so you can keep writing before you send. Dropping an image file on the window lands in the same place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No new command was added.&lt;/strong&gt; Paste is one verb that does the closest thing to what it is holding: text if text, a path if a picture. Adding verbs adds things to remember.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;server&lt;/strong&gt; writes the file, into a per-session temp area — so when you are connected to a &lt;code&gt;[domains]&lt;/code&gt; host, the file appears &lt;strong&gt;where the agent reading it lives&lt;/strong&gt;. The last 32 are kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;tsg update&lt;/code&gt; starts no external process
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tsg update&lt;/code&gt; fetches the latest release and swaps it in. It downloads nothing when you are already on the latest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the download-and-run shape had to go
&lt;/h3&gt;

&lt;p&gt;The first version ran &lt;code&gt;powershell -Command "irm &amp;lt;URL&amp;gt; | iex"&lt;/code&gt; inside itself, to keep exactly one way of installing. But &lt;strong&gt;that shape is indistinguishable from malware that downloads and runs code.&lt;/strong&gt; Windows Defender on a corporate machine blocked it — the process was denied (&lt;code&gt;os error 5&lt;/code&gt;) and the installed binary was removed along with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The machines that need it most are the strictest, so that is exactly where it broke.&lt;/strong&gt; It now pulls the release over HTTPS itself (&lt;code&gt;ureq&lt;/code&gt; / rustls) and replaces the file. No child process at all. The first-time &lt;code&gt;install.ps1&lt;/code&gt; / &lt;code&gt;install.sh&lt;/code&gt; are unchanged — there is no &lt;code&gt;tsg&lt;/code&gt; yet, so that one has to start from outside.&lt;/p&gt;

&lt;h3&gt;
  
  
  After the swap, no reopening and no &lt;code&gt;--kill&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tsg update&lt;/code&gt; &lt;strong&gt;reopens the window on the new build.&lt;/strong&gt; The multiplexer is a separate process, so replacing only the window reconnects you &lt;strong&gt;with your shells and agents still alive&lt;/strong&gt;. I watched the window process change while the multiplexer's PID stayed the same.&lt;/p&gt;

&lt;p&gt;It checks before it commits: it asks the new binary for its protocol version (&lt;code&gt;--protocol&lt;/code&gt;) and reopens only if it matches the running server. Only when they differ does it become a conversation about stopping sessions — and it does not stop them for you, because stopping one ends the shells and agents inside it.&lt;/p&gt;

&lt;p&gt;Related decision: &lt;strong&gt;the protocol version is not bumped for additive changes.&lt;/strong&gt; Bumping it means a running server stops accepting new windows, which means killing the shells inside. It moves only when the meaning of an existing message changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;No admin rights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows (PowerShell)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://raw.githubusercontent.com/hyuga611/tsumugi/main/install.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line &lt;strong&gt;leaves you with a working setup&lt;/strong&gt;: the binary in &lt;code&gt;%USERPROFILE%\bin&lt;/code&gt;, Start Menu / PATH / folder context menu, &lt;strong&gt;the shell integration&lt;/strong&gt;, and the Claude Code and Codex hooks when they are present.&lt;/p&gt;

&lt;p&gt;It used to be three steps, and skipping the last two left you with a terminal where the gutter, &lt;code&gt;[[&lt;/code&gt;, &lt;code&gt;ac&lt;/code&gt; and &lt;code&gt;go&lt;/code&gt; all did nothing — &lt;strong&gt;installed but not working&lt;/strong&gt;. It was one errand for the person typing, so it is one command now. Every change is printed, and &lt;code&gt;tsg --uninstall&lt;/code&gt; takes all of it back out.&lt;/p&gt;

&lt;p&gt;The CRT is linked statically, so it runs on machines &lt;strong&gt;without the VC++ redistributable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux (experimental)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/hyuga611/tsumugi/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Untested.&lt;/strong&gt; CI builds and tests on three OSes, but the window (winit / wgpu), the IME and &lt;code&gt;--install&lt;/code&gt; have parts written against Windows APIs, and nobody has run them yet. Shipping the binaries is to make trying it easy, not a promise that it works. Reports very welcome.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Check it took&lt;/strong&gt; — a shell reads its config at startup, so the window you typed in does not have it yet. Open a new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;tsg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-V&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c"&gt;# prints the version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;go&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c"&gt;# Function means the shell integration took&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;You need no configuration to use an agent.&lt;/strong&gt; The waiting mark on the tab and the notifications come from the hooks, so typing &lt;code&gt;claude&lt;/code&gt; or &lt;code&gt;codex&lt;/code&gt; in a pane just works. Configuration only decides what &lt;code&gt;go&lt;/code&gt; starts on the right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[shell]&lt;/span&gt;
&lt;span class="py"&gt;program&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pwsh"&lt;/span&gt;

&lt;span class="nn"&gt;[workspace]&lt;/span&gt;
&lt;span class="py"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claude"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Updating&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;tsg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows SmartScreen will warn on first launch — the binary is unsigned. More info, then Run anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also fixed in 0.3.x
&lt;/h2&gt;

&lt;p&gt;Twelve patch releases, almost all of them from running it on a &lt;strong&gt;different&lt;/strong&gt; machine — a locked-down corporate laptop. A sample:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows PowerShell 5.1 and PowerShell 7 read &lt;strong&gt;different&lt;/strong&gt; profiles; the installer wrote to whichever it found first. Now it writes to all of them.&lt;/li&gt;
&lt;li&gt;It guessed where &lt;code&gt;$PROFILE&lt;/code&gt; was. If you have never created one, neither directory exists and it gave up. Now it &lt;strong&gt;asks PowerShell&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The default shell was &lt;code&gt;cmd.exe&lt;/code&gt; (from &lt;code&gt;COMSPEC&lt;/code&gt;) — and cmd.exe has no way to emit OSC 133, which everything in tsumugi is built on. Everyone who installed it started from a state where nothing worked.&lt;/li&gt;
&lt;li&gt;Agent state was landing on the wrong pane: the hook ran &lt;code&gt;tsg --agent-state&lt;/code&gt; without naming a pane, so it attached to whichever was selected. Two agents, one badge that never clears. It reads &lt;code&gt;TSUMUGI_PANE&lt;/code&gt; now.&lt;/li&gt;
&lt;li&gt;Three bugs on top of Claude Code, all one root cause: ownership was decided by &lt;strong&gt;alt screen alone&lt;/strong&gt;, and some tools paint the whole screen without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And 0.3.13 is &lt;strong&gt;the four I hit while taking the screenshot for this post.&lt;/strong&gt; Worst first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[workspace] agent = "claude"&lt;/code&gt; built no workspace at all&lt;/strong&gt; — the headline feature of this post, with the configuration this post recommends. npm puts both &lt;code&gt;claude&lt;/code&gt; (an extensionless shell script) and &lt;code&gt;claude.cmd&lt;/code&gt; on Windows; &lt;code&gt;CreateProcessW&lt;/code&gt; does not consult PATHEXT, so a bare name hits the former and dies with &lt;code&gt;os error 193&lt;/code&gt;. A bare name is now resolved against the extensions &lt;code&gt;CreateProcessW&lt;/code&gt; can actually start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;That reason never reached the person who typed it.&lt;/strong&gt; &lt;code&gt;tsg --workspace&lt;/code&gt; sent the request, slept 200ms and detached without reading the reply, so the server's error went nowhere. &lt;code&gt;go&lt;/code&gt; calls exactly that, so all you saw was nothing happening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failed &lt;code&gt;go&lt;/code&gt; leaked a pane.&lt;/strong&gt; The tree spawns before the agent; when the agent failed, the tree stayed alive and attached to no tab. One more every time you retried.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shell integration could be dead in every pane (PowerShell only).&lt;/strong&gt; The guard against double-sourcing was an environment variable — and environment variables are inherited by children. Start the mux from a shell that already sourced it (that is: type &lt;code&gt;tsg&lt;/code&gt; in your terminal) and every pane's shell decides it has already run. Gutter, &lt;code&gt;[[ ]]&lt;/code&gt;, &lt;code&gt;ac&lt;/code&gt; / &lt;code&gt;io&lt;/code&gt; and &lt;code&gt;go&lt;/code&gt; all go dark. Launch from the Start Menu and you never see it; &lt;strong&gt;type &lt;code&gt;tsg&lt;/code&gt; in a terminal and you always do.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both halves of that list say the same thing. &lt;strong&gt;"Installed but not working" never reproduces on the machine of the person who installed it.&lt;/strong&gt; Every test passed here.&lt;/p&gt;

&lt;p&gt;Full list in the CHANGELOG.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hyuga611/tsumugi" rel="noopener noreferrer"&gt;https://github.com/hyuga611/tsumugi&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>terminal</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a terminal because I could never remember the keybindings</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Tue, 25 Aug 2026 13:07:40 +0000</pubDate>
      <link>https://dev.to/hyuga611/i-built-a-terminal-because-i-could-never-remember-the-keybindings-13k1</link>
      <guid>https://dev.to/hyuga611/i-built-a-terminal-because-i-could-never-remember-the-keybindings-13k1</guid>
      <description>&lt;p&gt;I could never remember the keybindings.&lt;/p&gt;

&lt;p&gt;I know WezTerm and NeoVim are good. I installed both more than once, and backed&lt;br&gt;
out both more than once, always at the same place. &lt;strong&gt;It isn't that I refused to&lt;br&gt;
learn them — I stalled before I got far enough to learn anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built the terminal I wanted instead: one where you can do everything&lt;br&gt;
without knowing a single key, and where knowing the keys only makes you faster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hyuga611/tsumugi" rel="noopener noreferrer"&gt;https://github.com/hyuga611/tsumugi&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rust, MIT / Apache-2.0. Developed on Windows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6yavncx00wgwbvtftpx5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6yavncx00wgwbvtftpx5.png" alt="editor" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  One idea: the screen is a document
&lt;/h2&gt;

&lt;p&gt;In a normal terminal, past output is over. You can scroll back and look at it,&lt;br&gt;
but you can't really &lt;em&gt;use&lt;/em&gt; it — you end up dragging with the mouse and hoping&lt;br&gt;
you grabbed the right lines.&lt;/p&gt;

&lt;p&gt;But scrollback isn't a log. It's &lt;em&gt;what you just did&lt;/em&gt;, and it's meant to be read&lt;br&gt;
back and reused. So tsumugi treats it as a document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;walk past output with vim motions (&lt;code&gt;j&lt;/code&gt; &lt;code&gt;k&lt;/code&gt; &lt;code&gt;w&lt;/code&gt; &lt;code&gt;[[&lt;/code&gt; &lt;code&gt;]]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;select a command &lt;strong&gt;and its output&lt;/strong&gt; as one thing (&lt;code&gt;ac&lt;/code&gt; / &lt;code&gt;io&lt;/code&gt; text objects)&lt;/li&gt;
&lt;li&gt;open a file in the same pane (&lt;code&gt;:e&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screen and a file are both "documents", so &lt;strong&gt;the same keys work on both.&lt;/strong&gt;&lt;br&gt;
There is no separate editor to launch.&lt;/p&gt;
&lt;h2&gt;
  
  
  Selecting "a command and its output" honestly
&lt;/h2&gt;

&lt;p&gt;Guessing at this always breaks. Decide that "lines starting with &lt;code&gt;$&lt;/code&gt; are&lt;br&gt;
prompts" and the first &lt;code&gt;$&lt;/code&gt; inside some output ruins it.&lt;/p&gt;

&lt;p&gt;So tsumugi uses &lt;strong&gt;OSC 133&lt;/strong&gt; — shell integration. The shell tells the terminal&lt;br&gt;
where the prompt starts, where the command starts, where the output starts, and&lt;br&gt;
what the exit code was.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tsg --install-shell-integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that you get ✓ / ✗ marks in the left gutter, &lt;code&gt;[[&lt;/code&gt; &lt;code&gt;]]&lt;/code&gt; to jump between&lt;br&gt;
prompts, and &lt;code&gt;]e&lt;/code&gt; to walk only the failed commands. &lt;strong&gt;Nothing is matched with a&lt;br&gt;
regex against your output, so no mark is ever a lie.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Every command has a mouse path — enforced by a test
&lt;/h2&gt;

&lt;p&gt;Since the whole point is "you don't have to know the keys", this isn't left to&lt;br&gt;
good intentions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;every_command_is_mouse_reachable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a command without a mouse path and CI fails. There are 78 of them right now.&lt;br&gt;
Right-click gives you "what you can do here". &lt;code&gt;Space t&lt;/code&gt; labels every path and URL&lt;br&gt;
on screen with one or two letters, and that letter opens it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running several AI agents turned me into a search function
&lt;/h2&gt;

&lt;p&gt;This part came out of using it, not planning it.&lt;/p&gt;

&lt;p&gt;I had three Claude Code sessions going on different jobs, and noticed that most&lt;br&gt;
of my actual work had become &lt;strong&gt;hunting for which one was waiting on me.&lt;/strong&gt; Look&lt;br&gt;
at one pane, look at the next, find the stalled one, answer it, start over.&lt;/p&gt;

&lt;p&gt;So I made the agents say so themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tsg --install-agent-hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That wires one line into Claude Code's and Codex's hooks. Tabs get a ● mark, the&lt;br&gt;
status bar counts how many are waiting, and &lt;code&gt;Space a&lt;/code&gt; jumps to the next one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhj9kxg39niwnpj5lyham.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhj9kxg39niwnpj5lyham.png" alt="agents" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the window isn't focused, the taskbar flashes. Anything running inside can&lt;br&gt;
also call for you with OSC 9 when a long job finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Losing the machine doesn't lose the session
&lt;/h2&gt;

&lt;p&gt;Closing the window was already fine — the multiplexer is a separate process, so&lt;br&gt;
shells and agents keep running. &lt;strong&gt;Losing power was not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now the layout and each pane's directory come back on the next launch. But&lt;br&gt;
&lt;strong&gt;the screen contents are never written to disk.&lt;/strong&gt; &lt;code&gt;SECURITY.md&lt;/code&gt; promises that&lt;br&gt;
scrollback never reaches the filesystem, and I didn't want to quietly break it&lt;br&gt;
for a convenience feature.&lt;/p&gt;

&lt;p&gt;So what comes back is the shape, the directories, and the argv. And the&lt;br&gt;
conversation? &lt;strong&gt;The agent owns its own transcript&lt;/strong&gt;, so tsumugi only needs to&lt;br&gt;
know how to say "continue". A restored pane gets &lt;code&gt;claude --continue&lt;/code&gt; &lt;em&gt;typed at&lt;br&gt;
the prompt but not run&lt;/em&gt; — pressing it is your call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing what the agent wrote, where you are
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Space g&lt;/code&gt; opens &lt;code&gt;git diff&lt;/code&gt; in colour. &lt;code&gt;Space G&lt;/code&gt; stages the hunk under the&lt;br&gt;
cursor, &lt;code&gt;Space R&lt;/code&gt; reverts it (twice — it destroys work).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfl34zrzd80qw4cugkn4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfl34zrzd80qw4cugkn4.png" alt="diff" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A diff you only look at means making the same decision again somewhere else&lt;br&gt;
later. And if an agent rewrites a file you have open, tsumugi reloads it (unless&lt;br&gt;
you have unsaved edits, in which case it tells you instead). Without that you&lt;br&gt;
read stale text, edit it, and delete the agent's work the moment you save.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;language servers: squiggles for errors, &lt;code&gt;gd&lt;/code&gt; to definition, Ctrl+Space to complete&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:grep&lt;/code&gt; searches the whole project and opens straight from the results&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:123&lt;/code&gt;, &lt;code&gt;s/old/new/&lt;/code&gt;, &lt;code&gt;g/&lt;/code&gt; for a regular expression&lt;/li&gt;
&lt;li&gt;SSH domains — the far side keeps running when the link drops&lt;/li&gt;
&lt;li&gt;Markdown rendered in place, images (Kitty graphics / Sixel)&lt;/li&gt;
&lt;li&gt;narrowing the window &lt;strong&gt;re-wraps&lt;/strong&gt; scrollback instead of truncating it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What building it actually taught me
&lt;/h2&gt;

&lt;p&gt;The lesson that stuck: &lt;strong&gt;you cannot test your own work with your own habits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Seven keys listed in the command palette did nothing when pressed — &lt;code&gt;Space g&lt;/code&gt;&lt;br&gt;
for the diff, &lt;code&gt;Space a&lt;/code&gt; to jump to a waiting agent, and five more. I had been&lt;br&gt;
verifying everything through the CLI (&lt;code&gt;tsg --run &amp;lt;id&amp;gt;&lt;/code&gt;) and had never once gone&lt;br&gt;
through the key path. A test that mechanically cross-checks the list against the&lt;br&gt;
implementation found all seven in one run.&lt;/p&gt;

&lt;p&gt;Opening a tab-indented file &lt;strong&gt;deleted every tab&lt;/strong&gt; — the worst kind, because&lt;br&gt;
saving then destroys someone else's indentation. That fell out of writing a test&lt;br&gt;
for auto-indent, not from using the editor.&lt;/p&gt;

&lt;p&gt;Wiring up LSP, I sent &lt;code&gt;initialized&lt;/code&gt; without waiting for the &lt;code&gt;initialize&lt;/code&gt;&lt;br&gt;
response (a spec violation) and rust-analyzer simply went quiet: running fine,&lt;br&gt;
zero diagnostics. Then a second one — language servers return &lt;code&gt;file:///c:/...&lt;/code&gt;&lt;br&gt;
with a &lt;strong&gt;lowercase drive letter&lt;/strong&gt;, and I was comparing paths as strings, so every&lt;br&gt;
diagnostic was arriving and being thrown away.&lt;/p&gt;

&lt;p&gt;All three looked like they were working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being straight about the state
&lt;/h2&gt;

&lt;p&gt;Developed on Windows. macOS and Linux build and pass CI, but &lt;strong&gt;nobody has run&lt;br&gt;
the window layer&lt;/strong&gt; (decorations, IME, the installer). The README says untested&lt;br&gt;
rather than supported, and I'd rather keep it that way until someone has.&lt;/p&gt;

&lt;p&gt;It isn't finished so much as &lt;em&gt;mine&lt;/em&gt; — I use it daily and can fix it myself. I&lt;br&gt;
started out unable to remember keybindings and now drive about half of it from&lt;br&gt;
the keyboard. Not because I finally memorised them, but because I could keep&lt;br&gt;
using it while I didn't.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>terminal</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your AGENTS.md is lying to your agent. So was my linter.</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Sun, 23 Aug 2026 01:39:00 +0000</pubDate>
      <link>https://dev.to/hyuga611/your-agentsmd-is-lying-to-your-agent-so-was-my-linter-oc1</link>
      <guid>https://dev.to/hyuga611/your-agentsmd-is-lying-to-your-agent-so-was-my-linter-oc1</guid>
      <description>&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; rot silently. You write "setup is &lt;code&gt;npm run build&lt;/code&gt;", "the entry point is &lt;code&gt;src/index.ts&lt;/code&gt;", someone renames a script, and &lt;strong&gt;the instruction quietly becomes a lie.&lt;/strong&gt; The agent believes it and breaks things.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/hyuga611/reflint" rel="noopener noreferrer"&gt;reflint&lt;/a&gt;: a linter that ignores wording and style entirely and checks one thing — do the references still resolve? Back-quoted paths against the disk, markdown link targets against the repo, &lt;code&gt;npm run &amp;lt;script&amp;gt;&lt;/code&gt; against &lt;code&gt;package.json&lt;/code&gt;. Zero dependencies, language-agnostic, exits 1 in CI.&lt;/p&gt;

&lt;p&gt;That is the launch paragraph. The rest of this post is what happened after I published it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I ran reflint against 118 public repositories that have an &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;CLAUDE.md&lt;/code&gt;, in both the before and after versions.&lt;/strong&gt; What it found was mostly my own breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A flag I had advertised in a blog post had never done anything
&lt;/h2&gt;

&lt;p&gt;reflint has &lt;code&gt;--code-blocks&lt;/code&gt;. The help text says it makes the tool "also check paths inside fenced code blocks", which implies the default skips them. Months earlier I had written a post saying, in so many words: turn this on and it checks bare paths inside code blocks.&lt;/p&gt;

&lt;p&gt;Across 118 repositories, &lt;strong&gt;the number where the flag changed the result was zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cause was wiring. Only the fourth of four scanners consulted the in-a-fence state. The &lt;code&gt;npm run&lt;/code&gt; scanner, the back-quoted reference scanner and the markdown link scanner never looked at it — they read fenced content by default, so turning the flag on could not add anything. The flag was dead.&lt;/p&gt;

&lt;p&gt;I moved all three inside the fence check. On the same 118 repositories, &lt;strong&gt;10 now change their result depending on the flag.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;pnpm -r build&lt;/code&gt; was reported as "no script named -r"
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENTS.md:3   `pnpm -r` — no script "-r" in package.json
AGENTS.md:4   `pnpm --filter` — no script "--filter" in package.json
AGENTS.md:5   `pnpm -w` — no script "-w" in package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The character class in the script-name regex included &lt;code&gt;-&lt;/code&gt;, so the flag right after &lt;code&gt;pnpm&lt;/code&gt; was read as the script name. &lt;strong&gt;16 of the 39 script-kind findings were this.&lt;/strong&gt; In any repo using pnpm workspaces, correct documentation turned red.&lt;/p&gt;

&lt;p&gt;Those forms run per-workspace-package, so the root &lt;code&gt;package.json&lt;/code&gt; is not the right thing to check them against. reflint now skips the whole invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. One misplaced fence and it checked nothing, then said everything was fine
&lt;/h2&gt;

&lt;p&gt;Fence state was tracked with the obvious &lt;code&gt;inFence = !inFence&lt;/code&gt; toggle. That breaks on any document that demonstrates a fence by wrapping it in a longer fence: the marker count goes odd and &lt;strong&gt;everything below sticks to "inside a fence" forever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the 118 — a 3,461-line &lt;code&gt;AGENTS.md&lt;/code&gt; — did exactly this. Headings and prose were being treated as fenced content.&lt;/p&gt;

&lt;p&gt;Combined with fix #1, this is the dangerous one. Fenced content is skipped by default now, so from the misplaced marker down, &lt;strong&gt;reflint inspects nothing, exits 0, and prints &lt;code&gt;reflint: all references resolve&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a linter that is the worst possible failure, and I had already been here once. In 0.9.2, the CLI installed via &lt;code&gt;npm i -g&lt;/code&gt; or &lt;code&gt;npx&lt;/code&gt; exited 0 without running at all, because the entry-point check compared &lt;code&gt;process.argv[1]&lt;/code&gt; against &lt;code&gt;import.meta.url&lt;/code&gt; and those two never match through a symlink. &lt;strong&gt;"Found no problems" and "never ran" are indistinguishable — to the user and to CI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Closing is now CommonMark's rule: same character, at least as long as the opener, no info string.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you go quiet, say why you went quiet
&lt;/h2&gt;

&lt;p&gt;Every one of those fixes creates a region that is not checked by default. If the count drops, the user cannot tell whether things got fixed or whether the tool stopped looking. That is the same failure again, one level up.&lt;/p&gt;

&lt;p&gt;So reflint now always reports what it held back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reflint: 2 broken references (3 inside code blocks, not checked — run with --code-blocks)
reflint: all references resolve (1 inside code blocks, not checked — run with --code-blocks)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON output gained a &lt;code&gt;skipped&lt;/code&gt; field. HTML comments are not counted — they are disabled text, not content that &lt;code&gt;--code-blocks&lt;/code&gt; would bring back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Across the 118 repositories: &lt;strong&gt;208 findings became 185.&lt;/strong&gt; All 23 that disappeared were noise, and &lt;strong&gt;zero new findings appeared.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For honesty's sake, two other fixes in the same release — HTML comments and indented code blocks — had &lt;strong&gt;zero occurrences in this corpus.&lt;/strong&gt; They are reproducible and I closed them, but they moved no numbers. The three above did.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different shape, same lesson: only the people who wrote it correctly got warned
&lt;/h2&gt;

&lt;p&gt;The release after the audit fixed something else. Agent instruction files are full of prohibitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Never run npm run release; releases are performed by a human.
  → `npm run release` — no script "release" in package.json

Never read or execute `scripts/deprecated.sh`; it was removed after the migration.
  → reference `scripts/deprecated.sh` does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of those are &lt;strong&gt;correctly written documents.&lt;/strong&gt; If you explicitly tell the agent not to use something that was removed, then of course it does not exist — that is the entire point of the sentence. And yet the person who documented it was the only one getting flagged.&lt;/p&gt;

&lt;p&gt;Prohibition lines are no longer scanned as references, and that test runs outside fences only, so a &lt;code&gt;# never edit this&lt;/code&gt; comment inside a block does not silently disqualify the real references under it.&lt;/p&gt;

&lt;p&gt;I have now hit this exact shape in three of my linters. Text rules see &lt;em&gt;mention&lt;/em&gt;, not &lt;em&gt;execution&lt;/em&gt;. The more dangerous a thing is, the more likely a careful author is to name it in order to declare, forbid, or scope it — so &lt;strong&gt;false positives concentrate on the people writing the best documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I actually want to leave here
&lt;/h2&gt;

&lt;p&gt;Through all of it, &lt;strong&gt;the test suite was green before any of these fixes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There were tests for &lt;code&gt;--code-blocks&lt;/code&gt;. They asserted that enabling it made the fourth scanner check fenced paths. They did not assert that the other three were skipping them. There were tests for fence tracking. None of them fed in a document with an odd marker count.&lt;/p&gt;

&lt;p&gt;The tests I write only cover the ways I imagined it breaking. 118 repositories of other people's &lt;code&gt;AGENTS.md&lt;/code&gt; cover the ways I did not. Until I ran it against them, I believed my tool worked.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hyuga/reflint                 &lt;span class="c"&gt;# auto-detects AGENTS.md / llms.txt / CLAUDE.md&lt;/span&gt;
npx @hyuga/reflint &lt;span class="nt"&gt;--code-blocks&lt;/span&gt;   &lt;span class="c"&gt;# also check inside fenced blocks (this works now)&lt;/span&gt;
npx @hyuga/reflint &lt;span class="nt"&gt;--since&lt;/span&gt; main    &lt;span class="c"&gt;# only files changed against a git ref&lt;/span&gt;
npx @hyuga/reflint &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI, which is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reflint&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;reflint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hyuga611/reflint@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings show up as inline PR annotations. One correction I had to make to my own README: GitHub does not block a merge on a failing check until you mark that check required. Until then it is a red X that anyone can click past.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/hyuga611/reflint" rel="noopener noreferrer"&gt;https://github.com/hyuga611/reflint&lt;/a&gt; — the changelog is written up per release, including the ones where I was wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devtools</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>I asked one question. All four of my linters failed it.</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:23:33 +0000</pubDate>
      <link>https://dev.to/hyuga611/i-asked-one-question-all-four-of-my-linters-failed-it-1mmm</link>
      <guid>https://dev.to/hyuga611/i-asked-one-question-all-four-of-my-linters-failed-it-1mmm</guid>
      <description>&lt;p&gt;Three separate times, in one of my linters, the same bug came back wearing a different hat: &lt;strong&gt;a text rule was looking at mentions and treating them as execution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That matters more than it sounds, because of who writes mentions. If you never think about &lt;code&gt;scripts/deprecated.sh&lt;/code&gt;, you never type it. The person who writes &lt;code&gt;Never run scripts/deprecated.sh — it was removed in the migration&lt;/code&gt; is the person being careful. My rules were reading that line as a step and reporting it. &lt;strong&gt;The tool punished exactly the people who did it right.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the third occurrence I stopped fixing instances and asked whether the shape was in my other tools. It was. In all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;I did not ask for a code review. Reviews come back with style notes. I asked a single adversarial question and gave it the source:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Construct an input that punishes the person who wrote it carefully.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four tools, four hits.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;What the careful writer got&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;skills-lint&lt;/td&gt;
&lt;td&gt;0.9.0&lt;/td&gt;
&lt;td&gt;A prohibition &lt;strong&gt;silenced a real broken reference&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tracklint&lt;/td&gt;
&lt;td&gt;0.7.0&lt;/td&gt;
&lt;td&gt;Comments counted as code — &lt;strong&gt;in both directions&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tokenlint&lt;/td&gt;
&lt;td&gt;0.5.0&lt;/td&gt;
&lt;td&gt;React inline styles missed entirely, inflating the headline number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reflint&lt;/td&gt;
&lt;td&gt;0.12.0&lt;/td&gt;
&lt;td&gt;Commands and paths inside prohibition sentences counted as steps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The worst one was not a false positive
&lt;/h2&gt;

&lt;p&gt;skills-lint checks whether a &lt;code&gt;SKILL.md&lt;/code&gt; references files that do not exist. It has a sensible exemption: if the document says the skill &lt;em&gt;produces&lt;/em&gt; a file, that file is not expected to be there yet. &lt;code&gt;PRODUCES&lt;/code&gt; matched on stems like &lt;code&gt;generat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So this happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Never generate &lt;span class="sb"&gt;`scripts/missing.py`&lt;/span&gt;.
When asked to deploy, execute &lt;span class="sb"&gt;`scripts/missing.py`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 1 registered &lt;code&gt;scripts/missing.py&lt;/code&gt; as an artifact this skill produces. Once a name is registered, references to it are exempt &lt;strong&gt;anywhere in the document&lt;/strong&gt; — including line 2, which is a genuinely broken reference to a file that does not exist.&lt;/p&gt;

&lt;p&gt;The tool printed &lt;code&gt;✓ all clean&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the part I keep thinking about. A false positive is loud: someone opens an issue, you fix it. &lt;strong&gt;A warning that never fires is invisible.&lt;/strong&gt; If a user had hit this, they would have concluded their skill was fine. There is no complaint to receive. The only reason I found it is that I went looking for the input that would cause it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broken in both directions at once
&lt;/h2&gt;

&lt;p&gt;tracklint checks conversion tracking on web forms, and it was matching regexes against raw text. Comments are raw text.&lt;/p&gt;

&lt;p&gt;The direction that punishes the honest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- This site intentionally does not call gtag() or load Google Analytics. --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line convinced the tool the project &lt;em&gt;has&lt;/em&gt; analytics, which switched on every wiring rule. An identical form went from &lt;code&gt;clean&lt;/code&gt; to &lt;code&gt;1 error&lt;/code&gt; because someone documented that they don't track.&lt;/p&gt;

&lt;p&gt;The direction that hides the real problem:&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="c1"&gt;// gtag must never be called here; conversion tracking is not implemented yet.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counted as a real call, so an AJAX form with genuinely no success-time tracking stopped reporting &lt;code&gt;ajax-no-conversion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;stripComments()&lt;/code&gt; that blanks comments while preserving length and newlines, so line numbers don't shift. I deliberately made it &lt;strong&gt;conservative&lt;/strong&gt; — &lt;code&gt;https://&lt;/code&gt; and &lt;code&gt;src="//cdn..."&lt;/code&gt; are not comments, and over-stripping would make the tool miss the analytics install and go quiet. Between a fix that over-reports and a fix that under-reports, I took over-reporting, for the same reason as above: noise is visible, silence isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a miss flatters your own headline number
&lt;/h2&gt;

&lt;p&gt;tokenlint reports a design-token coverage percentage: &lt;code&gt;tokenized / (tokenized + hardcoded)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its markup scanner only understood quoted HTML attributes, &lt;code&gt;style="..."&lt;/code&gt;. It had never once matched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ff0000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the ordinary way to hardcode a color in React. Files consisting entirely of hardcoded colors reported &lt;strong&gt;zero hardcoded values&lt;/strong&gt; — and because the miss lands in the denominator, every miss pushes the coverage number &lt;em&gt;up&lt;/em&gt;. My tool's own advertised metric was wrong in the flattering direction.&lt;/p&gt;

&lt;p&gt;Worth separating two things: this was a miss, not a judgment call. A &lt;code&gt;//&lt;/code&gt; line comment containing a Tailwind class is still not scanned, and that one stays — &lt;code&gt;//&lt;/code&gt; cannot be told apart from &lt;code&gt;https://&lt;/code&gt; without hiding whole URL lines, which trades a false positive for a miss. That trade I'm making on purpose. The React one I just never checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  One caveat about how this was found
&lt;/h2&gt;

&lt;p&gt;The model I asked could not run &lt;code&gt;node&lt;/code&gt; in its sandbox, so it validated by calling internal functions and reasoning about them. That is not evidence. &lt;strong&gt;Every claim it made, I re-ran through the real CLI myself&lt;/strong&gt; before writing a fix. After publishing, I installed each package fresh from npm and re-ran the failing path against the released build.&lt;/p&gt;

&lt;p&gt;An adversarial reviewer is very good at generating the input you would not have thought of. It is not a substitute for executing that input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions worth asking your own tool
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Construct an input that punishes the person who wrote it carefully."&lt;/strong&gt; Better than "review this." It forces a concrete artifact you can run instead of a list of concerns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What does this change stop reporting?"&lt;/strong&gt; Every one of these bugs was easy to see once framed as a miss, and invisible while framed as a fix. If your tool has a coverage percentage or a clean/dirty verdict, ask which direction a bug in it would move that number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sources, if you want the actual diffs. The changelogs are written in Japanese, but each entry contains the literal failing input, and that part reads fine in any language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/hyuga611/skills-lint" rel="noopener noreferrer"&gt;skills-lint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hyuga611/tracklint" rel="noopener noreferrer"&gt;tracklint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hyuga611/tokenlint" rel="noopener noreferrer"&gt;tokenlint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hyuga611/reflint" rel="noopener noreferrer"&gt;reflint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four were green on their own test suites the entire time. That's the other thing worth saying out loud: a test suite you wrote is a picture of what you already thought of.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I audited 2,465 published agent skills. 192 of them cannot be selected the way the spec says</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Tue, 18 Aug 2026 01:03:53 +0000</pubDate>
      <link>https://dev.to/hyuga611/i-audited-2465-published-agent-skills-192-of-them-cannot-be-selected-the-way-the-spec-says-4k70</link>
      <guid>https://dev.to/hyuga611/i-audited-2465-published-agent-skills-192-of-them-cannot-be-selected-the-way-the-spec-says-4k70</guid>
      <description>&lt;h2&gt;
  
  
  192 skills have no frontmatter at all
&lt;/h2&gt;

&lt;p&gt;An agent picks a skill by reading its &lt;code&gt;description&lt;/code&gt; and deciding whether the request matches. No &lt;code&gt;description&lt;/code&gt;, no selection — whatever else is in the file.&lt;/p&gt;

&lt;p&gt;I drew 2,465 skills at random from a public registry and ran a reference-integrity and portability check over them. &lt;strong&gt;192 of them (7.8%) ship without a single line of YAML frontmatter.&lt;/strong&gt; No &lt;code&gt;name&lt;/code&gt;, no &lt;code&gt;description&lt;/code&gt;. They are published, downloadable, and cannot be chosen the way the specification says skills get chosen.&lt;/p&gt;

&lt;p&gt;I should be careful about what that does and doesn't say. It is a statement about the files, not about any particular runtime — what a given agent does when the string is missing is something I did not measure, and inferring runtime behaviour from files is a mistake I made elsewhere in this same audit (below). What the count says is that in 192 published skills, the input that selection is supposed to run on is not there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the breakdown
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1,424 of 2,465 skills (57.8%) have at least one error-level finding.&lt;/strong&gt; Percentages are the share of sampled skills with at least one finding of that kind, not the share of findings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;skills&lt;/th&gt;
&lt;th&gt;of sample&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;frontmatter &lt;code&gt;name&lt;/code&gt; differs from the registry slug&lt;/td&gt;
&lt;td&gt;720&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a back-quoted path or link that does not resolve in the package&lt;/td&gt;
&lt;td&gt;445&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ships with no YAML frontmatter at all&lt;/td&gt;
&lt;td&gt;192&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;an absolute path that resolves only on the author's machine&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;malformed &lt;code&gt;allowed-tools&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a provider API key read straight from the environment&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;an external CLI the skill never declares&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One caveat I want to put next to the top row rather than in a footnote: that 29.2% is not 29.2% broken. The registry deliberately keeps the routable slug separate from the stored display name, and says so in its own docs, so a real share of those are working as intended. What remains worth knowing is that on nearly a third of published skills, the name the agent registers under is not the name the registry shows you.&lt;/p&gt;

&lt;p&gt;The sample is 2,465 drawn uniformly from 69,265 enumerated, seed &lt;code&gt;20260804&lt;/code&gt;. Those 69,265 are the most-recently-updated slice of the registry, not all of it, so every rate above is a rate inside that frame. The harness is published and the seed is fixed; re-run it and you should get the same draw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that mattered was my own
&lt;/h2&gt;

&lt;p&gt;Separately from the registry sample, I audited all 46 skills bundled inside &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;openclaw/openclaw&lt;/a&gt; — a repository with 385,000 stars — at a pinned commit.&lt;/p&gt;

&lt;p&gt;The first run produced &lt;strong&gt;219 findings&lt;/strong&gt;. After six precision fixes to my own tools, the same 46 skills produced &lt;strong&gt;59&lt;/strong&gt;. Every genuine defect appears in both runs. So the 160 that vanished were all mine.&lt;/p&gt;

&lt;p&gt;Publishing before that work would have made the census wrong by nearly a factor of four.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I had wrong
&lt;/h2&gt;

&lt;p&gt;The big one, 139 of 197 path findings: I resolved references against the skill's own folder instead of the repository it lives in. A skill sitting inside a repo and pointing at &lt;code&gt;packages/&lt;/code&gt; or &lt;code&gt;docs/&lt;/code&gt; got reported as pointing at nothing.&lt;/p&gt;

&lt;p&gt;The rest, listed plainly because the list is the useful part:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;openai/gpt-5.4&lt;/code&gt; read as a file path, because &lt;code&gt;.4&lt;/code&gt; looks like an extension.&lt;/li&gt;
&lt;li&gt;An artifact excused on the line that creates it, but reported as a missing reference on the line that reads it back — 16 of 80.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/home/YOUR_USER/…&lt;/code&gt; treated as an author's real home directory.&lt;/li&gt;
&lt;li&gt;Indented frontmatter parsing to nothing, so a skill with both required keys was reported as having neither.&lt;/li&gt;
&lt;li&gt;Sample output inside a fenced code block checked as if it were a live reference — 10.8% of path findings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Number 5 I found by dogfooding: a skill written to &lt;em&gt;document&lt;/em&gt; these linters could not pass them, because it quoted their own example output.&lt;/p&gt;

&lt;p&gt;Number 1 has a detail I keep thinking about. The guard that was supposed to stop model identifiers being read as paths already existed, with a comment explaining exactly why it existed. The example in that comment ended in a letter. Anything ending in a version number sailed straight through. The comment was right and the code under it was not, and the comment is what stopped me looking.&lt;/p&gt;

&lt;p&gt;A linter dies of false positives, so I treated the false positives as the bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding that looks textbook and isn't
&lt;/h2&gt;

&lt;p&gt;One bundled skill hardcodes &lt;code&gt;/Users/steipete/openclaw&lt;/code&gt; — a maintainer's home directory, in a file shipped to everybody. That is the textbook portability finding, and it is &lt;strong&gt;not a bug&lt;/strong&gt;. That skill is an explicit single-machine runbook for one canonical checkout, and the path is correct there. It was deliberately left out of the upstream report.&lt;/p&gt;

&lt;p&gt;This is the part a percentage cannot carry. A linter produces candidates. Whether a candidate is a defect depends on what the document is &lt;em&gt;for&lt;/em&gt;, and that judgement did not survive automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  One report was closed as &lt;code&gt;not planned&lt;/code&gt;, and closing it was right
&lt;/h2&gt;

&lt;p&gt;I filed two issues upstream. One was rejected, and I agree with the rejection.&lt;/p&gt;

&lt;p&gt;I had argued that a skill could not read the files bundled alongside it. It can: the prompt that hands the skill list to the model spells out the relative-path resolution rule. I had read one file, found no rule in it, and stopped.&lt;/p&gt;

&lt;p&gt;That is worth more than the finding would have been. &lt;strong&gt;Choosing a resolution rule is choosing a definition of "broken," and that definition has to match runtime behaviour, not the filesystem.&lt;/strong&gt; Mine was tuned to the permissive side afterwards, which is why every number above is a floor rather than a ceiling.&lt;/p&gt;

&lt;p&gt;The other report held up: two bundled skills pointed at files deleted in an earlier refactor, so the reads return nothing and the work proceeds without the rules it was supposed to apply — silently, as usual. The fix is merged and the issue is closed as completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not publish, and why
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The corpus.&lt;/strong&gt; 2,465 third-party skill files are other people's work. Re-fetch them with the harness instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 94 author paths.&lt;/strong&gt; Reported as a count, never as a list, with home-directory segments redacted in the example output. Nobody was contacted. Mass-filing issues against individuals, on the strength of a rule with a known false-positive history, would be the wrong move — see the entire middle of this post.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproducing
&lt;/h2&gt;

&lt;p&gt;The harness, the seed, and the results are in &lt;a href="https://github.com/hyuga611/agent-skill-audit" rel="noopener noreferrer"&gt;agent-skill-audit&lt;/a&gt;. Data is CC0, harness is MIT.&lt;/p&gt;

&lt;p&gt;The rates above are tied to the tool versions used on the run: tenken 0.2.0, skills-lint 0.7.1, carrylint 0.2.2, reflint 0.8.3. All of them have moved since — precision fixes in some places, new rules in others — so a re-run today would not reproduce the table exactly. That is the reason to publish the harness and not only the numbers.&lt;/p&gt;

&lt;p&gt;Corrections to the numbers themselves are welcome, and I would rather have them in public than not.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devtools</category>
      <category>testing</category>
    </item>
    <item>
      <title>My linter kept warning the people who did it right. Three times, in the same direction</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Mon, 17 Aug 2026 09:53:09 +0000</pubDate>
      <link>https://dev.to/hyuga611/my-linter-kept-warning-the-people-who-did-it-right-three-times-in-the-same-direction-2j45</link>
      <guid>https://dev.to/hyuga611/my-linter-kept-warning-the-people-who-did-it-right-three-times-in-the-same-direction-2j45</guid>
      <description>&lt;h2&gt;
  
  
  The warning landed on the only people who had done it properly
&lt;/h2&gt;

&lt;p&gt;I maintain a linter that reads agent config files — &lt;code&gt;SKILL.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt; — and fails CI when they bake in something that only works on the author's machine. One of its rules says: if you call an external CLI, declare it, or the next person won't have it.&lt;/p&gt;

&lt;p&gt;Declaring it means naming it in frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;codex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except that anyone with more than one dependency writes the list form, because that's what YAML is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;codex&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gemini&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My implementation only read the first shape. So the block list — the normal way, the way you write it the moment you have two of anything — was invisible to the linter, and it warned you for an undeclared CLI that you had, in fact, declared.&lt;/p&gt;

&lt;p&gt;Read that back slowly. Authors who ignored the dependency question entirely were never flagged, because they never wrote a &lt;code&gt;requires:&lt;/code&gt; key at all. Authors who sat down and wrote the contract properly got a warning telling them they hadn't. &lt;strong&gt;The rule was inverted with respect to the thing it was trying to encourage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I shipped that. It went out in a patch release, and I only found it because a commenter used the phrase "dependency contract" and I went to re-read my own implementation of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it happened again. Twice, in one release
&lt;/h2&gt;

&lt;p&gt;Two comments on a post of mine turned into new rules. One of them, &lt;code&gt;unverified-write&lt;/code&gt;, reports a file that changes external state — &lt;code&gt;git push&lt;/code&gt;, &lt;code&gt;npm publish&lt;/code&gt;, an &lt;code&gt;INSERT&lt;/code&gt; — and never reads that state back anywhere.&lt;/p&gt;

&lt;p&gt;Before publishing, I ran it over 586 real skill files pulled from a public registry, found two false-positive shapes in the data, fixed both, and re-measured. Fire rate 0.7%, and every hit I could check by hand was genuine. I felt good about it.&lt;/p&gt;

&lt;p&gt;Then I handed the diff to a different model for a pre-publish read, and it produced this input in about a minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Never run &lt;span class="sb"&gt;`git push --force`&lt;/span&gt; from this skill.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a &lt;code&gt;git push&lt;/code&gt; in a code span, in a file with no read-back anywhere. My rule flagged it as an unverified write.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; are &lt;em&gt;full&lt;/em&gt; of that sentence. Writing down "don't push without asking" is the single most common act of care in that genre of file. I had built a rule that warns you for pushing, specifically because you wrote down that you must not push.&lt;/p&gt;

&lt;p&gt;I fixed it, published, and then noticed the same shape one layer further out. Prohibitions were now excluded, but permissions were not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`git push`&lt;/span&gt; は明示の指示があるときだけ。
&lt;span class="p"&gt;-&lt;/span&gt; Only run &lt;span class="sb"&gt;`git push`&lt;/span&gt; when the user asks.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm publish`&lt;/span&gt; requires approval from a maintainer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody who writes those sentences has an unverified write. They have a policy. Three releases, three variations, all pointing the same way: &lt;strong&gt;the warning finds the author who wrote the rule down and misses the one who never mentioned it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it points that way
&lt;/h2&gt;

&lt;p&gt;A text-matching rule cannot see actions. It sees &lt;em&gt;mentions&lt;/em&gt;. And mentions of a dangerous operation are not distributed randomly across authors — they concentrate in the files of people who thought about it.&lt;/p&gt;

&lt;p&gt;The careless author's &lt;code&gt;AGENTS.md&lt;/code&gt; doesn't say &lt;code&gt;git push&lt;/code&gt; anywhere. There is nothing for the rule to catch. The careful author's file says it three times: once to declare when it's allowed, once to forbid the force variant, once in the actual deploy step. Two of those three are not the thing you're detecting, and both of them are evidence of care.&lt;/p&gt;

&lt;p&gt;So the base rate is against you. Among all the files containing the string you match on, the share written by conscientious authors is much higher than in the population — and every false positive you have is drawn from that pool. The people most likely to read your warning carefully, and most likely to uninstall you over it, are the people you are most likely to be wrong about.&lt;/p&gt;

&lt;p&gt;Static analysis has a name for the underlying distinction — use versus mention — and my older rule already knew it. Its CLI check ignores a bare &lt;code&gt;`codex`&lt;/code&gt; in prose and only fires on &lt;code&gt;codex exec build&lt;/code&gt;, an actual invocation with an argument. I wrote that exclusion two releases earlier, in response to the same class of complaint, and then built a new rule without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit that found nothing here
&lt;/h2&gt;

&lt;p&gt;The part I want to be honest about: running against 586 real files did not catch any of this.&lt;/p&gt;

&lt;p&gt;It couldn't. Published, downloadable skills are written to be &lt;em&gt;used&lt;/em&gt;; they say "run this" far more often than "never run this." The prohibition shape lives in team-internal &lt;code&gt;AGENTS.md&lt;/code&gt; files that nobody uploads to a registry. My corpus was real data, and it was the wrong real data — biased, in exactly the direction that hid the failure.&lt;/p&gt;

&lt;p&gt;That's worth separating out, because "test against real data" is advice I've given in writing and still believe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real-data audit tells you what your rule does to &lt;strong&gt;the corpus you can reach&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;An adversarial read tells you what your rule does to &lt;strong&gt;the input someone constructs on purpose&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one found in one pass what 586 files had not. It cost one prompt. If your detector will run on files you can't see — and a linter always does — you need both, and you should assume the corpus is the more comfortable of the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixed it
&lt;/h2&gt;

&lt;p&gt;Not more keywords. The fix was making the use/mention distinction structural, then checking the price:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A write signal only counts &lt;strong&gt;in a code context&lt;/strong&gt; — inside a fence, or inside a backtick span. Prose saying "after that we push to git" is not a step.&lt;/li&gt;
&lt;li&gt;A line carrying a prohibition (&lt;code&gt;never&lt;/code&gt;, &lt;code&gt;do not&lt;/code&gt;, 禁止) isn't a step.&lt;/li&gt;
&lt;li&gt;A line carrying a condition or a permission (&lt;code&gt;only … when&lt;/code&gt;, &lt;code&gt;requires approval&lt;/code&gt;, 〜のときだけ) isn't a step &lt;strong&gt;outside a fence&lt;/strong&gt;. Inside one, the lines are commands, and excluding them there would drop a real finding over an incidental comment like &lt;code&gt;git push origin main   # main only&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the part that isn't optional: re-run the corpus and prove the exclusions didn't eat the signal. Four true positives before, the same four after, fire rate unchanged at 0.7%. An exclusion you didn't measure is just a rule you deleted with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing, since your linter runs on other people's files
&lt;/h2&gt;

&lt;p&gt;The same review turned up something unrelated but worse. My check for a remote copy looked 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;\b(?:scp|rsync)\b[^\n]*\s\S+@\S+:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Greedy fill, then a search for &lt;code&gt;something@something:&lt;/code&gt;. On a long line containing many &lt;code&gt;@&lt;/code&gt; and no colon, it backtracks quadratically: 77ms at 20k characters, 312ms at 40k, and it keeps going up from there. A minified blob or a base64 payload on one line of somebody's repo is enough.&lt;/p&gt;

&lt;p&gt;I had been thinking of the input as "config files people wrote." It isn't. It's &lt;strong&gt;arbitrary text from strangers&lt;/strong&gt;, and a linter that hangs is a linter that stops a stranger's CI. The fix was to stop the filler crossing an &lt;code&gt;@&lt;/code&gt; so there's nothing to backtrack over; an 80,000-character line is now a test case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb I'm keeping
&lt;/h2&gt;

&lt;p&gt;When a detector matches on text, ask who says that text most often. If the answer is "the people being careful about it," your false positives are not evenly distributed — they're aimed. And the only reliable way to see it is to hand the rule to something that is actively trying to embarrass you, because your own corpus is made of the cases you already knew about.&lt;/p&gt;

&lt;p&gt;The tools: &lt;a href="https://github.com/hyuga611/carrylint" rel="noopener noreferrer"&gt;carrylint&lt;/a&gt; is the linter above; the read-back half it deliberately doesn't attempt is &lt;a href="https://github.com/hyuga611/genchi" rel="noopener noreferrer"&gt;genchi&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>ai</category>
      <category>opensource</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>That amazing skill you shared only runs on your machine. I built a linter for it — then found I was 85% wrong</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Mon, 17 Aug 2026 02:09:29 +0000</pubDate>
      <link>https://dev.to/hyuga611/that-amazing-skill-you-shared-only-runs-on-your-machine-i-built-a-linter-for-it-then-found-i-was-2dc2</link>
      <guid>https://dev.to/hyuga611/that-amazing-skill-you-shared-only-runs-on-your-machine-i-built-a-linter-for-it-then-found-i-was-2dc2</guid>
      <description>&lt;h2&gt;
  
  
  I mass-produced skills by saying "turn this into a skill." Then I shared them
&lt;/h2&gt;

&lt;p&gt;With Claude Code or Codex, any procedure that worked can become a reusable &lt;code&gt;SKILL.md&lt;/code&gt; just by asking "turn what we just did into a skill." I did this for image generation, deploy steps, everything. It was great.&lt;/p&gt;

&lt;p&gt;Where it fell apart was handing those skills to teammates. They ran on my machine and silently died on theirs. Every time I dug in, it was the same shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Output path hardcoded to &lt;code&gt;C:\Users\atlan\Downloads\out.png&lt;/code&gt; (my home directory)&lt;/li&gt;
&lt;li&gt;The body calls &lt;code&gt;codex exec ...&lt;/code&gt; with no instruction anywhere for installing that CLI&lt;/li&gt;
&lt;li&gt;Assumes &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; is already set, with no guidance when it isn't&lt;/li&gt;
&lt;li&gt;A model id like &lt;code&gt;gpt-image-2&lt;/code&gt; written inline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them are the author's environment baked in. None of them raise an error — they just fail quietly in the next person's hands, which is the worst kind.&lt;/p&gt;

&lt;p&gt;So I built &lt;code&gt;carrylint&lt;/code&gt;, a linter that fails CI on exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard made the &lt;em&gt;format&lt;/em&gt; portable. Whether the contents run is a separate question
&lt;/h2&gt;

&lt;p&gt;In December 2025, Anthropic made Agent Skills an open standard. One &lt;code&gt;SKILL.md&lt;/code&gt; now runs across Claude Code, Codex, Gemini CLI, Cursor, Copilot, and 20-odd others. &lt;strong&gt;Format&lt;/strong&gt; portability is solved.&lt;/p&gt;

&lt;p&gt;But a standard only guarantees the shape of the container. If the inside holds an absolute path or an undeclared CLI, the container is valid and the contents still don't run for anyone else. I couldn't find a tool looking at that.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;reflint&lt;/strong&gt; (also mine) asks: do the references exist?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;skills-lint&lt;/strong&gt; (also mine) asks: do skills collide, is the frontmatter valid?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;carrylint&lt;/strong&gt; asks: do the references resolve &lt;strong&gt;in a different environment, under a different model?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure class is inverted. The others check "is this correct as specified." carrylint checks "will the next person install this and have it actually run."&lt;/p&gt;

&lt;h2&gt;
  
  
  What it catches
&lt;/h2&gt;

&lt;p&gt;Run it against a deliberately non-portable sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ examples/bad/leaky-image-gen/SKILL.md — 3 errors / 3 warnings
  ✗ :16  [abs-path] machine-specific absolute path `C:\Users\alice\Downloads\out.png`
         — will not resolve on anyone else's machine (use a relative path or {baseDir})
  • :16  [undeclared-cli] calls `codex` but never declares or installs it
  ✗ :22  [abs-path] machine-specific absolute path `C:\Users\alice\Downloads\out.png`
  • :25  [provider-env] assumes `OPENAI_API_KEY` is set
  ✗ :27  [placeholder] unresolved placeholder `&amp;lt;FILL_ME&amp;gt;` left in a shipped file
  • :29  [todo] TODO/FIXME marker left in a shipped file

carrylint: 3 errors / 3 warnings
exit code: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules are split by severity. False positives are what get a linter uninstalled, so &lt;strong&gt;only things the next person will hit, with no room for interpretation&lt;/strong&gt;, are &lt;code&gt;error&lt;/code&gt; (which fails the PR). I moved that line a lot after shipping — see below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;error&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Author-environment absolute paths (&lt;code&gt;C:\…&lt;/code&gt;, &lt;code&gt;/Users/&amp;lt;realname&amp;gt;/&lt;/code&gt;); unfinished markers (&lt;code&gt;&amp;lt;FILL_ME&amp;gt;&lt;/code&gt;, &lt;code&gt;REPLACE_ME&lt;/code&gt;). &lt;code&gt;$HOME&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;, &lt;code&gt;YOUR_API_KEY&lt;/code&gt;, &lt;code&gt;/path/to/&lt;/code&gt; are &lt;strong&gt;excluded&lt;/strong&gt; — they're portable or a documentation convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;warn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Undeclared external CLIs (host commands like &lt;code&gt;claude mcp add&lt;/code&gt; excluded); raw provider-specific env references; leftover &lt;code&gt;TODO:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opt-in&lt;/td&gt;
&lt;td&gt;Hardcoded model ids (&lt;code&gt;claude-*&lt;/code&gt;, &lt;code&gt;gpt-*&lt;/code&gt;) — deliberate pinning is legitimate, so off by default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No LLM and no API key at runtime — pure static analysis. Since the criterion &lt;em&gt;is&lt;/em&gt; "are you locked into one environment or model", it works identically whether the skill was written by Claude or Codex. The tool embodies the thing it checks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I ran it against 230 real skills and learned I was 85% wrong
&lt;/h2&gt;

&lt;p&gt;This is the part I most want to be honest about. I had shipped it, but something nagged before I promoted it: &lt;strong&gt;my own examples and my own tests passing proves nothing.&lt;/strong&gt; So I collected &lt;strong&gt;230 real &lt;code&gt;SKILL.md&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt; files&lt;/strong&gt; from public GitHub repos and ran carrylint against them untouched.&lt;/p&gt;

&lt;p&gt;The good half. &lt;strong&gt;Skills that only run for their author are genuinely out there.&lt;/strong&gt; One PPT-generation skill hardcoded &lt;code&gt;/Users/guohao/Documents/...&lt;/code&gt; — the author's Mac path — into the body. Another used &lt;code&gt;C:/Users/vudrk/Desktop/AI Projects/&lt;/code&gt; as the base for every script. None of these error out; they die quietly for the next person. carrylint's reason to exist was sitting in the real data.&lt;/p&gt;

&lt;p&gt;The bad half. &lt;strong&gt;About 85% of my errors were false positives.&lt;/strong&gt; I was flagging &lt;code&gt;Bearer YOUR_API_KEY&lt;/code&gt; (the standard API-docs convention for "put your key here") as unfinished. I was flagging &lt;code&gt;claude mcp add&lt;/code&gt; — the host itself — as an undeclared CLI. I was flagging &lt;code&gt;$HOME/...&lt;/code&gt;, which resolves per user and is the &lt;em&gt;portable&lt;/em&gt; way to write it, as a machine-specific absolute path. I'd written that false positives are a linter's only cause of death, and then nearly shipped promotion on top of an 85% false-positive rate.&lt;/p&gt;

&lt;p&gt;Luckily the real data named exactly what to fix. &lt;strong&gt;v0.1.1&lt;/strong&gt; corrected four things: &lt;code&gt;$HOME&lt;/code&gt;/&lt;code&gt;~&lt;/code&gt;/generic names are portable; placeholders narrowed to genuine fill-me markers; host CLI setup commands (&lt;code&gt;claude mcp add&lt;/code&gt; etc.) excluded; the home-relative path rule dropped entirely. Re-run against the same 230: &lt;strong&gt;error false positives went from ~85% to near zero, and every true positive survived.&lt;/strong&gt; I then ran it against 70 repos it had never seen to check I hadn't overfit (3% fired, all genuine). The real examples I found are bundled in the repo as regression tests.&lt;/p&gt;

&lt;p&gt;One lesson: &lt;strong&gt;your own tests passing is not evidence of correctness. You find out by running against real data — and by doing it before you promote, not after.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Honestly: this niche is already crowded
&lt;/h2&gt;

&lt;p&gt;One more admission. Partway through designing this I went looking, and there were already 7+ linters for &lt;code&gt;SKILL.md&lt;/code&gt; as of 2026. My own skills-lint is one of them. This was not an empty lot.&lt;/p&gt;

&lt;p&gt;But I read the ones I could find, and &lt;strong&gt;none of them looked at whether the contents actually run somewhere else.&lt;/strong&gt; They stop at spec compliance and frontmatter. So carrylint does only that. Teams that mix Claude and Codex &lt;em&gt;and&lt;/em&gt; distribute skills to each other are honestly still rare — the demand may be slightly ahead of its time. Still, if "I shared it and it only ran for me" has ever stung you, the low-noise error rules (sharpened by that audit) should earn their place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The standard made &lt;code&gt;SKILL.md&lt;/code&gt; portable as a format. Whether it runs is a different question. carrylint fails CI on that difference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hyuga611/carrylint@v0&lt;/span&gt;     &lt;span class="c1"&gt;# in CI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hyuga/carrylint              &lt;span class="c"&gt;# right now, locally&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/hyuga611/carrylint" rel="noopener noreferrer"&gt;https://github.com/hyuga611/carrylint&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>githubactions</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Don't trust "Done." — forcing AI agents to re-fetch reality before they report completion</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:44:59 +0000</pubDate>
      <link>https://dev.to/hyuga611/dont-trust-done-forcing-ai-agents-to-re-fetch-reality-before-they-report-completion-3hk9</link>
      <guid>https://dev.to/hyuga611/dont-trust-done-forcing-ai-agents-to-re-fetch-reality-before-they-report-completion-3hk9</guid>
      <description>&lt;h2&gt;
  
  
  "Inserted the rows. Done." — except not a single row had landed
&lt;/h2&gt;

&lt;p&gt;I hand a lot of my client work to AI agents. Production deploys, report generation, bulk data inserts. Every procedure that works gets turned into a skill, and by now a few dozen skills run my day-to-day.&lt;/p&gt;

&lt;p&gt;The one that broke me was a bulk insert. At the end, the agent said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Inserted N rows. Done."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A completion report like any other. I almost took it. I opened the admin panel just in case — &lt;strong&gt;not one row had been added.&lt;/strong&gt; The insert command had failed partway through, the error had been swallowed, and the agent had confidently reported completion anyway.&lt;/p&gt;

&lt;p&gt;That was a cold moment. What if it had been a number in a report? A deliverable going to a client? &lt;strong&gt;How many "done" reports had I already waved through that were never actually done?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the nastiest species of hallucination. It isn't a plausible-sounding sentence. &lt;strong&gt;It's the fabrication of the fact that the work was completed at all.&lt;/strong&gt; And humans don't interrogate a confident "Done."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents lie (it isn't malice)
&lt;/h2&gt;

&lt;p&gt;Chase the cause and it isn't that the model is dishonest. It's that &lt;strong&gt;acting and checking are the same step.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One turn of an LLM agent looks roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call a tool (run the insert)&lt;/li&gt;
&lt;li&gt;Look at the return value, generate the next sentence&lt;/li&gt;
&lt;li&gt;Write "Done."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is the problem. When the tool's return value is empty, ambiguous, or a timeout, the model writes &lt;strong&gt;the most likely continuation — "it probably worked."&lt;/strong&gt; In the training data, the most natural thing to follow "ran the insert" is "done", so of course that's what comes out.&lt;/p&gt;

&lt;p&gt;The agent is &lt;strong&gt;claiming completion without ever looking at the world it just changed.&lt;/strong&gt; Having never looked, it can't notice the failure either.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's one fix: "done" may only be claimed from re-fetched real state
&lt;/h2&gt;

&lt;p&gt;The countermeasure is simple. &lt;strong&gt;An operation with side effects must re-read the state of the world afterward and confirm it, before anything is allowed to say "done."&lt;/strong&gt; I call this the &lt;em&gt;completion contract&lt;/em&gt; and made it a shared rule across all my skills. It's three lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anything with side effects — create, update, delete, upload, insert — re-fetches existence and state with a separate command before saying "done."&lt;/strong&gt; Inserted rows? Count them again. Uploaded a file? Hit the URL. Created a record? Look it up by id. &lt;strong&gt;Never use the return value of the same tool call as evidence&lt;/strong&gt; — that's exactly where "I think I did it" breeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty output, errors, and timeouts are never filled in.&lt;/strong&gt; If a count, path, or id couldn't be read, don't guess it — report "empty" or "failed" as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbers that go into a ledger or report must be values confirmed by a re-fetch.&lt;/strong&gt; Anything unconfirmed doesn't get written.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Put differently: &lt;strong&gt;the burden of proof moves to the agent.&lt;/strong&gt; Not "it should have worked" but "I re-read it just now with my own eyes and it is there."&lt;/p&gt;

&lt;p&gt;Here's the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (I think I did it)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Inserting the data
[tool] db_insert(...)   ← empty return value, but not conclusively an error
&amp;gt; Inserted N rows. Done.   ← claimed without looking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (completion contract)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Inserting the data
[tool] db_insert(...)   ← empty return value
&amp;gt; The insert returned nothing. Verifying that the rows exist.
[tool] db_count(where=...)   ← re-fetch
&amp;gt; 0 matching rows. The insert did not land (failed).
  Want me to check the error log and retry?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second agent can't lie. &lt;strong&gt;The moment you insert a step that goes and looks at the world afterward, "I think I did it" stops being physically possible.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the existing guardrails don't cover this
&lt;/h2&gt;

&lt;p&gt;You might think a guardrail library already handles it. I looked — guardrails-ai, deepeval, promptfoo, a few others. They're good tools. &lt;strong&gt;They just guard a different place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What they validate is essentially the &lt;strong&gt;text the LLM produced&lt;/strong&gt;: is the format right, is it harmful, is it factually consistent, is it close to the expected answer. They grade &lt;strong&gt;the content of the output.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the "I think I did it" hallucination is &lt;em&gt;flawless&lt;/em&gt; as text. "Inserted N rows. Done." is grammatical and internally consistent. No amount of grading the text catches it — &lt;strong&gt;because what's wrong isn't the text, it's the state of the world after the action.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a gap here. &lt;strong&gt;There are piles of tools that verify output, but as far as I could find, nothing that re-fetches the world after an action and reconciles the report against reality.&lt;/strong&gt; We've reached the point where agents actually rewrite the world, and verification stopped at "what did the agent say."&lt;/p&gt;

&lt;h2&gt;
  
  
  You can do this today, with no library
&lt;/h2&gt;

&lt;p&gt;The most important part first: &lt;strong&gt;this contract works right now, with no dependency.&lt;/strong&gt; Drop the three rules into your agent's system prompt, &lt;code&gt;CLAUDE.md&lt;/code&gt;, or &lt;code&gt;AGENTS.md&lt;/code&gt; as a single paragraph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Completion contract&lt;/span&gt;
An operation with side effects (create, update, delete, upload, insert) may not be
reported as complete until a separate command has re-fetched the resulting state and
the raw result has been shown. Empty output, errors, and timeouts are reported as
"empty" or "failed" as-is — never filled in with an imagined id, path, or count.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That alone visibly reduced false completions in my setup. Zero cost, zero dependencies. If you only try one thing, try this.&lt;/p&gt;

&lt;h2&gt;
  
  
  But prompts don't hold
&lt;/h2&gt;

&lt;p&gt;Here's what I learned running it: &lt;strong&gt;discipline written into a prompt gets quietly broken on a busy turn.&lt;/strong&gt; As context grows, the model steps over that paragraph "by accident" and starts saying "Done." again. Like a human promising to be careful, a declaration gets broken.&lt;/p&gt;

&lt;p&gt;At some point you want the discipline &lt;strong&gt;enforced, not just written&lt;/strong&gt; — the same way you replace a verbal note in code review with a linter that mechanically fails CI. I wanted "did you actually re-fetch before saying done?" backed by machinery instead of good intentions.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built the piece that enforces it — &lt;code&gt;genchi&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I published a small piece that backs the completion contract with machinery rather than goodwill: &lt;code&gt;genchi&lt;/code&gt; (現地現物 — &lt;em&gt;go and see the actual thing&lt;/em&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @hyuga/genchi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does exactly one thing: &lt;strong&gt;run a probe that re-fetches real state, and issue a verdict from nothing else.&lt;/strong&gt;&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hyuga/genchi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// the side effect&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;insert 45 rows&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;probe&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="c1"&gt;// ← re-reads real state, not the action's return value&lt;/span&gt;
  &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// reaching this line means the 45 rows are really there. Otherwise it threw GenchiIncomplete.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crux is that &lt;strong&gt;&lt;code&gt;verify&lt;/code&gt; / &lt;code&gt;gate&lt;/code&gt; accept nothing but a probe.&lt;/strong&gt; The evidence has to come from calling something at the moment completion is asserted, not from a value handed in beside the claim. Empty results, errors, and timeouts aren't swallowed; they're reported as failures rather than imagined into successes. A returned count of 0 (nothing landed) counts as incomplete too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing that requirement does not buy, and I claimed it did.&lt;/strong&gt; Until 0.3.0 the README said this made "I think I did it" &lt;em&gt;structurally unwritable&lt;/em&gt;. It is one line:&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;doTheInsert&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// suppose nothing landed&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;insert 45 rows&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="na"&gt;probe&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the action's own return value&lt;/span&gt;
               &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// → ok: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A probe is a function, and nothing in JavaScript can force a function to do I/O. Worse, the CLI printed &lt;code&gt;re-fetched: 45&lt;/code&gt; for a &lt;code&gt;--probe "echo 45"&lt;/code&gt; that re-fetched nothing — a tool whose entire subject is "don't report what you didn't check", asserting in its own output a thing it had not checked. Both are corrected in 0.3.0: the wording is now &lt;em&gt;the probe returned&lt;/em&gt;, and &lt;code&gt;--help&lt;/code&gt; states the limit without being asked.&lt;/p&gt;

&lt;p&gt;What requiring a probe actually buys is a &lt;em&gt;place&lt;/em&gt; to put the re-read — an expression somebody wrote on purpose — plus refusals that don't get quietly swallowed. That is worth having. It is less than I wrote down, and the difference is the sort of thing you only find by attacking your own package instead of re-reading it.&lt;/p&gt;

&lt;p&gt;There's a CLI for agents that don't write JS. Hand it a re-fetch command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;genchi verify &lt;span class="nt"&gt;--probe&lt;/span&gt; &lt;span class="s2"&gt;"psql -tAc 'select count(*) from t where batch=123'"&lt;/span&gt; &lt;span class="nt"&gt;--count&lt;/span&gt; 45
&lt;span class="c"&gt;# exit 0=verified / 1=empty or mismatched / 3=probe failed. Raw probe output is always emitted as evidence.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Claude Code, a Stop hook can block a turn that still has unverified completion contracts on it (&lt;code&gt;adapters/claude-code&lt;/code&gt;). No LLM and no API key at runtime — it's a zero-dependency static piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honestly: this may be slightly ahead of demand
&lt;/h2&gt;

&lt;p&gt;Let me be straight. As far as I could find, "re-fetch the world after the action and verify" was a genuine gap. But I don't think that many people have yet been burned specifically by &lt;em&gt;fabricated completion&lt;/em&gt; while letting agents rewrite real things. The demand may be a little ahead of its time.&lt;/p&gt;

&lt;p&gt;I still built it &lt;strong&gt;framework-agnostic&lt;/strong&gt; on purpose — the Claude Code hook is pushed out to a thin adapter and the core works from any agent. I have a linter (carrylint) whose whole message is "don't bake your environment in", so it would be incoherent for my own tool to be Claude-Code-only. Unlike my static linters (reflint for reference integrity, skills-lint for skill collisions, carrylint for runtime portability), this is the first piece of mine that &lt;strong&gt;goes and looks at the world at runtime.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've ever had that cold moment over something that was reported done and wasn't, it should land.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The worst hallucination from an AI agent isn't prose — it's &lt;strong&gt;the fabrication of having completed the work.&lt;/strong&gt; The cause is that acting and checking are the same step.&lt;/li&gt;
&lt;li&gt;There's one fix: &lt;strong&gt;"done" may only be claimed from re-fetched real state.&lt;/strong&gt; Empty and failed get reported as empty and failed.&lt;/li&gt;
&lt;li&gt;Existing guardrails validate &lt;strong&gt;text output&lt;/strong&gt;. &lt;strong&gt;Re-fetching world state after an action and reconciling it&lt;/strong&gt; is the gap.&lt;/li&gt;
&lt;li&gt;The contract &lt;strong&gt;works today as a single prompt paragraph.&lt;/strong&gt; To enforce it in code: &lt;code&gt;npm i @hyuga/genchi&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go doubt one more "Done."&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/hyuga611/genchi" rel="noopener noreferrer"&gt;https://github.com/hyuga611/genchi&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I attacked my own npm package before launching it. It let the proposer approve their own writes</title>
      <dc:creator>hyuga</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:13:32 +0000</pubDate>
      <link>https://dev.to/hyuga611/i-attacked-my-own-npm-package-before-launching-it-it-let-the-proposer-approve-their-own-writes-4mki</link>
      <guid>https://dev.to/hyuga611/i-attacked-my-own-npm-package-before-launching-it-it-let-the-proposer-approve-their-own-writes-4mki</guid>
      <description>&lt;p&gt;I maintain a library whose entire job is one sentence: &lt;em&gt;a person who did not write this statement looked at what it actually does, and said yes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An LLM proposes an &lt;code&gt;UPDATE&lt;/code&gt;. The library runs it for real inside a transaction, reads the before and after values back out of the database, rolls back, and shows a human the measurement. Not a summary the model wrote about its own SQL — the values the database produced when the statement ran.&lt;/p&gt;

&lt;p&gt;Last week I found out it was letting the proposer be the person who said yes. And writing "approved" into the audit trail when they did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I attacked it in the first place
&lt;/h2&gt;

&lt;p&gt;I saw someone get taken apart in a &lt;code&gt;shellcheck&lt;/code&gt; issue thread. They had posted an alternative tool, and the reply was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A cursory glance also tells you it's a vibecoded clone of shellcheck.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One glance. That's the bar now, and my GitHub profile fits the pattern that gets you that reply — eleven repositories published in a month. I do have tests. 386 of them, passing, including 161 against real MySQL 8.4 and PostgreSQL 16 in containers.&lt;/p&gt;

&lt;p&gt;But a passing test suite only contains the attacks you already thought of. So before launching I installed my own published package with &lt;code&gt;npx&lt;/code&gt;, as a stranger, and went looking for a way to break it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing I tried worked
&lt;/h2&gt;

&lt;p&gt;I used the example from my own README — a privilege escalation riding along inside a quota change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llm-safe-sql plan &lt;span class="s2"&gt;"UPDATE members SET quota = quota + 10,
&lt;/span&gt;&lt;span class="go"&gt;    profile = '{\"role\":\"admin\"}' WHERE id = 7" --as kenji

Measured by running the statement and rolling it back
  id = 7
&lt;/span&gt;&lt;span class="gp"&gt;      quota: 5 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;15
&lt;span class="gp"&gt;      profile: {"role":"user"} -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;:&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="go"&gt;
This needs a person. Neither the assistant nor this tool can approve it:
&lt;/span&gt;&lt;span class="gp"&gt;  llm-safe-sql approve &amp;lt;id&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--as&lt;/span&gt; you@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool says it out loud: &lt;em&gt;this needs a person, neither the assistant nor this tool can approve it.&lt;/em&gt; So I approved it as the same person who proposed it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llm-safe-sql approve &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--as&lt;/span&gt; kenji
&lt;span class="go"&gt;Approved.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llm-safe-sql apply &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--as&lt;/span&gt; kenji
&lt;span class="go"&gt;Applied: UPDATE on members, 1 row(s).
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Committed. &lt;code&gt;quota=15&lt;/code&gt;, &lt;code&gt;role=admin&lt;/code&gt;, confirmed by querying the database directly rather than trusting the tool's own output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commit wasn't the frightening part
&lt;/h2&gt;

&lt;p&gt;The audit table was.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;planned    actor=kenji&lt;/span&gt;
&lt;span class="py"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;approved   actor=kenji&lt;/span&gt;
&lt;span class="py"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;applied    actor=kenji&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's an &lt;code&gt;approved&lt;/code&gt; row. Anybody reading that trail later concludes a human reviewed this change. Nobody read anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A missing audit trail is better than that.&lt;/strong&gt; A missing one tells you it's missing. A complete one that describes a review which never happened is a record that decisions get built on top of. I had shipped a machine that stores the absence of review &lt;em&gt;as&lt;/em&gt; review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I can't excuse
&lt;/h2&gt;

&lt;p&gt;I had thought about this attack. Just not all of it.&lt;/p&gt;

&lt;p&gt;My spec document already said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;P5&lt;/strong&gt; — This library's own plan and audit tables are refused &lt;strong&gt;regardless of configuration&lt;/strong&gt;. &lt;em&gt;A model that can write the plan table can approve its own writes.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I knew self-approval was the thing to prevent. I closed the indirect path — tamper with the plan table to mark something approved — and left the front door open. You just run &lt;code&gt;approve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The README's answer to "the model can't approve" was that &lt;code&gt;approve&lt;/code&gt; lives in a separate process the model has no path to. That's true of the deployment I recommend. It is not true of the one &lt;code&gt;npx&lt;/code&gt; gives you, and nobody starts at the recommended deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the check was the interesting part
&lt;/h2&gt;

&lt;p&gt;The comparison itself is three lines. The judgement calls were:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignore case and surrounding whitespace.&lt;/strong&gt; A check that &lt;code&gt;--as Kenji&lt;/code&gt; walks past is theatre. It refuses the honest caller and waves through anyone who hit shift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't get clever about matching.&lt;/strong&gt; &lt;code&gt;alice@example.com&lt;/code&gt; &lt;em&gt;can&lt;/em&gt; approve a plan proposed by &lt;code&gt;alice&lt;/code&gt;. Refusing it because it contains the string would lock out a legitimate second reviewer, and a security check that blocks honest use is a security check that gets switched off. I made it deliberately dumb.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship an escape hatch.&lt;/strong&gt; Some people really do hold both roles — a solo operator with nobody to hand the card to. &lt;code&gt;--allow-self-approve&lt;/code&gt; exists. It approves the plan and leaves &lt;em&gt;both&lt;/em&gt; acts under the one name in the audit trail. It buys you an apply. It does not buy you a tidier story about who reviewed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I nearly shipped something worse
&lt;/h2&gt;

&lt;p&gt;Fix written, tests green, about to publish. And I realised:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--as&lt;/code&gt; is self-asserted.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My new check compares two strings handed to the same process from the same untrusted place. It stops one identity running both halves — an agent and its operator sharing &lt;code&gt;$USER&lt;/code&gt;, which is exactly what a single terminal gives you and exactly how the plausible-looking audit trail gets manufactured by accident. It does nothing about a person who types a different name.&lt;/p&gt;

&lt;p&gt;And my own README says this, in a section about which guards are real:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most of what this library does runs &lt;em&gt;inside this process&lt;/em&gt;, holding a credential that can write. That is worth saying out loud, because the alternative is an operator believing in a boundary that turns out to be &lt;strong&gt;one &lt;code&gt;if&lt;/code&gt; statement in a library they have never read.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had just written an &lt;code&gt;if&lt;/code&gt; statement. Shipping it quietly would have meant the release that adds the guard also commits the exact defect that paragraph exists to prevent.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;check&lt;/code&gt; now says it every run, in every configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;! `--as` is taken at its word: nothing here authenticates anybody.
  So the refusal that stops a proposer approving their own plan catches
  one identity running both halves — an agent and its operator sharing
  $USER, which is what a single terminal gives you — and does not catch
  a person who types a different name.
  Actor separation is a record, not a boundary.
  The boundary is applyConnection: a database account the proposing side
  has no password for.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State what the guard buys and what it doesn't, before anyone asks. What it buys is that a silent non-review becomes a refusal. What it does not buy is authorisation. The only identity here that means anything is a database account the proposing side has no password for. There's a test pinning that line in place, because if it silently disappeared nobody would notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing I deliberately did not fix
&lt;/h2&gt;

&lt;p&gt;The same audit turned up that anyone can cancel anyone else's plan.&lt;/p&gt;

&lt;p&gt;I left it. Cancelling only ever prevents an apply, the MCP surface doesn't expose it, and everyone who can reach it can already write to the plan table directly. Adding a name check there would put a second authorisation-shaped string comparison next to one that already needs a paragraph explaining it isn't authorisation.&lt;/p&gt;

&lt;p&gt;It's written into the spec under "out of scope" so it reads as a decision rather than a gap somebody closes by reflex.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run something like this
&lt;/h2&gt;

&lt;p&gt;Try approving a plan under the same identity that proposed it. If it goes through, your audit log is recording reviews that did not happen.&lt;/p&gt;

&lt;p&gt;If you were on 0.5.2 or earlier of mine, this finds the affected records — verified on MySQL 8.4, PostgreSQL 16 and SQLite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;proposed_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logged_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;llm_safe_sql_audit&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;llm_safe_sql_audit&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'planned'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logged_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rows that come back were committed on one person's word while the trail reads as though two people were involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-in-the-loop only means anything if the loop checks that the approver is somebody else.&lt;/strong&gt; Mine didn't, from the first release through 0.5.2, while saying otherwise on the tin. The test count had nothing to say about it — the hole showed up the first time I installed my own package as a stranger and typed the laziest possible thing.&lt;/p&gt;

&lt;p&gt;Fixed in 0.6.0.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/hyuga611/llm-safe-sql" rel="noopener noreferrer"&gt;https://github.com/hyuga611/llm-safe-sql&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;code&gt;@hyuga/llm-safe-sql&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>security</category>
      <category>database</category>
    </item>
  </channel>
</rss>
