<?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: Ofri Peretz</title>
    <description>The latest articles on DEV Community by Ofri Peretz (@ofri-peretz).</description>
    <link>https://dev.to/ofri-peretz</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%2F3669992%2F6b34d598-2f6e-4b24-89ac-4edb086ffb9b.png</url>
      <title>DEV Community: Ofri Peretz</title>
      <link>https://dev.to/ofri-peretz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ofri-peretz"/>
    <language>en</language>
    <item>
      <title>Three of My Planned Articles Cited Numbers That Do Not Exist</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Thu, 12 Nov 2026 02:20:22 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/three-of-my-planned-articles-cited-numbers-that-do-not-exist-3n00</link>
      <guid>https://dev.to/ofri-peretz/three-of-my-planned-articles-cited-numbers-that-do-not-exist-3n00</guid>
      <description>&lt;p&gt;I keep a document listing twelve articles I intend to write, each with a&lt;br&gt;
measured number behind it. Before writing any of them I checked the numbers&lt;br&gt;
against the benchmark JSON the document names as its source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two of them appear in no result file anywhere.&lt;/strong&gt; A third contradicts the&lt;br&gt;
file it cites. A fourth is a real number attached to the wrong subject.&lt;/p&gt;

&lt;p&gt;Nothing had flagged this, because nothing recomputes prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the audit found
&lt;/h2&gt;

&lt;p&gt;The document says one plugin produced &lt;strong&gt;981&lt;/strong&gt; findings against another's&lt;br&gt;
&lt;strong&gt;21,557&lt;/strong&gt;. The JSON it cites says &lt;strong&gt;1,375&lt;/strong&gt; and &lt;strong&gt;23,325&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep -rlF 21557&lt;/code&gt; across every result file returns nothing. So does &lt;code&gt;4311&lt;/code&gt;, the&lt;br&gt;
other figure in that row. Not "in a different run" — in no run.&lt;/p&gt;

&lt;p&gt;Then the subtler one. The document says a dominant rule accounts for 87% of a&lt;br&gt;
plugin's output and that &lt;strong&gt;"every other rule combined accounts for 0.6%."&lt;/strong&gt; The&lt;br&gt;
87% is exactly right: 20,334 of 23,325. The 0.6% is a real number too — it is&lt;br&gt;
the sum of the &lt;strong&gt;bottom five&lt;/strong&gt; rules, 142 findings. Every &lt;em&gt;other&lt;/em&gt; rule is 2,991,&lt;br&gt;
which is &lt;strong&gt;12.8%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing was invented. A correct figure drifted one clause away from the set it&lt;br&gt;
describes, and in that position it is twenty times wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways a number goes bad in prose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mode&lt;/th&gt;
&lt;th&gt;example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no source at all&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;21,557&lt;/code&gt; — in no result file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contradicts its own cited source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;981&lt;/code&gt; where the JSON says &lt;code&gt;1,375&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;real number, wrong subject&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.6%&lt;/code&gt; = bottom five rules, not "every other rule"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contradicted by a later run&lt;/td&gt;
&lt;td&gt;"297 files" where the result data records 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the third is catchable by careful reading. The rest require the source&lt;br&gt;
open next to the prose, which is exactly the thing nobody does when the&lt;br&gt;
document reads fluently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the document could not fail
&lt;/h2&gt;

&lt;p&gt;The JSON is generated. Re-run the benchmark and every number in it is replaced.&lt;br&gt;
If a figure is wrong, the next run makes it right, and if the harness breaks the&lt;br&gt;
run fails.&lt;/p&gt;

&lt;p&gt;The document is written. It has no relationship to the data except that a person&lt;br&gt;
once read one and typed into the other. It cannot be re-run. It has no failing&lt;br&gt;
state. A number in it is not a measurement — it is &lt;a href="https://ofriperetz.dev/go/reproducibility-vs-replicability?utm_source=devto&amp;amp;from=numbers-in-prose-rot" rel="noopener noreferrer"&gt;a memory of a&lt;br&gt;
measurement&lt;/a&gt;,&lt;br&gt;
and memories are not version-controlled against the thing they remember.&lt;/p&gt;

&lt;p&gt;This is the same shape as &lt;a href="https://ofriperetz.dev/go/proxy-metrics?utm_source=devto&amp;amp;from=numbers-in-prose-rot" rel="noopener noreferrer"&gt;a metric standing in for the thing it&lt;br&gt;
measures&lt;/a&gt;: the document was&lt;br&gt;
treated as the source because it was the artifact people actually read.&lt;/p&gt;

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

&lt;p&gt;Three of the twelve are dropped for the drift above. Not rewritten — dropped,&lt;br&gt;
because their central claims rest on figures nobody can re-derive, and an&lt;br&gt;
article whose number cannot be reproduced is worth less than no article.&lt;/p&gt;

&lt;p&gt;Six more went for other reasons once I started checking: defects already fixed&lt;br&gt;
since the document was written, a claim I could not test with the data on hand,&lt;br&gt;
one topic already covered better elsewhere. &lt;strong&gt;Three of twelve survived&lt;/strong&gt;, and&lt;br&gt;
those three are written.&lt;/p&gt;

&lt;p&gt;I would have written most of the other nine without checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit
&lt;/h2&gt;

&lt;p&gt;Two rules, both cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never let a number live only in prose.&lt;/strong&gt; If it matters, it belongs in a&lt;br&gt;
generated artifact the prose points at. A README that states a benchmark result&lt;br&gt;
should render it from the result file, or link to it, or both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before you cite your own document, grep the number.&lt;/strong&gt; Not the claim — the&lt;br&gt;
digits. &lt;code&gt;grep -rlF 21557 results/&lt;/code&gt; takes a second and is the entire audit. It&lt;br&gt;
found two figures that had been sitting in a planning document for weeks,&lt;br&gt;
waiting to be published as fact.&lt;/p&gt;

&lt;p&gt;The reason this is worth your attention is that the drift is invisible from the&lt;br&gt;
inside. The document read perfectly well. It was internally consistent, it cited&lt;br&gt;
its source by filename, and every number in it looked like the kind of number&lt;br&gt;
that comes out of a benchmark. It just did not.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://ofriperetz.dev/go/r/8v7joo8tnv?utm_source=devto&amp;amp;from=numbers-in-prose-rot" rel="noopener noreferrer"&gt;Interlace ESLint ecosystem&lt;/a&gt;. Source on &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=numbers-in-prose-rot" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · Follow: &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=numbers-in-prose-rot" rel="noopener noreferrer"&gt;Dev.to/ofri-peretz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Take the last number you published about your own work. Can you point at the file it came from?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>javascript</category>
      <category>eslint</category>
    </item>
    <item>
      <title>The Most Precise Plugin in My Benchmark Scored 10.8%</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Sat, 07 Nov 2026 07:59:36 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/the-most-precise-plugin-in-my-benchmark-scored-108-p64</link>
      <guid>https://dev.to/ofri-peretz/the-most-precise-plugin-in-my-benchmark-scored-108-p64</guid>
      <description>&lt;p&gt;Six ESLint security plugins, one labelled corpus of 69 vulnerable fixtures,&lt;br&gt;
same harness, same day — &lt;code&gt;eslint-plugin-security@4.0.1&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;@microsoft/eslint-plugin-sdl@1.1.0&lt;/code&gt;, ESLint 10.8.1, Node 24.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I wrote the fixtures.&lt;/strong&gt; That sentence has to come before the table, because&lt;br&gt;
it changes what every row below is worth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/confusion-matrix-tp-fp-fn-tn?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;TP, FP and FN&lt;/a&gt; are&lt;br&gt;
the four boxes everything below is derived from.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;TP&lt;/th&gt;
&lt;th&gt;FP&lt;/th&gt;
&lt;th&gt;FN&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interlace&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/benchmark-sonarjs-vs-interlace?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;eslint-plugin-sonarjs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;51.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/eslint-plugin-security-is-unmaintained-heres-what-nobody-tells-you-96h?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;eslint-plugin-security&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;23.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/benchmark-microsoft-sdl-vs-interlace?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;@microsoft/eslint-plugin-sdl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;15.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eslint-plugin-no-unsanitized&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;10.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eslint-plugin-security-node&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;10.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three rows have a piece of their own, linked above — one tool read closely&lt;br&gt;
rather than ranked. This is &lt;a href="https://ofriperetz.dev/go/benchmark-17-eslint-security-plugins-compared?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;seventeen plugins&lt;/a&gt;&lt;br&gt;
compressed into one table, and compression is the subject.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read the column the ranking hides
&lt;/h2&gt;

&lt;p&gt;F1 blends precision and recall, which is how a table like this stops being&lt;br&gt;
informative. Split them and a different tool leads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eslint-plugin-no-unsanitized&lt;/code&gt; sits second from the bottom at 10.8% F1. Its&lt;br&gt;
&lt;strong&gt;precision is 80%&lt;/strong&gt; — 4 true positives against 1 false positive, the highest&lt;br&gt;
of any community plugin here. It detects four things. It is right about&lt;br&gt;
substantially all of them.&lt;/p&gt;

&lt;p&gt;That is not a consolation prize. A tool with 80% precision and a narrow scope&lt;br&gt;
is one you leave switched on. A tool that finds more and is wrong more often is&lt;br&gt;
one somebody disables in month three — and a rule nobody runs has a recall of&lt;br&gt;
zero regardless of what the table says.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why my row says 100%, and why you should discount it
&lt;/h2&gt;

&lt;p&gt;A 100% row against fixtures written by the people who wrote the rules is the&lt;br&gt;
expected outcome of grading your own homework. It is not evidence of&lt;br&gt;
superiority and I will not present it as any.&lt;/p&gt;

&lt;p&gt;What it is: &lt;strong&gt;a regression gate we happen to publish.&lt;/strong&gt; It says our rules still&lt;br&gt;
detect what we built them to detect. On the day it stops saying 100%, something&lt;br&gt;
broke.&lt;/p&gt;

&lt;p&gt;Three limits follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The fixtures encode &lt;strong&gt;our&lt;/strong&gt; threat model. A plugin scoped to a different
surface — browser DOM, Angular, Electron — scores low here for reasons that
say nothing about its quality on that surface.&lt;/li&gt;
&lt;li&gt;F1 on a self-authored corpus measures agreement with our idea of the problem,
not correctness in the world.&lt;/li&gt;
&lt;li&gt;Recall against 69 of my fixtures is not recall against the field.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;@microsoft/eslint-plugin-sdl&lt;/code&gt; is the clean example: 15.6% here, and it is a&lt;br&gt;
well-made tool aimed at frontend and Microsoft-stack hardening. Pointing a&lt;br&gt;
Node-backend corpus at it measures the mismatch, not the plugin.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check I ran before publishing any of it
&lt;/h2&gt;

&lt;p&gt;I did not take the F1 column on trust — including my own row.&lt;/p&gt;

&lt;p&gt;Every value was recomputed from its own TP/FP/FN with &lt;code&gt;2·P·R/(P+R)&lt;/code&gt;. All six&lt;br&gt;
matched the published figure to within a tenth of a point. Then the check that&lt;br&gt;
actually matters: &lt;strong&gt;TP + FN = 69 on every row.&lt;/strong&gt; If those totals disagreed, the&lt;br&gt;
rows would be describing six differently-scoped runs and the table would not be&lt;br&gt;
a comparison at all.&lt;/p&gt;

&lt;p&gt;A minute's work, and the difference between a table you can defend and one you&lt;br&gt;
copied. &lt;a href="https://ofriperetz.dev/go/precision-recall-f1-for-static-analysis?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;A number without its measurement is a&lt;br&gt;
memory&lt;/a&gt;,&lt;br&gt;
and &lt;a href="https://ofriperetz.dev/go/ranking-vs-measuring?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;a rank throws away the distribution behind&lt;br&gt;
it&lt;/a&gt; — a leaderboard is&lt;br&gt;
the least recoverable form a measurement can take.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the table is for
&lt;/h2&gt;

&lt;p&gt;Not for picking a plugin. For two narrower things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A scope map.&lt;/strong&gt; The recall column tells you what each tool was built to see.&lt;br&gt;
Four of 69 is not failure; it is a statement of scope, useful when you are&lt;br&gt;
deciding what to run alongside what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A regression gate.&lt;/strong&gt; Which is what it is to me, and the only claim here I&lt;br&gt;
would defend without qualification.&lt;/p&gt;

&lt;p&gt;If you want the version of this you can trust without knowing me, run it&lt;br&gt;
yourself. The corpus and the harness are in&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=labelled-corpus-f1-leaderboard" rel="noopener noreferrer"&gt;the plugins repo&lt;/a&gt; under&lt;br&gt;
&lt;code&gt;benchmarks/&lt;/code&gt;, and the suite that produced this table is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx benchmarks/suites/ilb-juliet/run.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin the versions above or your numbers will not match mine — &lt;code&gt;eslint-plugin-security&lt;/code&gt;'s&lt;br&gt;
recall moves between releases, which is exactly why an unpinned comparison&lt;br&gt;
table is worth nothing. Then add fixtures for the threats &lt;em&gt;you&lt;/em&gt; care about. The&lt;br&gt;
result will disagree with mine, and yours will be the one worth having.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Whose fixtures is your security tooling being graded on — and have you ever read them?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>eslint</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My Harness Reported 0 Findings on 100 Files. All 100 Were Ignored.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Tue, 03 Nov 2026 02:15:06 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/my-harness-reported-0-findings-on-100-files-all-100-were-ignored-1okn</link>
      <guid>https://dev.to/ofri-peretz/my-harness-reported-0-findings-on-100-files-all-100-were-ignored-1okn</guid>
      <description>&lt;p&gt;I ran fourteen lint rules over 100 files and got back zero findings. Every&lt;br&gt;
rule silent, clean sweep, nothing to fix.&lt;/p&gt;

&lt;p&gt;The correct answer was 308 findings in 54 of those files. The harness had read&lt;br&gt;
none of them, exited 0, and printed nothing to stderr.&lt;/p&gt;

&lt;p&gt;Here are the three ways that happens, all of them measured this week, all of&lt;br&gt;
them silent.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. The files were outside the base path
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ESLint&lt;/code&gt; resolves what it is allowed to lint relative to &lt;code&gt;cwd&lt;/code&gt;. Point it at a&lt;br&gt;
directory that is not under the &lt;code&gt;cwd&lt;/code&gt; of the process, and every file comes back&lt;br&gt;
ignored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File ignored because outside of base path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That string arrives as a &lt;strong&gt;message with a null &lt;code&gt;ruleId&lt;/code&gt;&lt;/strong&gt; — the same shape as a&lt;br&gt;
parse error. If your loop counts &lt;code&gt;messages.filter(m =&amp;gt; m.ruleId)&lt;/code&gt;, or skips&lt;br&gt;
files whose messages look unfamiliar, you have just discarded the only&lt;br&gt;
explanation you were given.&lt;/p&gt;

&lt;p&gt;On the CLI it is quieter still. Exit code 0. Empty stderr.&lt;/p&gt;

&lt;p&gt;Fix: set &lt;code&gt;cwd&lt;/code&gt; to the tree you are measuring.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The &lt;code&gt;.ts&lt;/code&gt; files were never eligible
&lt;/h2&gt;

&lt;p&gt;Flat config (ESLint 9.39.2 here) matches files by pattern. Omit &lt;code&gt;files&lt;/code&gt;, and&lt;br&gt;
the default covers &lt;code&gt;.js&lt;/code&gt; and friends — &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;.ts&lt;/code&gt;, in an ecosystem that is&lt;br&gt;
mostly TypeScript.&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;// eslint.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-eval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/a.ts   ← contains eval()
src/b.js   ← clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;eslint &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="go"&gt;0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No output. Exit 0. The &lt;code&gt;.ts&lt;/code&gt; file contains &lt;code&gt;eval()&lt;/code&gt; and was never opened.&lt;/p&gt;

&lt;p&gt;This one is worse than the first, because a partial corpus still produces&lt;br&gt;
plausible numbers. A repository that is 80% TypeScript reports on the other&lt;br&gt;
20% and calls it a result.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;files: ['**/*.ts', '**/*.tsx']&lt;/code&gt;, explicitly, and check the count of files&lt;br&gt;
actually linted against a count you obtained some other way.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The build under test was not the build you ship
&lt;/h2&gt;

&lt;p&gt;In a monorepo, &lt;code&gt;node_modules/&amp;lt;your-package&amp;gt;&lt;/code&gt; is usually a symlink into the&lt;br&gt;
workspace. Anything you measure through it is unreleased source.&lt;/p&gt;

&lt;p&gt;I asked a plugin which of its rules carry a fixer. From the workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prefer-event-target: code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the published package, same command, clean directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prefer-event-target: no fixer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both true. Different questions. I nearly published a correction to a correct&lt;br&gt;
article on the strength of the first one.&lt;/p&gt;

&lt;p&gt;Fix: measure in a scratch directory with an &lt;code&gt;npm i &amp;lt;pkg&amp;gt;@&amp;lt;exact-version&amp;gt;&lt;/code&gt;, and&lt;br&gt;
say which tree the number came from.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why all three survive review
&lt;/h2&gt;

&lt;p&gt;Nothing throws. Nothing warns. The exit code is 0 in every case — and the one&lt;br&gt;
time I saw a &lt;code&gt;1&lt;/code&gt;, it came from a real finding in a file that &lt;em&gt;was&lt;/em&gt; linted, not&lt;br&gt;
from the file that was skipped. "It exited non-zero, so it noticed" is the&lt;br&gt;
wrong conclusion and I drew it once.&lt;/p&gt;

&lt;p&gt;They also fail at three different layers — which files are eligible, which&lt;br&gt;
eligible files are covered, and which build is under test — so fixing one&lt;br&gt;
leaves the others exactly as they were.&lt;/p&gt;

&lt;p&gt;And they all fail toward good news. There is no version of these bugs that&lt;br&gt;
invents findings. That asymmetry is what makes them survive: a number that is&lt;br&gt;
too high gets investigated, and a number that is too low gets celebrated.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check
&lt;/h2&gt;

&lt;p&gt;One rule, before you believe any of it: &lt;strong&gt;write the code the tool must flag,&lt;br&gt;
and make sure it flags it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;C&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&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;red&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;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&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;i&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines, tripping at least two of the rules under test. If the canary is&lt;br&gt;
silent, the corpus number is not a number.&lt;/p&gt;

&lt;p&gt;That is what caught mine. The hundred-file zero was completely believable —&lt;br&gt;
it was the four-line file reporting zero that was not.&lt;/p&gt;

&lt;p&gt;A count from an instrument you have never seen fail is not a measurement, it is&lt;br&gt;
a hope.&lt;/p&gt;

&lt;p&gt;This is the cheap half of &lt;a href="https://ofriperetz.dev/go/what-ground-truth-caught-that-unit-tests-missed?utm_source=devto&amp;amp;from=lint-harness-measured-nothing" rel="noopener noreferrer"&gt;what ground truth catches that unit tests&lt;br&gt;
miss&lt;/a&gt;:&lt;br&gt;
the tests all passed, because the harness they exercised was working — it was&lt;br&gt;
the corpus it pointed at that was empty. It is also why a result you cannot&lt;br&gt;
re-run is not yet a result, which is the whole distinction between&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/reproducibility-vs-replicability?utm_source=devto&amp;amp;from=lint-harness-measured-nothing" rel="noopener noreferrer"&gt;reproducibility and replicability&lt;/a&gt;.&lt;br&gt;
And a zero that means "read nothing" corrupts the one cell every other metric&lt;br&gt;
is derived from — the true-negative box of &lt;a href="https://ofriperetz.dev/go/confusion-matrix-tp-fp-fn-tn?utm_source=devto&amp;amp;from=lint-harness-measured-nothing" rel="noopener noreferrer"&gt;the confusion&lt;br&gt;
matrix&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://ofriperetz.dev/go/r/8v7joo8tnv?utm_source=devto&amp;amp;from=lint-harness-measured-nothing" rel="noopener noreferrer"&gt;Interlace ESLint ecosystem&lt;/a&gt;. Source on &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=lint-harness-measured-nothing" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · Follow: &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=lint-harness-measured-nothing" rel="noopener noreferrer"&gt;Dev.to/ofri-peretz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the last clean report you got — and would you know if the tool had read nothing?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>eslint</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Plugin Has 14 Rules. Two of Them Found 60% of Everything.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:43:29 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/my-plugin-has-14-rules-two-of-them-found-60-of-everything-o3j</link>
      <guid>https://dev.to/ofri-peretz/my-plugin-has-14-rules-two-of-them-found-60-of-everything-o3j</guid>
      <description>&lt;p&gt;A plugin's README tells you it has fourteen rules. It does not tell you that on&lt;br&gt;
your codebase, two of them will do almost all of the work and six will never&lt;br&gt;
speak at all.&lt;/p&gt;

&lt;p&gt;I measured mine. Fourteen rules in &lt;code&gt;eslint-plugin-react-features&lt;/code&gt; that have no&lt;br&gt;
counterpart in &lt;code&gt;eslint-plugin-react&lt;/code&gt;, pointed at the 100 &lt;code&gt;.tsx&lt;/code&gt; files of this&lt;br&gt;
blog — a public tree, so the numbers below are yours to check rather than mine&lt;br&gt;
to be believed on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;308 findings across 54 files.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;th&gt;findings&lt;/th&gt;
&lt;th&gt;files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;react-no-inline-functions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;113&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;react-render-optimization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;71&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hooks-exhaustive-deps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;require-data-slot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-unnecessary-rerenders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-inline-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-raw-color-literal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-arbitrary-token-class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;six rules&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules are 59.7% of the total. Four are 84.7%. The mean is 22 findings per&lt;br&gt;
rule, and the top rule is five times it.&lt;/p&gt;

&lt;p&gt;That is not a rule list. That is a power curve with a rule list printed over it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the total is the wrong number
&lt;/h2&gt;

&lt;p&gt;"308 findings" is the number that reaches a standup, and it is the least useful&lt;br&gt;
one available. It is a sum over a distribution nobody looked at, and it invites&lt;br&gt;
exactly the wrong Monday morning — a team triaging 308 things.&lt;/p&gt;

&lt;p&gt;Read the table instead and the work is two decisions, not 308. Decide once&lt;br&gt;
about inline functions in JSX. Decide once about render optimisation. You have&lt;br&gt;
now addressed 184 findings without opening 184 files, and what remains is small&lt;br&gt;
enough to be honest about.&lt;/p&gt;

&lt;p&gt;This is the same failure as &lt;a href="https://ofriperetz.dev/go/aggregate-benchmarks-lie-heres-what-700-ai-functions-look-like-by-security-domain?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;an aggregate benchmark score&lt;/a&gt;,&lt;br&gt;
one level down: a total computed over a distribution nobody inspected, which is&lt;br&gt;
also &lt;a href="https://ofriperetz.dev/go/ranking-vs-measuring?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;what a leaderboard throws away&lt;/a&gt;&lt;br&gt;
when it collapses a measurement into a rank.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ofriperetz.dev/go/precision-recall-f1-for-static-analysis?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;same trap runs the other way for rule counts&lt;/a&gt;:&lt;br&gt;
fourteen rules sounds like fourteen kinds of coverage. On this corpus it was&lt;br&gt;
eight, and effectively two.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check that stops this being a lie
&lt;/h2&gt;

&lt;p&gt;A concentration this steep has an obvious failure mode: one file with a&lt;br&gt;
repeated pattern, counted 113 times, dressed up as a finding.&lt;/p&gt;

&lt;p&gt;So the file column exists to kill that reading. &lt;code&gt;react-no-inline-functions&lt;/code&gt;&lt;br&gt;
fires across &lt;strong&gt;38 distinct files&lt;/strong&gt;, &lt;code&gt;react-render-optimization&lt;/code&gt; across 27. This&lt;br&gt;
is a habit spread through a codebase, not one defect multiplied.&lt;/p&gt;

&lt;p&gt;The opposite check matters too. &lt;code&gt;no-inline-style&lt;/code&gt; fires 9 times in &lt;strong&gt;one&lt;/strong&gt; file&lt;br&gt;
— that row &lt;em&gt;is&lt;/em&gt; a single site, and reading it as a codebase-wide problem would&lt;br&gt;
be the same error in miniature. The column tells you which is which.&lt;/p&gt;
&lt;h2&gt;
  
  
  Six rules said nothing
&lt;/h2&gt;

&lt;p&gt;Six of fourteen produced zero: &lt;code&gt;required-attributes&lt;/code&gt;, &lt;code&gt;react-class-to-hooks&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;no-default-test-id&lt;/code&gt;, &lt;code&gt;no-is-prefix-prop&lt;/code&gt;, &lt;code&gt;no-kind-prop-discriminator&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;no-wrapper-sub-component&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A zero is not a verdict on the rule. It is a statement about your code — and&lt;br&gt;
distinguishing "this rule is dead" from "this rule is waiting" takes seconds,&lt;br&gt;
because every one of them ships invalid-case fixtures that prove it can fire.&lt;br&gt;
All six have them. They are waiting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run it on yours
&lt;/h2&gt;

&lt;p&gt;The measurement is one &lt;code&gt;ESLint&lt;/code&gt; instance and a counter, and two things about it&lt;br&gt;
are load-bearing:&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;eslint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ESLint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;overrideConfigFile&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="na"&gt;overrideConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-features&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// else every file is "outside of base path"&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`find &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -name '*.tsx'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// a glob returned zero here&lt;/span&gt;
&lt;span class="k"&gt;for &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;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eslint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lintFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&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;fatal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruleId&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;fatal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FATAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// never skip these&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first run reported &lt;strong&gt;0 findings on 100 files&lt;/strong&gt; and all fourteen rules&lt;br&gt;
zero-yield. It was wrong in the most comfortable direction. &lt;code&gt;cwd&lt;/code&gt; defaulted to&lt;br&gt;
the plugin repo, so ESLint ignored every file as outside its base path — and&lt;br&gt;
the loop discarded any message with a null &lt;code&gt;ruleId&lt;/code&gt;, which is exactly where&lt;br&gt;
that explanation was written.&lt;/p&gt;

&lt;p&gt;What caught it was a canary: a four-line component with an inline arrow and an&lt;br&gt;
inline style, which &lt;em&gt;must&lt;/em&gt; trip at least two rules. It reported zero. Nothing&lt;br&gt;
from that harness was believed until the canary reported six.&lt;/p&gt;

&lt;p&gt;Before you trust a lint number, make the tool fail on purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; eslint-plugin-react-features
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-react-features?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;eslint-plugin-react-features&lt;/a&gt; is part of the &lt;a href="https://ofriperetz.dev/go/r/8v7joo8tnv?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;Interlace ESLint ecosystem&lt;/a&gt;. Source on &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · Follow: &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=rule-yield-distribution-own-plugin" rel="noopener noreferrer"&gt;Dev.to/ofri-peretz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What does your top rule's file count look like — one habit, or one file?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>eslint</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rename the Variable, Change the Security Verdict</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:36:46 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/rename-the-variable-change-the-security-verdict-5a25</link>
      <guid>https://dev.to/ofri-peretz/rename-the-variable-change-the-security-verdict-5a25</guid>
      <description>&lt;p&gt;Four lines, run through my own XPath injection rule today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;//user[@id='&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// silent&lt;/span&gt;
&lt;span class="nx"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;//user[@id='&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// FIRES&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;QueryValidateSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QueryInputSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// FIRES&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SchemaA&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SchemaB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// silent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two are the same vulnerability. The last two are the same statement.&lt;br&gt;
The only thing that changed is what the variables are called.&lt;/p&gt;
&lt;h2&gt;
  
  
  The heuristic
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;no-xpath-injection&lt;/code&gt; has to decide whether a value is attacker-controlled. Here&lt;br&gt;
is how it decides:&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;varName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;req&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;params&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;keyword&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;varName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// tainted&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole test. Not where the value came from — what it is spelled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both errors, one cause
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The false positive.&lt;/strong&gt; &lt;code&gt;QueryInputSchema&lt;/code&gt; lowercases to &lt;code&gt;queryinputschema&lt;/code&gt;,&lt;br&gt;
which contains &lt;code&gt;query&lt;/code&gt; and also &lt;code&gt;input&lt;/code&gt;. Tainted. There is no XPath call in&lt;br&gt;
that statement at all — it is a Zod schema being re-exported. The rule reports&lt;br&gt;
an injection risk on an assignment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The false negative.&lt;/strong&gt; &lt;code&gt;id&lt;/code&gt; contains none of the seven substrings, so the&lt;br&gt;
value is not tainted, so a concatenation flowing into &lt;code&gt;xpath.select&lt;/code&gt; — a sink&lt;br&gt;
this rule already recognises, it is in the default &lt;code&gt;xpathFunctions&lt;/code&gt; list — is&lt;br&gt;
not reported. Textbook XPath injection, silently accepted.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;//user[...]&lt;/code&gt; string does contain &lt;code&gt;user&lt;/code&gt;. It does not matter: the check&lt;br&gt;
runs on the identifier, never on the literal.&lt;/p&gt;

&lt;p&gt;One predicate, opposite failures. That is what makes this a single defect&lt;br&gt;
rather than two bugs that happen to share a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worse than being wrong
&lt;/h2&gt;

&lt;p&gt;A rule with a 70% false-positive rate is annoying and legible — you read the&lt;br&gt;
findings, you see the noise, you tune it or drop it.&lt;/p&gt;

&lt;p&gt;This is not that. This rule is &lt;strong&gt;right about the shape of the problem&lt;/strong&gt; and&lt;br&gt;
wrong about the input to it, which means its errors correlate with naming&lt;br&gt;
conventions rather than with code. A codebase that says &lt;code&gt;userInput&lt;/code&gt; gets&lt;br&gt;
flagged everywhere. A codebase that says &lt;code&gt;val&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;raw&lt;/code&gt; or &lt;code&gt;id&lt;/code&gt; gets a clean&lt;br&gt;
report and the same vulnerabilities.&lt;/p&gt;

&lt;p&gt;Two teams, identical risk, opposite verdicts — and neither has any way to tell&lt;br&gt;
from the output which one they are.&lt;/p&gt;

&lt;p&gt;It also fails toward silence in exactly the case that matters. Short, neutral&lt;br&gt;
identifiers are what you find in the compact, clever code most likely to be&lt;br&gt;
doing something sharp with strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it should read instead
&lt;/h2&gt;

&lt;p&gt;The value's origin, not its spelling. Did it come from a request handler&lt;br&gt;
parameter, a &lt;code&gt;process.argv&lt;/code&gt;, a &lt;code&gt;req.*&lt;/code&gt; member, a function boundary the rule&lt;br&gt;
cannot see past? That is&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/taint-vs-heuristic-detection?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;taint analysis&lt;/a&gt;,&lt;br&gt;
and it is more expensive than a substring match — which is precisely why the&lt;br&gt;
substring match was written.&lt;/p&gt;

&lt;p&gt;The honest framing is that this rule is a heuristic wearing a taint rule's&lt;br&gt;
messaging. The fix is not a longer keyword list. A longer list moves the&lt;br&gt;
boundary; it does not stop the verdict being a function of names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that finds these
&lt;/h2&gt;

&lt;p&gt;Take any rule you rely on. Write the vulnerable case. Then &lt;strong&gt;rename every&lt;br&gt;
identifier in it to a single letter&lt;/strong&gt; and run it again.&lt;/p&gt;

&lt;p&gt;If the verdict changes, the rule is reading names. You now know something about&lt;br&gt;
your tooling that no amount of reading its findings would have told you —&lt;br&gt;
because a finding you never get is indistinguishable from a clean file, and&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/precision-recall-f1-for-static-analysis?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;precision and recall measured on a corpus that shares your naming conventions&lt;br&gt;
will both look fine&lt;/a&gt;.&lt;br&gt;
It is the same reason &lt;a href="https://ofriperetz.dev/go/what-ground-truth-caught-that-unit-tests-missed?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;a labelled fixture set catches what unit tests&lt;br&gt;
miss&lt;/a&gt;:&lt;br&gt;
the tests assert the rule's behaviour on the names its author chose.&lt;/p&gt;

&lt;p&gt;This is a known class in our own tracker, with a lint check that gates new&lt;br&gt;
occurrences and a documented list of the ones still outstanding. This rule is&lt;br&gt;
on that list. Measured against the workspace build of&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-secure-coding?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;&lt;code&gt;eslint-plugin-secure-coding&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
today — if you are on the published release, re-run before assuming.&lt;/p&gt;

&lt;p&gt;The rule is&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/r/15ycy9y2rx5?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;&lt;code&gt;no-xpath-injection&lt;/code&gt;&lt;/a&gt;;&lt;br&gt;
the predicate is in&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/r/1g2thii15zm?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;its source&lt;/a&gt;.&lt;br&gt;
Both are open, which is the only reason this article can be specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;⭐ Star the repo if you would rather find your linter's blind spots in an article than in an incident.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-secure-coding?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;eslint-plugin-secure-coding&lt;/a&gt; is part of the &lt;a href="https://ofriperetz.dev/go/r/8v7joo8tnv?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;Interlace ESLint ecosystem&lt;/a&gt;. Source on &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · Follow: &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=rule-that-reads-variable-names" rel="noopener noreferrer"&gt;Dev.to/ofri-peretz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rename the variables in your worst file to single letters. Does your linter still find the bug?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>eslint</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Six Rules Found Nothing. Only Five of Them Were Waiting.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:28:49 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/six-rules-found-nothing-only-five-of-them-were-waiting-2h1a</link>
      <guid>https://dev.to/ofri-peretz/six-rules-found-nothing-only-five-of-them-were-waiting-2h1a</guid>
      <description>&lt;p&gt;I measured fourteen lint rules against a real codebase. Six found nothing.&lt;/p&gt;

&lt;p&gt;The tempting move is to switch those six off. The report says zero, zero looks&lt;br&gt;
like dead weight, and a shorter config is easier to defend. That instinct is&lt;br&gt;
wrong at least five times out of six, and here is the ten-second check that&lt;br&gt;
tells you which.&lt;/p&gt;
&lt;h2&gt;
  
  
  A zero is three different facts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dead.&lt;/strong&gt; The rule cannot fire. A regression, a bad selector, an AST assumption&lt;br&gt;
that stopped being true. It will report zero on every codebase forever and&lt;br&gt;
nobody will notice, because zero is what a working rule looks like too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting.&lt;/strong&gt; The rule can fire; your code does not contain the pattern. This is&lt;br&gt;
the correct output and the rule is earning its keep as a ratchet — silent now,&lt;br&gt;
loud the day someone writes the thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unconfigured.&lt;/strong&gt; The rule can fire, but not until you give it options. Silent&lt;br&gt;
on every codebase, including the ones full of exactly what it was built to&lt;br&gt;
catch.&lt;/p&gt;

&lt;p&gt;The report renders all three as &lt;code&gt;0&lt;/code&gt;. The rule list does not distinguish them&lt;br&gt;
either.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ask the rule to fire
&lt;/h2&gt;

&lt;p&gt;You do not have to reason about which case you are in. Every rule worth using&lt;br&gt;
ships &lt;code&gt;invalid&lt;/code&gt; test fixtures — the smallest piece of code its author swears it&lt;br&gt;
must flag. Take one and lint it:&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;eslint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ESLint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;overrideConfigFile&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="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                   &lt;span class="c1"&gt;// else: "outside of base path"&lt;/span&gt;
  &lt;span class="na"&gt;overrideConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;languageOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tsParser&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;       &lt;span class="c1"&gt;// @typescript-eslint/parser&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-features&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-features/no-is-prefix-prop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eslint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lintText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interface Props { isLooped: boolean; }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/t.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser line is not optional. Most of the fixtures below are &lt;code&gt;interface&lt;/code&gt;&lt;br&gt;
declarations, and the default parser cannot read them — you get a fatal parse&lt;br&gt;
error where you expected the rule's verdict, and a message whose &lt;code&gt;ruleId&lt;/code&gt; is&lt;br&gt;
&lt;code&gt;null&lt;/code&gt;. Print those; never skip them.&lt;/p&gt;

&lt;p&gt;Fires: the rule works, your code is clean, keep it. Silent: you have learned&lt;br&gt;
something the report could not tell you.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;p&gt;Five of the six fired immediately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;th&gt;fixture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;react-class-to-hooks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;class MyComponent extends Component { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-default-test-id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;function Card({ "data-testid": d = "card" })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-is-prefix-prop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface Props { isLooped: boolean; }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-kind-prop-discriminator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`interface Props { type: "checkbox" \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;no-wrapper-sub-component&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;function MyButton(props) { &amp;lt;Button {...props} /&amp;gt; }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Waiting, all five. The codebase genuinely has none of it.&lt;/p&gt;

&lt;p&gt;The sixth did not. &lt;code&gt;required-attributes&lt;/code&gt; stayed silent on &lt;code&gt;&amp;lt;Button /&amp;gt;&lt;/code&gt; — its&lt;br&gt;
own fixture, at &lt;code&gt;error&lt;/code&gt;, with nothing else enabled.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one that could not fire
&lt;/h2&gt;

&lt;p&gt;Reading its test file explains it:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;Button /&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;   &lt;span class="c1"&gt;// ← the part that matters&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule takes its attribute list from configuration. Without options there is&lt;br&gt;
nothing it has been asked to require, so it correctly does nothing. Supply the&lt;br&gt;
options and the same fixture reports one error.&lt;/p&gt;

&lt;p&gt;That is not a bug. It is worse than a bug in one specific way: &lt;strong&gt;a bug gets&lt;br&gt;
noticed.&lt;/strong&gt; This rule sits in a config, at &lt;code&gt;error&lt;/code&gt;, looking enabled, and reports&lt;br&gt;
zero on every codebase in the world — including one that violates it on every&lt;br&gt;
line. The report cannot distinguish that from success.&lt;/p&gt;

&lt;p&gt;Five waiting, one unconfigured, none dead. I would have guessed a different&lt;br&gt;
split, and switching off "the six that found nothing" would have thrown away&lt;br&gt;
five working ratchets and left the one real problem in place.&lt;/p&gt;
&lt;h2&gt;
  
  
  The habit
&lt;/h2&gt;

&lt;p&gt;Before you delete a rule for being quiet, make it speak. If it will not, you&lt;br&gt;
have found something more interesting than a finding.&lt;/p&gt;

&lt;p&gt;The same instinct applies one level up: a lint run that reports zero is not&lt;br&gt;
evidence that a tool ran. A harness of mine once reported 0 findings across 100&lt;br&gt;
files because every file was silently ignored — a perfectly believable number,&lt;br&gt;
and completely false. Zero is the one output that looks identical whether&lt;br&gt;
everything worked or nothing did, which is why&lt;br&gt;
&lt;a href="https://ofriperetz.dev/go/precision-recall-f1-for-static-analysis?utm_source=devto&amp;amp;from=zero-yield-rules-dead-or-waiting" rel="noopener noreferrer"&gt;a count means nothing without the measurement behind it&lt;/a&gt;&lt;br&gt;
and why &lt;a href="https://ofriperetz.dev/go/aggregate-benchmarks-lie-heres-what-700-ai-functions-look-like-by-security-domain?utm_source=devto&amp;amp;from=zero-yield-rules-dead-or-waiting" rel="noopener noreferrer"&gt;an aggregate hides the distribution that produced it&lt;/a&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 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; eslint-plugin-react-features
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-react-features?utm_source=devto&amp;amp;from=zero-yield-rules-dead-or-waiting" rel="noopener noreferrer"&gt;eslint-plugin-react-features&lt;/a&gt; is part of the &lt;a href="https://ofriperetz.dev/go/r/8v7joo8tnv?utm_source=devto&amp;amp;from=zero-yield-rules-dead-or-waiting" rel="noopener noreferrer"&gt;Interlace ESLint ecosystem&lt;/a&gt;. Source on &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=zero-yield-rules-dead-or-waiting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · Follow: &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=zero-yield-rules-dead-or-waiting" rel="noopener noreferrer"&gt;Dev.to/ofri-peretz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which rule in your config has never once fired — and have you ever asked it to?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>eslint</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>13 Repos Keep a Credential in Web Storage. Seven Are Governments.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Sun, 11 Oct 2026 08:26:36 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/13-repos-keep-a-credential-in-web-storage-seven-are-governments-20be</link>
      <guid>https://dev.to/ofri-peretz/13-repos-keep-a-credential-in-web-storage-seven-are-governments-20be</guid>
      <description>&lt;p&gt;"Never put a JWT in localStorage" is the most repeated piece of frontend security advice and the least measured. So I measured it.&lt;/p&gt;

&lt;p&gt;258,117 source files. &lt;strong&gt;1,423 touch web storage&lt;/strong&gt; — that is the denominator, found by reading source text before any rule ran. They live in 189 public repositories.&lt;/p&gt;

&lt;p&gt;Thirteen of those repositories put a credential in storage — eleven of them a bearer token. &lt;strong&gt;Seven of the thirteen are government digital services.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Helsinki · &lt;code&gt;kukkuu-admin&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localStorage.setItem(API_TOKEN, user.access_token)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UK ONS · &lt;code&gt;eq-author-app&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localStorage.setItem("accessToken", user.ra)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US GSA · &lt;code&gt;srt-ui&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localStorage.setItem('token', data.token)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canada · &lt;code&gt;cds-snc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localStorage.setItem("token", response.access_token)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France · &lt;code&gt;betagouv&lt;/code&gt; ×2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xsrfToken&lt;/code&gt;, &lt;code&gt;NATIVE_TOKEN_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;British Columbia · &lt;code&gt;sso-requests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sessionStorage.setItem(TOKEN_SESSION, …)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The non-government six are &lt;code&gt;cloudflare-os&lt;/code&gt;, &lt;code&gt;headlamp&lt;/code&gt;, &lt;code&gt;mozilla_fxa&lt;/code&gt;, &lt;code&gt;solid-playground&lt;/code&gt;, &lt;code&gt;wix_skills&lt;/code&gt; and a Twilio app that stores a passcode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rate is the boring half
&lt;/h2&gt;

&lt;p&gt;Thirteen out of 189 is about 7%. If you came expecting an epidemic, there isn't one — most code touching web storage keeps credentials out of it.&lt;/p&gt;

&lt;p&gt;What makes seven percent interesting is &lt;em&gt;where&lt;/em&gt; it lands. Public-sector frontends are built by rotating contractors against procurement deadlines, and they inherit whatever the OIDC tutorial did. The tutorial put the token in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a mechanism, not an accusation. Two of the seven — Helsinki and the UK ONS — store a &lt;strong&gt;refresh&lt;/strong&gt; token beside the access token, and that is the part that matters: an access token expires in minutes, a refresh token is a long-lived credential sitting where any injected script can read it synchronously. That is the whole argument: web storage is &lt;a href="https://ofriperetz.dev/go/client-storage-trust-boundary?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;a trust boundary you share with every script on the page&lt;/a&gt;, and one &lt;a href="https://ofriperetz.dev/go/dom-sink-taxonomy?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;DOM sink&lt;/a&gt; is all it takes to cross it. The six non-government hits are developer tools, playgrounds and internal consoles, where the threat model is genuinely different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two of the thirteen are not the mistake you think
&lt;/h2&gt;

&lt;p&gt;Reading the code changes the verdict twice.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bcgov_sso-requests&lt;/code&gt; uses &lt;code&gt;sessionStorage&lt;/code&gt;, not &lt;code&gt;localStorage&lt;/code&gt; — same XSS exposure, but the token dies with the tab instead of persisting. &lt;code&gt;betagouv_sante-psy&lt;/code&gt; stores an &lt;strong&gt;XSRF&lt;/strong&gt; token, which is a different risk class entirely: it is meant to be readable by your own page, and it is not a bearer credential. That is why the count above says &lt;em&gt;credential&lt;/em&gt; and not &lt;em&gt;auth token&lt;/em&gt;: eleven of the thirteen are bearer tokens, and the two that are not are this XSRF token and a Twilio app passcode. &lt;code&gt;bcgov&lt;/code&gt; is a bearer token — it is on this list for the store it chose, not for what it stores.&lt;/p&gt;

&lt;p&gt;Counting them all as "a JWT in localStorage" would have been technically defensible and substantively wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  My own rule is right 13 times out of 19
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;no-jwt-in-storage&lt;/code&gt; flagged 19 repositories. I read all 39 files. Six repos are false positives, and all six fail the same way — the rule matched an identifier &lt;em&gt;name&lt;/em&gt; rather than proving a credential:&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="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sessionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// a demo session id&lt;/span&gt;
&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app_authServer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// a server URL&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AUTH_STATUS_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// the string "success"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also flagged: a co-browse session descriptor, a telemetry session, a sandbox id, and an example &lt;code&gt;user&lt;/code&gt; object. That is &lt;strong&gt;68% precision&lt;/strong&gt; on real source.&lt;/p&gt;

&lt;p&gt;I am reporting it because the thirteen are only believable if you know what the instrument does when it is wrong. A field study that publishes its hit rate and not its miss rate is a marketing page.&lt;/p&gt;

&lt;p&gt;One more, worth its own line: &lt;code&gt;no-cookie-auth-tokens&lt;/code&gt; fired &lt;strong&gt;zero&lt;/strong&gt; times across all 1,423 files. A rule that never fires in a corpus this size is telling you about the rule, not the corpus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint.config.mjs — after `npm i -D eslint-plugin-browser-security`&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-browser-security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/*.{ts,tsx,js,jsx}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browser-security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browser-security/no-jwt-in-storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect to read the hits rather than trust them — &lt;a href="https://ofriperetz.dev/go/ground-truth-in-security-testing?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;ground truth is the hard half&lt;/a&gt; of any audit like this. The corpus here is an adoption scan, not a random draw from GitHub, so "7 of 13" describes what I looked at. Point it at a different corpus and the mix will move.&lt;/p&gt;




&lt;p&gt;More of these — &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;follow on dev.to&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Open your own repo and grep for &lt;code&gt;setItem&lt;/code&gt;. Is the thing you find a bearer token, or did you just name a variable badly?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/ground-truth-in-security-testing?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;Ground Truth in Security Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/cwe-taxonomy-explained?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;The CWE Taxonomy, Explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-browser-security?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=token-in-localstorage-field-study" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
      <category>eslint</category>
    </item>
    <item>
      <title>I Linted 14 Public AI SDK Repos. 12 Ship a Call With No Token Ceiling.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Wed, 07 Oct 2026 07:49:42 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/i-linted-14-public-ai-sdk-repos-12-ship-a-call-with-no-token-ceiling-2349</link>
      <guid>https://dev.to/ofri-peretz/i-linted-14-public-ai-sdk-repos-12-ship-a-call-with-no-token-ceiling-2349</guid>
      <description>&lt;p&gt;I argued last week that &lt;a href="https://ofriperetz.dev/go/agent-resource-bounds?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;an AI SDK call with no bounds is three CWEs in one missing config object&lt;/a&gt;. Fair question back: does anyone actually ship that?&lt;/p&gt;

&lt;p&gt;So I linted for it. 20,004 source files across 14 public repositories, 474 of which import the Vercel AI SDK, &lt;strong&gt;116 of which contain a real generation call&lt;/strong&gt;. That 116 is the denominator; everything below is a fraction of it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bound&lt;/th&gt;
&lt;th&gt;Files missing it&lt;/th&gt;
&lt;th&gt;Repositories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;output cap (&lt;code&gt;maxOutputTokens&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;103 / 116&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12 of 14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;request timeout&lt;/td&gt;
&lt;td&gt;50 / 116&lt;/td&gt;
&lt;td&gt;9 of 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;abort signal on a stream&lt;/td&gt;
&lt;td&gt;44 / 116&lt;/td&gt;
&lt;td&gt;7 of 14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Read the repo column, not the percentage
&lt;/h2&gt;

&lt;p&gt;88.8% is the number that would travel, and it is the one I trust least. One repository, &lt;code&gt;cloudflare_agents&lt;/code&gt;, contributes 52 of those 103 files — just over half. Percentages of files measure how a codebase is split into modules as much as how it was written.&lt;/p&gt;

&lt;p&gt;The repo count is the robust statistic. Twelve of fourteen survives deleting any single project from the corpus. The percentage does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five of the fourteen are Vercel's own
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ai-elements&lt;/code&gt;, &lt;code&gt;vercel_chat&lt;/code&gt;, &lt;code&gt;vercel_examples&lt;/code&gt;, &lt;code&gt;vercel_streamdown&lt;/code&gt; and &lt;code&gt;vercel-labs_agent-eval&lt;/code&gt; are the SDK vendor's repositories, and they are in the findings.&lt;/p&gt;

&lt;p&gt;That is not a gotcha — it is the mechanism. Example code is deliberately minimal; a ceiling is noise in a snippet teaching you &lt;code&gt;streamText&lt;/code&gt;. But the minimal shape is the shape that gets copied, and it is missing the same parameter in the reader's production handler, where it is no longer noise. The omission is correct upstream and wrong downstream, which is exactly why nothing catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an unbounded call looks like in the wild
&lt;/h2&gt;

&lt;p&gt;Not exotic. This is the common form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one worth showing is subtler — &lt;code&gt;302ai_302-AI-Studio&lt;/code&gt; builds its options indirectly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;streamTextOptions&lt;/span&gt; &lt;span class="o"&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;baseConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&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;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;generateText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamTextOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I assumed this was a false positive: a rule cannot see into an object built elsewhere. It isn't. &lt;code&gt;baseConfig&lt;/code&gt; sets &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;providerOptions&lt;/code&gt; and &lt;code&gt;tools&lt;/code&gt; — and the whole 1,500-line file contains exactly one occurrence of &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;abortSignal&lt;/code&gt; or &lt;code&gt;maxOutputTokens&lt;/code&gt; between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bound I threw out
&lt;/h2&gt;

&lt;p&gt;The audit measured a fourth — step count — and I am not reporting it. Two reasons, either sufficient. The SDK already defaults &lt;code&gt;stopWhen&lt;/code&gt; to &lt;code&gt;stepCountIs(1)&lt;/code&gt;, so the loop is bounded unless you raised it yourself; a rule firing there reports a non-defect. And spot-checking four of its flagged files found one that sets both &lt;code&gt;stopWhen&lt;/code&gt; and &lt;code&gt;stepCountIs&lt;/code&gt; and was flagged anyway.&lt;/p&gt;

&lt;p&gt;A rule that fires on a safe default doesn't make the number bigger. It makes the other three unbelievable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on yours
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint.config.mjs — after `npm i -D eslint-plugin-vercel-ai-security`&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-vercel-ai-security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/*.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vercel-ai-security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vercel-ai-security/require-max-tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest limits, because n = 14 is small — and a corpus you assembled yourself is &lt;a href="https://ofriperetz.dev/go/ground-truth-in-security-testing?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;the hard half of any audit&lt;/a&gt;. This is an adoption-scan convenience sample, not a random draw from npm, and two directories in it turned out to be the same project cloned twice — caught by content fingerprint, since every checkout reported the same &lt;code&gt;git remote&lt;/code&gt;. The claim is that this is what the SDK's own ecosystem looks like. It is not a population rate.&lt;/p&gt;

&lt;p&gt;The prior sweep I tried to reuse recorded findings but no denominator and no repo list, so it could not answer this at all. A count without its denominator isn't a small result. It's not a result.&lt;/p&gt;




&lt;p&gt;More of these — &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;follow on dev.to&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Check the handler you shipped this week: is there a ceiling on it, or did the example not have one either?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/agent-resource-bounds?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;Your LLM Call Has No Ceiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/cwe-taxonomy-explained?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;The CWE Taxonomy, Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/getting-started-eslint-plugin-vercel-ai-security?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;Getting Started with eslint-plugin-vercel-ai-security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-vercel-ai-security?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=ai-agent-unbounded-loop-audit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>security</category>
      <category>eslint</category>
    </item>
    <item>
      <title>Your LLM Call Has No Ceiling. TypeScript Is Fine With It. Three CWEs Aren't.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:10:34 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/your-llm-call-has-no-ceiling-typescript-is-fine-with-it-three-cwes-arent-dkd</link>
      <guid>https://dev.to/ofri-peretz/your-llm-call-has-no-ceiling-typescript-is-fine-with-it-three-cwes-arent-dkd</guid>
      <description>&lt;p&gt;&lt;code&gt;generateText&lt;/code&gt; with a model and a prompt and nothing else is a valid call. TypeScript is satisfied. The type system has no opinion about how many tokens come back, how long it takes, or who pays for the answer nobody reads.&lt;/p&gt;

&lt;p&gt;Three rules in &lt;code&gt;eslint-plugin-vercel-ai-security&lt;/code&gt; fire on a call written that way, and they map to three &lt;em&gt;different&lt;/em&gt; CWEs — 770, 400 and 404. (A CWE is a label, not a verdict — &lt;a href="https://ofriperetz.dev/go/cwe-taxonomy-explained?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;what the taxonomy actually claims&lt;/a&gt; — and counting CWEs is a &lt;a href="https://ofriperetz.dev/go/proxy-metrics?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;proxy metric&lt;/a&gt;.) Same missing config object. Three separate ways to lose.&lt;/p&gt;

&lt;p&gt;A note on the bound people expect: step count. The SDK defaults &lt;code&gt;stopWhen&lt;/code&gt; to &lt;code&gt;stepCountIs(1)&lt;/code&gt;, so a tool-calling loop does not run away on its own — that hole gets opened deliberately, by raising the ceiling, not by forgetting it. The three below are genuinely unbounded when you say nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The output with no cap — CWE-770
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// no maxOutputTokens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it gets written:&lt;/strong&gt; the parameter is optional, and the happy path never needs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it survives review:&lt;/strong&gt; output length reads as a &lt;em&gt;quality&lt;/em&gt; knob, not a resource bound. But this is billed per token. An unbounded output is an unbounded invoice, and nothing in the diff looks like money.&lt;/p&gt;

&lt;p&gt;Fix: set &lt;code&gt;maxOutputTokens&lt;/code&gt; to the longest answer you would pay for — there is no correct number, only the difference between a ceiling and none. Note the rename: this was &lt;code&gt;maxTokens&lt;/code&gt; in v4. Guidance written against v4 still &lt;em&gt;reads&lt;/em&gt; correct in review and bounds nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The request that never returns — CWE-400
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// no timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it gets written:&lt;/strong&gt; &lt;code&gt;fetch&lt;/code&gt;-shaped APIs feel like they time out. This one doesn't — &lt;code&gt;timeout&lt;/code&gt; is optional and the SDK sets no default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it survives review:&lt;/strong&gt; staging latency is fine, so nobody asks what happens when the provider hangs instead of failing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;totalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&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;&lt;code&gt;timeout&lt;/code&gt; has been first-class since &lt;strong&gt;6.0.14&lt;/strong&gt; and also takes &lt;code&gt;stepMs&lt;/code&gt;, &lt;code&gt;chunkMs&lt;/code&gt;, &lt;code&gt;toolMs&lt;/code&gt;. Below that, hand-roll an &lt;code&gt;AbortController&lt;/code&gt;; the rule accepts either, but reach for the parameter first.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The stream nobody can cancel — CWE-404
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// no abortSignal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will file this under polish. Here is why that is wrong: the client is gone, and the server keeps generating — and billing — for a reader who will never see a token of it.&lt;/p&gt;

&lt;p&gt;Why &lt;em&gt;shutdown&lt;/em&gt; (CWE-404) rather than consumption (400)? The resource was acquired correctly and never released — the handle outlives the request that justified it. A release bug, not an acquisition bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// client disconnected&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abortSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;All three are one defect: &lt;strong&gt;an allocation with no ceiling.&lt;/strong&gt; Tokens, wall-clock, lifetime.&lt;/p&gt;

&lt;p&gt;I don't trust an allocation whose ceiling I can't see — the same instinct that keeps me out of a position whose downside I can't draw on one page.&lt;/p&gt;

&lt;p&gt;Classic resource-exhaustion review asks whether an attacker can make something loop forever. For an LLM call, the answer is worse — you don't need an attacker. A verbose model and one retry will do it, and the meter runs the entire time.&lt;/p&gt;

&lt;p&gt;This is OWASP LLM10, Unbounded Consumption — &lt;a href="https://ofriperetz.dev/go/100-owasp-llm-top-10-coverage-for-vercel-ai-sdk?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;the full top-10 mapping for this SDK&lt;/a&gt; covers the other nine. That list is a separate taxonomy from &lt;a href="https://ofriperetz.dev/go/owasp-top-10-explained?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;the web Top 10&lt;/a&gt; — LLM10 has no A-number. For governance rather than linting, see &lt;a href="https://ofriperetz.dev/go/r/hgb7extkuq?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;NIST AI RMF&lt;/a&gt;. That is why these are security rules, not style rules. It is the move &lt;a href="https://ofriperetz.dev/go/injection-beyond-sql?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;injection across nine interpreters&lt;/a&gt; makes too: CWE-770, 400 and 404 are the same sentence in three grammars.&lt;/p&gt;




&lt;h2&gt;
  
  
  The config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; eslint-plugin-vercel-ai-security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 18+. The peer range is ESLint 8 ∥ 9 ∥ 10, so both config formats work.&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;// eslint.config.mjs — ESLint 9 · 10&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;vercelAi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-vercel-ai-security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/*.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vercel-ai-security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vercelAi&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vercel-ai-security/require-max-tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vercel-ai-security/require-request-timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vercel-ai-security/require-abort-signal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.eslintrc.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ESLint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&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;"plugins"&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;"vercel-ai-security"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&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;"vercel-ai-security/require-max-tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vercel-ai-security/require-request-timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vercel-ai-security/require-abort-signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&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;On oxlint, add &lt;code&gt;"jsPlugins": ["eslint-plugin-vercel-ai-security/oxlint"]&lt;/code&gt; — alpha, not under semver.&lt;/p&gt;

&lt;p&gt;Rule docs: &lt;a href="https://ofriperetz.dev/go/r/12y5mkhgudu?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;require-max-tokens&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/r/10tceywee5m?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;require-request-timeout&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/r/19kzghbou99?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;require-abort-signal&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;More of these — &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;follow on dev.to&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Has an LLM call ever run longer in production than you expected — and what told you first, the logs or the bill?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/securing-ai-agents-in-the-vercel-ai-sdk?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;Securing AI Agents in the Vercel AI SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/getting-started-eslint-plugin-vercel-ai-security?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;Getting Started with eslint-plugin-vercel-ai-security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ofriperetz.dev/go/cwe-taxonomy-explained?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;The CWE Taxonomy, Explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/npm/eslint-plugin-vercel-ai-security?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=agent-resource-bounds" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>security</category>
      <category>eslint</category>
    </item>
    <item>
      <title>Everyone Greps for SQL Injection. Nobody Greps for the Other Eight.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Sun, 27 Sep 2026 19:27:57 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/everyone-greps-for-sql-injection-nobody-greps-for-the-other-eight-4pjm</link>
      <guid>https://dev.to/ofri-peretz/everyone-greps-for-sql-injection-nobody-greps-for-the-other-eight-4pjm</guid>
      <description>&lt;p&gt;Ask a developer to find injection in a codebase and they grep for string concatenation near a database call. They will find the SQL. They will walk past eight other interpreters on the way.&lt;/p&gt;

&lt;p&gt;An interpreter nobody remembers is still an interpreter. Build a string, hand it to something that parses it, and you have the same bug — whether or not that parser speaks SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape, not the syntax
&lt;/h2&gt;

&lt;p&gt;The taxonomy already knew this. &lt;a href="https://ofriperetz.dev/go/r/oifoecdzhl?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-943&lt;/a&gt; is "Improper Neutralization of Special Elements in Data Query Logic" — the parent class that SQL injection is merely the famous child of. Its siblings are the same defect aimed at different parsers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Injection&lt;/th&gt;
&lt;th&gt;CWE&lt;/th&gt;
&lt;th&gt;The interpreter you forgot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GraphQL&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/oifoecdzhl?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-943&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the query resolver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LDAP&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/53boeup8fm?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-90&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the directory filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XPath&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/q81s0sg350?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-643&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the XML path engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XXE&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/z83zibdpu3?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-611&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the XML entity resolver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/7s1i4oqkq7?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-94&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the template compiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directive&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/p8zyd4svrq?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-96&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the server-side include&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format string&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/up96k5xh8m?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-134&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the format specifier parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object / prototype&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ofriperetz.dev/go/r/1kazi0cikub?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-915&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the JavaScript engine itself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Be precise about what that parent covers: the honest version is narrower than the tidy one. MITRE lists exactly four children under CWE-943 — check the Research Concepts view (View-1000) on that page — and all four are query languages: &lt;a href="https://ofriperetz.dev/go/sql-injection-node-postgres-pattern?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;SQL&lt;/a&gt; (CWE-89), LDAP (CWE-90), XPath (CWE-643) and XQuery (&lt;a href="https://ofriperetz.dev/go/r/2g332hx5v93?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-652&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Everything else in the table is &lt;em&gt;commonly mapped&lt;/em&gt; there, not formally filed under it. GraphQL has no dedicated CWE at all; neither do the &lt;a href="https://ofriperetz.dev/go/getting-started-eslint-plugin-mongodb-security?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;NoSQL operator injections&lt;/a&gt;. And XXE, format string and prototype pollution are not siblings — they are separate defects that happen to share a blind spot, not a parent.&lt;/p&gt;

&lt;p&gt;Eight interpreters plus SQL, in two distinct shapes — data-query injection (the CWE-943 family) and code/context injection (everything else in the table). SQL got the name people recognise. The parser inside your LDAP filter did not, and that is the entire reason it goes unaudited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three that fail in ways SQL does not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;XXE needs no injected operator at all.&lt;/strong&gt; Parse attacker-supplied XML with entity resolution enabled and the parser fetches local files on their behalf. There is no quote to escape, no operator to smuggle. The payload is the document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LDAP fails silently.&lt;/strong&gt; &lt;code&gt;(uid=&lt;/code&gt; plus a string is a filter, and a stray &lt;code&gt;)&lt;/code&gt; turns an authentication check into a wildcard matching every entry in the directory. No error, no stack trace — a login that simply succeeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template injection escalates further than SQL.&lt;/strong&gt; &lt;a href="https://ofriperetz.dev/go/r/22tmnjr54l4?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;PortSwigger's server-side template injection research&lt;/a&gt; showed a template engine will hand over remote code execution as readily as a database hands over rows. A templating call is not a formatting convenience; it is an evaluator.&lt;/p&gt;

&lt;p&gt;GraphQL is the odd one, because injection is only half of it. A query the client shapes is also a cost problem — the caller, not you, decides how many nested resolvers run. That is resource exhaustion (&lt;a href="https://ofriperetz.dev/go/r/1jwfv9lhpa4?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;CWE-400&lt;/a&gt;) wearing a query's clothes, and why a depth limit is a security control, not a performance tweak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why grep finds SQL and misses the rest
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/r/g6zs5f0wnx?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt; folds this whole family into A03. Most tooling does not, because a pattern matcher looks for a &lt;em&gt;sink it has been taught&lt;/em&gt; — a &lt;code&gt;query(&lt;/code&gt;, an &lt;code&gt;execute(&lt;/code&gt;. Nobody teaches it &lt;code&gt;XPathEvaluator&lt;/code&gt;, &lt;code&gt;libxmljs&lt;/code&gt;, an LDAP &lt;code&gt;filter:&lt;/code&gt;, a &lt;code&gt;compile(&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is also the honest limit of pattern matching. Seeing a &lt;code&gt;+&lt;/code&gt; flowing into a sink does not prove the value came from a request, and it goes quiet when the concatenation happens one helper away. That gap between "looks dangerous" and "is reachable" is the whole subject of &lt;a href="https://ofriperetz.dev/go/taint-vs-heuristic-detection?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;taint analysis&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;p&gt;Do not start from a checklist. Grep your own code for the &lt;em&gt;constructors&lt;/em&gt;: &lt;code&gt;XPathEvaluator&lt;/code&gt;, &lt;code&gt;libxmljs&lt;/code&gt;, an LDAP &lt;code&gt;filter:&lt;/code&gt;, a template &lt;code&gt;compile(&lt;/code&gt;, any &lt;code&gt;%s&lt;/code&gt; you assemble by hand. For each, ask one question — can user input reach this string?&lt;/p&gt;

&lt;p&gt;Grep is the triage list, not the verdict: it tells you &lt;em&gt;where to look&lt;/em&gt;, taint tells you &lt;em&gt;whether it reaches&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If the answer is "probably not, but I would have to check", that is exactly the answer most people give about SQL right before they find the other eight.&lt;/p&gt;

&lt;p&gt;Which interpreter is live in your stack right now that you have never grepped for? My money is on GraphQL.&lt;/p&gt;

&lt;p&gt;More on how these classes get named and ranked: &lt;a href="https://ofriperetz.dev/go/cwe-taxonomy-explained?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;the CWE taxonomy&lt;/a&gt; and &lt;a href="https://ofriperetz.dev/go/owasp-top-10-explained?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;the OWASP Top 10&lt;/a&gt;. I write these at &lt;a href="https://ofriperetz.dev/go/r/2cvyogppkrw?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;dev.to/ofri-peretz&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/eslint?utm_source=devto&amp;amp;from=injection-beyond-sql" rel="noopener noreferrer"&gt;⭐ Star the repo if you would rather your linter knew about the other eight interpreters too.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>One Import Moves a commander CLI to burgee. --json Still Says null.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:28:33 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/one-import-moves-a-commander-cli-to-burgee-json-still-says-null-6h8</link>
      <guid>https://dev.to/ofri-peretz/one-import-moves-a-commander-cli-to-burgee-json-still-says-null-6h8</guid>
      <description>&lt;p&gt;I changed one line in a commander CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import { Command } from "commander";
&lt;/span&gt;&lt;span class="gi"&gt;+ import { Command } from "burgee/commander";
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Help, output, errors and exit codes stayed byte-identical on seven invocations. The same file also answers &lt;code&gt;--schema&lt;/code&gt;, &lt;code&gt;--mcp&lt;/code&gt; and &lt;code&gt;completion &amp;lt;shell&amp;gt;&lt;/code&gt;. And &lt;code&gt;--json&lt;/code&gt; answers &lt;code&gt;"data":null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One import describes the CLI to an agent; answering takes a &lt;code&gt;return&lt;/code&gt;. I &lt;a href="https://ofriperetz.dev/go/i-built-what-i-benchmark-heres-how-i-try-not-to-cheat?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;maintain burgee&lt;/a&gt;; everything below ran on &lt;code&gt;commander@15.0.0&lt;/code&gt; and &lt;code&gt;burgee@0.11.1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The program
&lt;/h2&gt;

&lt;p&gt;Plain commander, ESM, as &lt;code&gt;cli.commander.js&lt;/code&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="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&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="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commander&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;program&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;program&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lines&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Count and rank the lines in a text file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;program&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;count the lines in a file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;file&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file to read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--skip-blank&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ignore empty lines&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trimEnd&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skipBlank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; lines in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&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;program&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;longest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;print the longest lines in a file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;file&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file to read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-n, --top &amp;lt;count&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;how many lines to show&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trimEnd&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&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="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&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="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;for &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;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&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;line&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;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The swap
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;commander@15.0.0 burgee@0.11.1   &lt;span class="c"&gt;# or: yarn/pnpm/bun add&lt;/span&gt;
npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
&lt;span class="nb"&gt;cp &lt;/span&gt;cli.commander.js cli.js
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'alpha\n\nbravo charlie\ndelta echo foxtrot golf\n\nhotel\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sample.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four work, as does &lt;code&gt;require()&lt;/code&gt;. TypeScript's legacy &lt;code&gt;node&lt;/code&gt; resolver cannot see the subpath. After the import swap in &lt;code&gt;cli.js&lt;/code&gt;:&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;diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;node cli.commander.js &lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;node cli.js &lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;identical
&lt;span class="go"&gt;identical
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same for &lt;code&gt;count --help&lt;/code&gt;, both commands, a missing argument and a typo. Usage errors still exit &lt;code&gt;1&lt;/code&gt;; burgee's native exit &lt;code&gt;2&lt;/code&gt; for "rewrite the command" does not come with the swap. Once &lt;code&gt;commander&lt;/code&gt; leaves &lt;code&gt;dependencies&lt;/code&gt;, the declared Node range goes from &lt;code&gt;&amp;gt;=22.12.0&lt;/code&gt; to &lt;code&gt;^20.19.0 || &amp;gt;=22.13.0&lt;/code&gt;. The swapped file ran on Node 20 and 22; only 22.12.x drops out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What answers with no other edit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--schema&lt;/code&gt;&lt;/strong&gt;: the command tree as data, &lt;a href="https://ofriperetz.dev/go/securing-ai-agents-in-the-vercel-ai-sdk?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;a JSON Schema per command&lt;/a&gt;:&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;node cli.js &lt;span class="nt"&gt;--schema&lt;/span&gt; | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'.commands[0].inputSchema'&lt;/span&gt;
&lt;span class="go"&gt;{"type":"object","properties":{"file":{"type":"string","description":"file to read"},"skipBlank":{"type":"boolean","flag":"--skip-blank","description":"ignore empty lines"}},"required":["file"],"additionalProperties":false}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Errors under &lt;code&gt;--json&lt;/code&gt;&lt;/strong&gt;: typed, with the fix:&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;node cli.js count sample.txt &lt;span class="nt"&gt;--skp-blank&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="go"&gt;{"ok":false,"error":{"code":"commander.unknownOption","message":"unknown option '--skp-blank'","fix":"--skip-blank"}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://ofriperetz.dev/go/r/2fuz2t9t523?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; discovery&lt;/strong&gt;: every command becomes a tool, and an undeclared one says so instead of guessing a hint:&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/list"}'&lt;/span&gt; | node cli.js &lt;span class="nt"&gt;--mcp&lt;/span&gt; | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'.result.tools[] | {name, annotations}'&lt;/span&gt;
&lt;span class="go"&gt;{"name":"count","annotations":{"effects":"undeclared"}}
{"name":"longest","annotations":{"effects":"undeclared"}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.effects("read_only")&lt;/code&gt; on &lt;code&gt;count&lt;/code&gt; yields &lt;code&gt;readOnlyHint: true&lt;/code&gt;. That call is a one-way door: real commander throws a &lt;code&gt;TypeError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completions&lt;/strong&gt;: &lt;code&gt;completion &amp;lt;shell&amp;gt;&lt;/code&gt; for bash, zsh, fish, pwsh and fig, as static output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where one import stops
&lt;/h2&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;node cli.js count sample.txt &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="go"&gt;6 lines in sample.txt
{"ok":true,"data":null,"meta":{"provenance":{}}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;burgee's envelope wraps what an action &lt;em&gt;returns&lt;/em&gt;. commander discards return values, so commander programs rarely write one. Under &lt;code&gt;--mcp&lt;/code&gt;, that printed line lands on the JSON-RPC stream, where no client can parse it.&lt;/p&gt;

&lt;p&gt;The second edit, in &lt;code&gt;cli.answering.js&lt;/code&gt;: return the result, and keep prose off stdout when a machine asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+const machine = ["--json", "--mcp"].some((f) =&amp;gt; process.argv.includes(f));
+const say = (text) =&amp;gt; {
+  if (!machine) console.log(text);
+};
&lt;/span&gt; ...
&lt;span class="gd"&gt;-    console.log(`${lines.length} lines in ${file}`);
&lt;/span&gt;&lt;span class="gi"&gt;+    say(`${lines.length} lines in ${file}`);
+    return { file, lines: lines.length };
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;node cli.answering.js count sample.txt &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="go"&gt;{"ok":true,"data":{"file":"sample.txt","lines":6},"meta":{"provenance":{}}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Humans still see &lt;code&gt;6 lines in sample.txt&lt;/code&gt;. MCP tool calls get the same envelope. The &lt;code&gt;argv&lt;/code&gt; test is crude. 0.11.1's façade has no public "machine asked?" flag yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two defects, fixed in 0.11.1
&lt;/h2&gt;

&lt;p&gt;Drafting on 0.9.2, I hit two defects in what the swap adds. An MCP call ignored the schema's &lt;code&gt;"flag":"--skip-blank"&lt;/code&gt; and passed &lt;code&gt;--skipBlank&lt;/code&gt;, which the program refused. Completions offered &lt;code&gt;--no-skip-blank&lt;/code&gt; and &lt;code&gt;--no-version&lt;/code&gt;, likewise refused. I fixed both in 0.11.1. The same call now answers:&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"count","arguments":{"file":"sample.txt","skipBlank":true}}}'&lt;/span&gt; | node cli.answering.js &lt;span class="nt"&gt;--mcp&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.result.content[0].text'&lt;/span&gt;
&lt;span class="go"&gt;{"ok":true,"data":{"file":"sample.txt","lines":4},"meta":{"provenance":{"skipBlank":{"source":"flag"}}}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still open: an action's throw stays uncaught under &lt;code&gt;--json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the grade covers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/r/unfeqptkco?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;commander's own suite&lt;/a&gt; (v15.0.0, 110 files, unmodified) runs against &lt;code&gt;burgee/commander&lt;/code&gt; next to a control on real commander. Re-run for this article: &lt;strong&gt;1360 / 1360&lt;/strong&gt;, control 1360 / 1360. That grade measures what you &lt;em&gt;keep&lt;/em&gt;. The two defects sat in what you &lt;em&gt;gain&lt;/em&gt;, which commander's tests cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  When commander alone is the right size
&lt;/h2&gt;

&lt;p&gt;On disk, commander 15.0.0 unpacks to 207,368 bytes; burgee 0.11.1 and the &lt;a href="https://ofriperetz.dev/go/burgee-zero-dependency-cli-stack?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;five sibling packages&lt;/a&gt; it installs, to 1,266,628. In a bundle, burgee's own &lt;code&gt;lighter-than-commander&lt;/code&gt; gate measures &lt;strong&gt;not met, 1.524×&lt;/strong&gt;. If no agent will call your CLI, commander is the smaller choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ofriperetz.dev/go/r/11og8jstylb?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;burgee vs commander&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/r/j58a8rly66?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;compatibility&lt;/a&gt; · &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/burgee?utm_source=devto&amp;amp;from=burgee-change-one-import" rel="noopener noreferrer"&gt;source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which command in your CLI prints something an agent currently scrapes with a regex, and what would returning it instead break?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>A commander Stack Installs 70 Packages, burgee's Family 9. Like for Like, the Family Weighs 2.4 More.</title>
      <dc:creator>Ofri Peretz</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:13:43 +0000</pubDate>
      <link>https://dev.to/ofri-peretz/a-commander-stack-installs-70-packages-burgees-family-9-like-for-like-the-family-weighs-24x-aed</link>
      <guid>https://dev.to/ofri-peretz/a-commander-stack-installs-70-packages-burgees-family-9-like-for-like-the-family-weighs-24x-aed</guid>
      <description>&lt;p&gt;I maintain burgee, nine CLI packages, and counted them against the stack they replace, one usual package per layer:&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 commander chalk ora inquirer string-width cosmiconfig execa signal-exit terminal-link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;70 packages, maintained by 25 npm accounts.&lt;/strong&gt; burgee's nine install &lt;strong&gt;9 packages from 1 account&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the flattering row. &lt;a href="https://ofriperetz.dev/go/eslint-plugin-dependency-weight?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; printed the rows against my plugins; same method, same rule here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer by layer
&lt;/h2&gt;

&lt;p&gt;Each incumbent is the first whole package in burgee's own "Replaces" column (the terminal row lists half of ansi-escapes first; with it, 66 packages), so the stack comes from &lt;a href="https://ofriperetz.dev/go/i-built-what-i-benchmark-heres-how-i-try-not-to-cheat?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;my design surface, not a neutral sample&lt;/a&gt;. Resolved 2026-09-23, one package at a time; KB is 1,024 bytes of files, whole tree included.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;incumbent&lt;/th&gt;
&lt;th&gt;pkgs&lt;/th&gt;
&lt;th&gt;KB&lt;/th&gt;
&lt;th&gt;burgee family&lt;/th&gt;
&lt;th&gt;pkgs&lt;/th&gt;
&lt;th&gt;KB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:commander@15.0.0"&gt;commander@15.0.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;203&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:burgee@0.10.0"&gt;burgee@0.10.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1169&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:chalk@6.0.0"&gt;chalk@6.0.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:roundel@0.4.3"&gt;roundel@0.4.3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:ora@9.4.1"&gt;ora@9.4.1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;278&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:flagstaff@0.3.7"&gt;flagstaff@0.3.7&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;546&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:inquirer@14.2.2"&gt;inquirer@14.2.2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;1002&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:caique@0.4.3"&gt;caique@0.4.3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:string-width@8.2.2"&gt;string-width@8.2.2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:linegauge@0.4.4"&gt;linegauge@0.4.4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:cosmiconfig@10.0.1"&gt;cosmiconfig@10.0.1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1789&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:seniority@0.4.3"&gt;seniority@0.4.3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;149&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:execa@10.0.1"&gt;execa@10.0.1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;632&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:bellpull@0.2.3"&gt;bellpull@0.2.3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:signal-exit@4.1.0"&gt;signal-exit@4.1.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:closeout@0.4.1"&gt;closeout@0.4.1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:terminal-link@5.0.0"&gt;terminal-link@5.0.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:paratext@0.5.4"&gt;paratext@0.5.4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;On bytes, burgee's side is heavier in six of nine layers.&lt;/strong&gt; Installed together, the incumbents weigh 3,851 KB; burgee, 1,577. The incumbent trees overlap (79 summed, 70 installed); burgee's overlap completely (20 summed, 9 installed).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that byte gap comes from
&lt;/h2&gt;

&lt;p&gt;Three layers carry it, none cleanly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cosmiconfig is 1,789 KB, and 1,702 of that is js-yaml and its argparse.&lt;/strong&gt; seniority parses no YAML: it reads the JSON subset and refuses the rest by name. Against cosmiconfig's own test suite it passes 186 of 243.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;execa has no graded drop-in.&lt;/strong&gt; bellpull's compat row is cross-spawn, not execa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;inquirer&lt;/strong&gt; is graded through &lt;code&gt;@inquirer/core&lt;/code&gt; (41 of 41), not &lt;code&gt;inquirer&lt;/code&gt; itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep only layers where burgee's drop-in scores what the incumbent scores on &lt;a href="https://ofriperetz.dev/go/r/4ourcwqz96?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;its own suite&lt;/a&gt;: commander, chalk, ora, &lt;code&gt;@inquirer/core@12.0.3&lt;/code&gt;, string-width, signal-exit (not terminal-link: 8 of 10). &lt;strong&gt;28 packages, 662 KB.&lt;/strong&gt; burgee still installs all nine (sibling dependencies pull in the other three), 1,577 KB. &lt;strong&gt;Like for like, burgee is 2.4× heavier on disk&lt;/strong&gt;, while the count stays 28 against 9, maintainers 14 against one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three gates burgee publishes as not met
&lt;/h2&gt;

&lt;p&gt;burgee's &lt;a href="https://ofriperetz.dev/go/r/2chfqwzmyn4?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;README&lt;/a&gt; states nine gates; three fail their ≤ 1.0× target. I re-ran each with burgee's own fixtures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gate&lt;/th&gt;
&lt;th&gt;published&lt;/th&gt;
&lt;th&gt;re-run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;burgee&lt;/code&gt; bundle vs cac&lt;/td&gt;
&lt;td&gt;2.636×&lt;/td&gt;
&lt;td&gt;2.661×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;burgee/commander&lt;/code&gt; bundle vs commander&lt;/td&gt;
&lt;td&gt;1.514×&lt;/td&gt;
&lt;td&gt;1.520×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cold start vs cac&lt;/td&gt;
&lt;td&gt;1.443×&lt;/td&gt;
&lt;td&gt;1.67–2.31×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bundle rows agree within a percent. The cold-start re-runs, at load average above 80, read worse than burgee's published figure. Either way, not met. burgee's parity rows compare its bundles against cac, commander or yargs &lt;strong&gt;plus&lt;/strong&gt; cosmiconfig, exit-hook and restore-cursor (0.282×, 0.468×, 0.531×). Those stacks include cosmiconfig, which my like-for-like rule excludes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "zero dependencies" means here
&lt;/h2&gt;

&lt;p&gt;Six of nine declare no dependencies. burgee, flagstaff and caique depend only on those six: &lt;strong&gt;no dependency outside the family&lt;/strong&gt;, which is not zero. burgee's README table says 0 runtime dependencies; the registry lists five, all siblings.&lt;/p&gt;

&lt;p&gt;One account is also a &lt;a href="https://ofriperetz.dev/go/eslint-plugin-maintenance-signals?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;concentration&lt;/a&gt;. Mid-measurement, my pipeline published &lt;a href="mailto:burgee@0.10.0"&gt;burgee@0.10.0&lt;/a&gt; at 05:34 UTC, then the closeout and linegauge it requires at 05:39 and 05:42; for eight minutes &lt;code&gt;npm i burgee&lt;/code&gt; failed with &lt;code&gt;ETARGET&lt;/code&gt;. And the family declares Node 24, where the incumbent stack's floor is Node 22.18. If you support Node 22.18, the incumbents are your option.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check yours
&lt;/h2&gt;

&lt;p&gt;Part 1's method, plus bytes and maintainers:&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;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm init &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
npm i &lt;span class="nt"&gt;--package-lock-only&lt;/span&gt; &amp;lt;your dependencies&amp;gt;
node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"console.log(Object.keys(require('./package-lock.json').packages).filter(k=&amp;gt;k.startsWith('node_modules/')).length)"&lt;/span&gt;
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"let t=0;const f=require('fs'),w=d=&amp;gt;f.readdirSync(d,{withFileTypes:true}).forEach(e=&amp;gt;{const p=d+'/'+e.name;e.isDirectory()?e.name!=='.bin'&amp;amp;&amp;amp;w(p):e.isFile()&amp;amp;&amp;amp;e.name!=='.package-lock.json'&amp;amp;&amp;amp;(t+=f.statSync(p).size)});w('node_modules');console.log(t)"&lt;/span&gt;
node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"const l=require('./package-lock.json').packages,m=new Set(),u=[];Promise.all(Object.keys(l).filter(k=&amp;gt;k.startsWith('node_modules/')).map(k=&amp;gt;fetch('https://registry.npmjs.org/'+k.split('node_modules/').pop().replace('/','%2F')).then(r=&amp;gt;r.ok?r.json():Promise.reject()).then(d=&amp;gt;(d.maintainers||[]).forEach(x=&amp;gt;m.add(x.name))).catch(()=&amp;gt;u.push(k)))).then(()=&amp;gt;console.log(m.size,'unfetched',u.length))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Part 1 subtracted a 69-package ESLint baseline; here the shared baseline is Node, zero packages. pnpm, yarn 1 and bun resolve the same counts. The family ships often and its caret ranges float: pin all nine as tabled, or expect drift.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Run it on your CLI: how many npm accounts can ship code into your tree, and did you know that number before today?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[If the method earns a place in your release review, ⭐ &lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/burgee?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;star burgee&lt;/a&gt;.](&lt;a href="https://ofriperetz.dev/go/gh/ofri-peretz/burgee?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack" rel="noopener noreferrer"&gt;https://ofriperetz.dev/go/gh/ofri-peretz/burgee?utm_source=devto&amp;amp;from=burgee-zero-dependency-cli-stack&lt;/a&gt;)&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>cli</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
