<?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: quintetkit</title>
    <description>The latest articles on DEV Community by quintetkit (@quintetkit).</description>
    <link>https://dev.to/quintetkit</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%2F4109022%2Fcf76ff45-b365-4b7c-a788-a19c036c50f2.png</url>
      <title>DEV Community: quintetkit</title>
      <link>https://dev.to/quintetkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quintetkit"/>
    <language>en</language>
    <item>
      <title>Bisection Narrowed It Correctly and Still Never Reached the Answer</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Tue, 22 Sep 2026 04:16:31 +0000</pubDate>
      <link>https://dev.to/quintetkit/bisection-narrowed-it-correctly-and-still-never-reached-the-answer-3pge</link>
      <guid>https://dev.to/quintetkit/bisection-narrowed-it-correctly-and-still-never-reached-the-answer-3pge</guid>
      <description>&lt;p&gt;I built a pipeline that publishes articles to Zenn by &lt;code&gt;git push&lt;/code&gt;, using its GitHub&lt;br&gt;
integration. I pushed twelve at once.&lt;br&gt;
&lt;strong&gt;Two went live. The other ten did nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No errors. The integration was fine. The push arrived. The deploy reported success.&lt;br&gt;
And the articles did not appear.&lt;/p&gt;

&lt;p&gt;It took me a full day to find the cause. &lt;strong&gt;The cause itself was not difficult.&lt;/strong&gt;&lt;br&gt;
What was difficult is that the reason for the failure was never returned to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first (all wrong)
&lt;/h2&gt;

&lt;p&gt;My first assumption was that one file was malformed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tried&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Confirm the push arrived&lt;/td&gt;
&lt;td&gt;It did&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BOM / CRLF / control characters&lt;/td&gt;
&lt;td&gt;Clean. Identical in shape to the articles that worked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diff the frontmatter&lt;/td&gt;
&lt;td&gt;No difference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty commit, redeploy&lt;/td&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change the body, redeploy&lt;/td&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename the slug (twice)&lt;/td&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait 4.5 hours, push a single file&lt;/td&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Push a brand new article the platform had never stored&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No change&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row killed the hypothesis. &lt;strong&gt;A brand new file was not saved either&lt;/strong&gt;, so&lt;br&gt;
this was not about any individual file.&lt;/p&gt;

&lt;p&gt;Half a day gone, spent knocking down guesses one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching to bisection
&lt;/h2&gt;

&lt;p&gt;I stopped guessing and started cutting the input in half.&lt;/p&gt;

&lt;p&gt;The repository held twelve articles plus a book (ten chapters and a config). I deleted&lt;br&gt;
the book outright.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Book removed → &lt;strong&gt;no change&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I deleted articles too, down to the two live ones plus one candidate —&lt;br&gt;
&lt;strong&gt;three files total.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three files → &lt;strong&gt;no change&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That yields a conclusion:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The repository contents are not the cause.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was a correct conclusion. Bisection worked. It killed hypotheses far faster than&lt;br&gt;
the exhaustive approach that preceded it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it still did not reach the answer.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it could not
&lt;/h2&gt;

&lt;p&gt;Any experiment you run locally reduces to: change the input, observe the output.&lt;br&gt;
Bisection is one such method — &lt;strong&gt;halve the input range, watch the output change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It assumes &lt;strong&gt;the output is observable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my case the output was a single bit: the article appeared, or it did not. And the&lt;br&gt;
actual reason for the failure &lt;strong&gt;is not returned in the push response, not exposed&lt;br&gt;
through the API, and not written anywhere in the repository.&lt;/strong&gt; It is displayed only&lt;br&gt;
on Zenn's deploy dashboard.&lt;/p&gt;

&lt;p&gt;Which is to say: &lt;strong&gt;I had never once looked at the place where the reason lived.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However far you narrow the input, if the output stays one bit wide, one bit is all you&lt;br&gt;
get. &lt;strong&gt;When the reason sits somewhere unobservable, local experiments cannot reach it&lt;br&gt;
in principle.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual reason
&lt;/h2&gt;

&lt;p&gt;The deploy dashboard said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The following articles were not deployed because the posting limit was reached:&lt;br&gt;
(four article names)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A posting rate limit.&lt;/strong&gt; The deploy had succeeded every single time; the articles&lt;br&gt;
were rejected inside it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zenn.dev/faq/rate-limit" rel="noopener noreferrer"&gt;Zenn's FAQ&lt;/a&gt; states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The limit logic is &lt;strong&gt;not disclosed&lt;/strong&gt; — explicitly, to prevent gaming&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Articles are judged on the number posted in the previous 24 hours&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Books have a separate weekly limit&lt;/li&gt;
&lt;li&gt;Hitting the limit is temporary; posting resumes after enough time passes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measured: two got through in 24 hours, and everything after that was dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The very first thing I did — pushing twelve at once — was the cause.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I had the order backwards
&lt;/h2&gt;

&lt;p&gt;The mistake was not using bisection. It was the order.&lt;/p&gt;

&lt;p&gt;What I should have done, &lt;strong&gt;before starting to narrow anything, is find out where the&lt;br&gt;
failure reason is reported.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the push response? → no&lt;/li&gt;
&lt;li&gt;Through the API? → no&lt;/li&gt;
&lt;li&gt;Written back into the repository? → no&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On the deploy dashboard? → yes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That check takes five minutes. Instead I spent half a day &lt;strong&gt;staring at an output that&lt;br&gt;
was never going to say anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"No error appeared" is not the same as working. &lt;strong&gt;It may only mean you are not looking&lt;br&gt;
where the errors appear.&lt;/strong&gt; Those two states are entirely different.&lt;/p&gt;

&lt;h3&gt;
  
  
  I fell into the same hole again the same day
&lt;/h3&gt;

&lt;p&gt;On dev.to, updating nine published articles through the API. Spaced three seconds&lt;br&gt;
apart, three of them failed with &lt;code&gt;500 Internal Server Error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;500 is a server-side fault. Read plainly, it means the other end broke. I waited ten&lt;br&gt;
seconds and retried — all three succeeded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rate limit was arriving as a 500.&lt;/strong&gt; Not a 429.&lt;/p&gt;

&lt;p&gt;The error you get back does not necessarily describe what happened. &lt;strong&gt;"A 500 came&lt;br&gt;
back" means a 500 came back. It does not mean the server broke.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation makes you look like a spammer without meaning to
&lt;/h2&gt;

&lt;p&gt;One more thing, and it is not technical.&lt;/p&gt;

&lt;p&gt;Publishing twelve articles at once is &lt;strong&gt;physically impossible for a person.&lt;/strong&gt; From the&lt;br&gt;
platform's side, that shape is indistinguishable from a machine flooding the queue.&lt;/p&gt;

&lt;p&gt;Zenn does have a &lt;a href="https://info.zenn.dev/2026-03-10-ai-contents-guideline" rel="noopener noreferrer"&gt;policy on AI-written content&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posting machine-generated text is prohibited&lt;/li&gt;
&lt;li&gt;Mass-producing AI-generated posts is grounds for account suspension&lt;/li&gt;
&lt;li&gt;But &lt;strong&gt;using AI is not itself prohibited&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;What it asks for is that "a human is the one publishing" — that &lt;strong&gt;the author has
verified the content&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My articles are built from bugs I actually hit and numbers I actually measured. The&lt;br&gt;
substance is on the right side of that policy. &lt;strong&gt;The delivery was not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rate limit is better understood as a warning than a punishment. Which means&lt;br&gt;
looking for a workaround is the wrong move. &lt;strong&gt;Slowing down is the correct fix.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep exactly one article queued for publication at a time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole fix. When one goes live, queue the next.&lt;/p&gt;

&lt;p&gt;An automated pipeline, left alone, &lt;strong&gt;will try to emit everything it is holding.&lt;/strong&gt; The&lt;br&gt;
limit had to come from what is acceptable to publish, not from what is available to&lt;br&gt;
publish.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Bisection assumes the output is observable. &lt;strong&gt;When the output is one bit wide, you
can narrow the range correctly and still never reach the answer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before you start narrowing, find out where failure reasons are reported.&lt;/strong&gt; Five minutes&lt;/li&gt;
&lt;li&gt;"No error appeared" and "I am not looking where errors appear" are different states&lt;/li&gt;
&lt;li&gt;The error code you receive may not describe what happened — a rate limit can arrive
as a 500&lt;/li&gt;
&lt;li&gt;Automated publishing takes the shape of machine flooding. &lt;strong&gt;Rate-limit yourself by
what is acceptable to publish, not by what you have available&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bisection I spent half a day on reached a correct conclusion.&lt;br&gt;
&lt;strong&gt;One line of a log would have finished it in five minutes.&lt;/strong&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://dev.to/quintetkit/a-default-that-is-safe-on-create-is-destructive-on-update-4aj4"&gt;A Default That Is Safe on Create Is Destructive on Update&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/treating-it-worked-as-verification-ships-code-that-only-works-with-one-commit-2bk3"&gt;Treating "It Worked" as Verification Ships Code That Only Works With One Commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/assigning-5-personas-to-claude-code-for-parallel-development-5abn"&gt;Assigning 5 Personas to Claude Code for Parallel Development&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>githubactions</category>
      <category>webdev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The Page You Wrote by Hand Is the One That Stops Improving</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Sun, 20 Sep 2026 11:23:52 +0000</pubDate>
      <link>https://dev.to/quintetkit/the-page-you-wrote-by-hand-is-the-one-that-stops-improving-5e7i</link>
      <guid>https://dev.to/quintetkit/the-page-you-wrote-by-hand-is-the-one-that-stops-improving-5e7i</guid>
      <description>&lt;p&gt;I wrote a static site generator and added &lt;code&gt;canonical&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; to 28 article&lt;br&gt;
pages. Fourteen in Japanese, fourteen in English. Until then, search engines had no&lt;br&gt;
way to know they were translations of each other rather than 28 unrelated pages.&lt;/p&gt;

&lt;p&gt;The build passed on the first run. All 28 pages had it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two pages did not.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The pages that were missing it were the most linked pages on the site
&lt;/h2&gt;

&lt;p&gt;The landing page, and its English version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site/index.html          &amp;lt;- product page (Japanese)
site/index.en.html       &amp;lt;- product page (English)
site/articles/*.html     &amp;lt;- 14 articles
site/en/articles/*.html  &amp;lt;- 14 articles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The articles go through a shared &lt;code&gt;page_shell()&lt;/code&gt; function. &lt;strong&gt;The two landing pages are&lt;br&gt;
hand-written HTML.&lt;/strong&gt; They predate the generator, and their layout is a one-off, so&lt;br&gt;
there was never a reason to put them on the template.&lt;/p&gt;

&lt;p&gt;So when I added the annotations to &lt;code&gt;page_shell()&lt;/code&gt;, nothing happened to those two files.&lt;/p&gt;

&lt;p&gt;And it was not only the new annotations.&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;28 articles&lt;/th&gt;
&lt;th&gt;2 landing pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rel="canonical"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hreflang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;og:title&lt;/code&gt; / &lt;code&gt;og:description&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;og:url&lt;/code&gt; / &lt;code&gt;og:type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;og:image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;twitter:card&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;og:image&lt;/code&gt; and &lt;code&gt;twitter:card&lt;/code&gt; are there, because I put them in by hand the day I wrote&lt;br&gt;
the page. Everything added to the template since then is missing.&lt;/p&gt;

&lt;p&gt;The practical effect: &lt;strong&gt;a shared link rendered an image with a guessed title.&lt;/strong&gt; With no&lt;br&gt;
&lt;code&gt;og:title&lt;/code&gt;, a crawler falls back to &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; or whatever heading it finds convincing.&lt;/p&gt;

&lt;p&gt;And these two pages are where the READMEs of three repositories and the footers of 24&lt;br&gt;
articles send people. The most linked pages on the site had the least markup on them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The exception you wrote by hand does not receive later improvements
&lt;/h2&gt;

&lt;p&gt;Stated generally:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improvements go into the template. Things that do not go through the template do not&lt;br&gt;
get them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the day you write it, the hand-written page is usually the better one. You spent&lt;br&gt;
time on it, and you built it for that page specifically. &lt;strong&gt;That part is not the mistake.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mistake is what happens afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feb   write the landing page by hand   ... it is the best page on the site
May   add OGP to the template          ... only the articles improve
Jul   add structured data              ... only the articles improve
Sep   add hreflang                     ... only the articles improve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The hand-written page stays at the quality it had the day you wrote it.&lt;/strong&gt; Everything&lt;br&gt;
around it keeps rising, so in relative terms it falls.&lt;/p&gt;

&lt;p&gt;The same shape shows up all over a codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the one &lt;code&gt;try/catch&lt;/code&gt; that does not go through the shared error handler&lt;/li&gt;
&lt;li&gt;the one &lt;code&gt;fetch&lt;/code&gt; that does not go through the shared HTTP client&lt;/li&gt;
&lt;li&gt;the one &lt;code&gt;console.log&lt;/code&gt; that does not go through the logger&lt;/li&gt;
&lt;li&gt;the script you run by hand that CI never touches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those was a reasonable call when it was made. Every one of them stops&lt;br&gt;
receiving what lands afterward.&lt;/p&gt;
&lt;h2&gt;
  
  
  Checking the output beats normalizing the input
&lt;/h2&gt;

&lt;p&gt;There are two ways out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A. Remove the exception and put everything on the template.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The right direction, but not always available. My landing page has a one-off layout,&lt;br&gt;
and forcing it through the article template breaks it. Generalizing the template enough&lt;br&gt;
to hold it would &lt;strong&gt;make the generation of the other 28 pages more complicated.&lt;/strong&gt; Adding&lt;br&gt;
complexity to 28 pages for the sake of 2 is not a trade I want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B. Check the output, whatever produced it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what I did. &lt;strong&gt;Look at what came out, not at how it was made.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hreflang&lt;/code&gt; has a &lt;strong&gt;reciprocity&lt;/strong&gt; requirement. If A says "the English version is B", then&lt;br&gt;
B has to say "the Japanese version is A". &lt;strong&gt;If it does not, the annotation is ignored&lt;br&gt;
entirely.&lt;/strong&gt; A one-sided declaration does nothing.&lt;/p&gt;

&lt;p&gt;So read every HTML file that was produced and check exactly that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.github.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ann&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# lang -&amp;gt; href
&lt;/span&gt;    &lt;span class="n"&gt;alts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;link rel=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alternate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; hreflang=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; href=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]+)&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;alts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alts&lt;/span&gt;

&lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alts&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;alts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                      &lt;span class="c1"&gt;# external hosts will not annotate back
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has no hreflang of its own&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; does not point back&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages with hreflang: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  inconsistent: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This check does not know whether a page came from the template.&lt;/strong&gt; It does not need to.&lt;br&gt;
Hand-written or generated, if the file landed in &lt;code&gt;site/&lt;/code&gt;, it is held to the same&lt;br&gt;
standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managing the input means managing every path into it, and you will miss one when a new&lt;br&gt;
path appears.&lt;/strong&gt; Checking the output is one place, and it holds no matter how many paths&lt;br&gt;
there are.&lt;/p&gt;
&lt;h2&gt;
  
  
  Some annotations are worth keeping even when they cannot work
&lt;/h2&gt;

&lt;p&gt;Running the check gave this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pages with hreflang: 32
pointing at external hosts: 20   &amp;lt;- reciprocity cannot hold
inconsistent within the site: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The articles are also published on other platforms, and those copies are the &lt;code&gt;canonical&lt;/code&gt;&lt;br&gt;
ones. That means &lt;code&gt;hreflang&lt;/code&gt; points at an external URL too — and &lt;strong&gt;those platforms will&lt;br&gt;
never point back.&lt;/strong&gt; Reciprocity does not hold, so those 20 annotations are ignored.&lt;/p&gt;

&lt;p&gt;I left them pointing outward anyway, because there are only two options:&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;reciprocity&lt;/th&gt;
&lt;th&gt;agrees with canonical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;point at the external URL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;no&lt;/strong&gt; (annotation ignored)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;point at my own non-canonical copy&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A page whose &lt;code&gt;hreflang&lt;/code&gt; and &lt;code&gt;canonical&lt;/code&gt; disagree &lt;strong&gt;gets both signals distrusted.&lt;/strong&gt; Being&lt;br&gt;
ignored is the cheaper failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sometimes you pick the option you know does not work, because the alternative does&lt;br&gt;
damage.&lt;/strong&gt; What matters is picking it &lt;em&gt;knowing&lt;/em&gt; that. An annotation that is inert because&lt;br&gt;
you chose it and one that is inert because you never checked look identical today, and&lt;br&gt;
differ entirely the day you can fix it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Improvements land in the template. Anything outside it gets none of them.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A hand-written exception &lt;strong&gt;freezes at the quality it had on day one&lt;/strong&gt;, and falls
behind as everything around it rises&lt;/li&gt;
&lt;li&gt;The page you put the most care into is the one most likely to be left behind&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the output rather than normalizing every input&lt;/strong&gt; — it holds as paths multiply&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hreflang&lt;/code&gt; requires &lt;strong&gt;reciprocity&lt;/strong&gt;; a one-sided declaration does nothing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An ignored annotation is better than one that contradicts your &lt;code&gt;canonical&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/write-the-same-decision-in-two-places-and-only-one-of-them-gets-fixed-5dcp"&gt;Write the Same Decision in Two Places and Only One of Them Gets Fixed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/publishing-the-same-article-in-three-places-splits-its-search-value-3hnj"&gt;Publishing the Same Article in Three Places Splits Its Search Value&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/treating-it-worked-as-verification-ships-code-that-only-works-with-one-commit-2bk3"&gt;Treating "It Worked" as Verification Ships Code That Only Works With One Commit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>html</category>
      <category>seo</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Write the Same Decision in Two Places and Only One of Them Gets Fixed</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Sat, 19 Sep 2026 02:25:41 +0000</pubDate>
      <link>https://dev.to/quintetkit/write-the-same-decision-in-two-places-and-only-one-of-them-gets-fixed-5dcp</link>
      <guid>https://dev.to/quintetkit/write-the-same-decision-in-two-places-and-only-one-of-them-gets-fixed-5dcp</guid>
      <description>&lt;p&gt;There are two storefronts, one domestic and one international, and &lt;strong&gt;the&lt;br&gt;
international one is not open yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I made a rule: if the international store is not open, send the reader to the&lt;br&gt;
domestic one. It has an English interface and takes international cards.&lt;br&gt;
&lt;strong&gt;Being sent somewhere you can actually buy beats being sent nowhere.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wrote that rule into the function that generates the footer of each article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gumroad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The full kit is available here.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gumroad&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# until the international store opens, send readers to the domestic one
&lt;/span&gt;    &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The full kit is on BOOTH ...&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;booth&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It went out to 24 articles. All of them say you can buy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The page they link to said "Coming soon"
&lt;/h2&gt;

&lt;p&gt;The function that rewrites the button on the product page lives somewhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gumroad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;new_btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;a class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;btn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; href=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The English button only looks at &lt;code&gt;gumroad&lt;/code&gt;. &lt;strong&gt;It is not open, so that is &lt;code&gt;None&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;aria-disabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Purchase Quintet (Coming soon)&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which left the site saying this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24 articles   "you can buy it here" -&amp;gt; link to the product page
product page  "Coming soon"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Readers arrive at the product page from the articles.&lt;/strong&gt; The order could not be&lt;br&gt;
worse. Only the people who read a whole article, got interested, and clicked&lt;br&gt;
were told to come back later. &lt;strong&gt;The readers most likely to buy were the only&lt;br&gt;
ones turned away.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  I did not recognize it as the same decision
&lt;/h2&gt;

&lt;p&gt;In hindsight the two pieces of code decide the same thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which storefront do we send a reader of this language to?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So why did it not register? &lt;strong&gt;Because one builds a paragraph of text and the&lt;br&gt;
other rewrites an HTML attribute.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;buy_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# returns a Markdown paragraph
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rewrite_lp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# regex-replaces HTML
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different arguments, different return types, different output formats. When code&lt;br&gt;
does not look alike, &lt;strong&gt;the fact that it implements the same policy does not&lt;br&gt;
surface.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Duplication is usually described as the same code written twice. &lt;strong&gt;The dangerous&lt;br&gt;
kind is the same decision written twice in two different shapes.&lt;/strong&gt; The first kind&lt;br&gt;
you can grep for. The second kind you cannot.&lt;/p&gt;
&lt;h2&gt;
  
  
  What caught it was reading the diff before pushing
&lt;/h2&gt;

&lt;p&gt;Not a test. &lt;strong&gt;I read the diff before pushing.&lt;/strong&gt;&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;-&amp;lt;a class="btn" href="https://quartet-dev.booth.pm/items/..."&amp;gt;Purchase Quintet&amp;lt;/a&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+&amp;lt;a class="btn" href="#" aria-disabled="true"&amp;gt;Purchase Quintet (Coming soon)&amp;lt;/a&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated site is committed to a repository, so regenerating it produces a&lt;br&gt;
diff. &lt;strong&gt;Checking that nothing came along besides the change you meant&lt;/strong&gt; is what&lt;br&gt;
works against this class of bug.&lt;/p&gt;

&lt;p&gt;What I meant to do was add a fifteenth article. A button had no business being&lt;br&gt;
disabled. &lt;strong&gt;The mismatch between intent and diff is the only signal there was.&lt;/strong&gt;&lt;br&gt;
A workflow that does not read diffs ships this.&lt;/p&gt;
&lt;h2&gt;
  
  
  One place for the decision, separate places for the presentation
&lt;/h2&gt;

&lt;p&gt;There is a range of possible fixes. &lt;strong&gt;Merging the two functions is too far.&lt;/strong&gt; One&lt;br&gt;
returns Markdown, the other rewrites HTML. Merging them only moves the branching&lt;br&gt;
inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Share the decision, nothing else.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Which storefront this language&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s readers go to, or None if none is open.

    This used to be written in two places. A fallback was added to the article
    footer and not to the landing page, so the articles said &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;you can buy it&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
    while the page they linked to said &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coming soon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;. The destination is decided
    here; how it is presented is up to the caller.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ja&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gumroad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It returns the name of the store, not the URL.&lt;/strong&gt; The callers do not need to know&lt;br&gt;
where to send people so much as &lt;strong&gt;which store ended up being chosen&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the article wording differs by store (sending readers abroad needs an explanation)&lt;/li&gt;
&lt;li&gt;the landing page only disables its button when there is no store at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The presentation stays different. Only the decision is shared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# article
&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gumroad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# landing page
&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ja&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run every combination
&lt;/h2&gt;

&lt;p&gt;Two stores, each open or not, is &lt;strong&gt;four cases.&lt;/strong&gt; I ran all four and checked that&lt;br&gt;
the articles and the landing pages say the same thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;booth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no  gumroad=no  -&amp;gt; EN page=#        JA page=#        EN article=none&lt;/span&gt;
&lt;span class="py"&gt;booth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes gumroad=no  -&amp;gt; EN page=BOOTH    JA page=BOOTH    EN article=booth&lt;/span&gt;
&lt;span class="py"&gt;booth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no  gumroad=yes -&amp;gt; EN page=Gumroad  JA page=#        EN article=gumroad&lt;/span&gt;
&lt;span class="py"&gt;booth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes gumroad=yes -&amp;gt; EN page=Gumroad  JA page=BOOTH    EN article=gumroad&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second row is the bug. &lt;strong&gt;Before the fix, the English page was &lt;code&gt;#&lt;/code&gt; there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four cases is few enough to run all of them. &lt;strong&gt;While the combinations are still&lt;br&gt;
countable, running all of them is faster than arguing about which one is&lt;br&gt;
representative.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also checked that restoring the original settings produces a zero diff. The&lt;br&gt;
test rewrites configuration, and &lt;strong&gt;forgetting to restore it breaks the live&lt;br&gt;
links.&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The dangerous duplication is not the same code twice — it is &lt;strong&gt;the same
decision twice, in two different shapes.&lt;/strong&gt; grep does not find that&lt;/li&gt;
&lt;li&gt;When code does not look alike, &lt;strong&gt;the shared policy does not surface&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Commit the generated output and &lt;strong&gt;read the diff before pushing.&lt;/strong&gt; The mismatch
between what you meant and what changed is the signal&lt;/li&gt;
&lt;li&gt;Share &lt;strong&gt;only the decision.&lt;/strong&gt; Sharing the presentation pushes the branching back inside&lt;/li&gt;
&lt;li&gt;Have the decision return &lt;strong&gt;which option was chosen&lt;/strong&gt;, not the final value, so
callers can present it differently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While the combinations are countable, run all of them&lt;/strong&gt; — and verify that a
test which rewrites configuration puts it back&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/every-test-passed-and-it-could-not-read-a-single-real-record-59pn"&gt;Every Test Passed and It Could Not Read a Single Real Record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://quintetkit.github.io/en/articles/handwritten-page-stops-improving.html" rel="noopener noreferrer"&gt;The Page You Wrote by Hand Is the One That Stops Improving&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/a-default-that-is-safe-on-create-is-destructive-on-update-4aj4"&gt;A Default That Is Safe on Create Is Destructive on Update&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>webdev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>A Default That Is Safe on Create Is Destructive on Update</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Fri, 18 Sep 2026 09:47:39 +0000</pubDate>
      <link>https://dev.to/quintetkit/a-default-that-is-safe-on-create-is-destructive-on-update-4aj4</link>
      <guid>https://dev.to/quintetkit/a-default-that-is-safe-on-create-is-destructive-on-update-4aj4</guid>
      <description>&lt;p&gt;I wanted to add links to nine already-published articles. Just a bulk update through&lt;br&gt;
my own publishing tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool tried to unpublish them.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The safe direction of a default flips with the operation
&lt;/h2&gt;

&lt;p&gt;The tool was built like this:&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;publish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rest&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--publish&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;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&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="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// decided by an explicit flag, not by frontmatter&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;published&lt;/code&gt; does not come from the article's frontmatter. It comes from &lt;code&gt;--publish&lt;/code&gt; on&lt;br&gt;
the command line. Without the flag, you get a draft.&lt;/p&gt;

&lt;p&gt;The reason was right there in a comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Draft by default. Publish only when &lt;code&gt;--publish&lt;/code&gt; is explicit,&lt;br&gt;
because &lt;strong&gt;failing to publish is safer than publishing by accident.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;For creation, that is correct.&lt;/strong&gt; Shipping a half-written draft to the world costs more&lt;br&gt;
than forgetting to ship a finished one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For updates, the same default points the other way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forget &lt;code&gt;--publish&lt;/code&gt; while updating an existing article and you send &lt;code&gt;published: false&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;A live article drops back to draft.&lt;/strong&gt; The URL stays; it just 404s.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgot to publish → notice later, publish then&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un-published by accident → every inbound link and every bit of search equity dies&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same phrase, "err on the safe side", named opposite directions depending on the verb.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix was not "infer it more cleverly"
&lt;/h2&gt;

&lt;p&gt;My first instinct was to preserve the existing state on update: if &lt;code&gt;--publish&lt;/code&gt; is absent,&lt;br&gt;
keep whatever the article currently is.&lt;/p&gt;

&lt;p&gt;I dropped it. &lt;strong&gt;You cannot distinguish "I meant to unpublish this" from "I forgot the&lt;br&gt;
flag."&lt;/strong&gt; Inferring past something you cannot distinguish means that one day someone&lt;br&gt;
genuinely wants to unpublish and is silently ignored.&lt;/p&gt;

&lt;p&gt;So it refuses:&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="c1"&gt;// published comes from the explicit flag, so an update without --publish drags a&lt;/span&gt;
&lt;span class="c1"&gt;// live article back to draft. Accidental unpublishing is far more common than&lt;/span&gt;
&lt;span class="c1"&gt;// deliberate, so refuse. The URL survives but 404s, and the backlinks die with it.&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;existing&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&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;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" is already published. Updating without --publish reverts it to a draft.\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  Pass --publish to update it.&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;I ran it without the flag and confirmed it exits 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you want to unpublish, that should be its own operation.&lt;/strong&gt; Never something that&lt;br&gt;
happens as a side effect of updating.&lt;/p&gt;
&lt;h2&gt;
  
  
  And then: identity was keyed on a value that changes
&lt;/h2&gt;

&lt;p&gt;The same tool had a second hole — how it found the existing article.&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="c1"&gt;// update if an article with the same title exists, otherwise create&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mine&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/articles/me/all?per_page=100&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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="kr"&gt;any&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It matched on the title.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same day, I fixed a bug in the frontmatter parser. It stripped the outer quotes from&lt;br&gt;
&lt;code&gt;title: "Treating \"It Worked\" as ..."&lt;/code&gt; without unescaping what was inside, so&lt;br&gt;
&lt;strong&gt;an article had shipped with backslashes in its name.&lt;/strong&gt;&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="c1"&gt;// before&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\([&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$1&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;p&gt;The moment that landed, &lt;code&gt;fm.title&lt;/code&gt; produced a different string. And &lt;strong&gt;the published&lt;br&gt;
article still carried the old one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;find&lt;/code&gt; misses. It concludes there is no existing article. It falls through to create.&lt;/p&gt;

&lt;p&gt;Had I pushed without noticing, &lt;strong&gt;a second copy of the same article would have gone live.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Fixing one bug is what exposed the other
&lt;/h3&gt;

&lt;p&gt;This is not "I hit two bugs." It is &lt;strong&gt;one bug whose fix activated the other.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Identity was keyed on a derived value &lt;strong&gt;that the tool itself computes.&lt;/strong&gt; Change how it&lt;br&gt;
is computed and the value changes. Change the value and identity breaks.&lt;/p&gt;

&lt;p&gt;The same shape shows up everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keying on a normalised name → fixing the normalisation rules orphans your existing rows&lt;/li&gt;
&lt;li&gt;Keying on a hash of the body → reformatting makes it a different object&lt;/li&gt;
&lt;li&gt;Keying on a display string → correcting a typo in it is an identity change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Never key identity on a value you might fix.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: record the id the other side assigned
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;id&lt;/code&gt; is assigned by the API. Nothing I do changes it. So the tool now records the id of&lt;br&gt;
every article it creates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"filename -&amp;gt; article id. Titles change; identity must not."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ids"&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;"composite-github-action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4576296&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"linter-false-positives"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4577641&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;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;knownId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;knownId&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;mine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;knownId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;knownId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;knownId&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;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) is not on the server.\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  Either the article was deleted, or this is a different account's key.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  Continuing would create a duplicate, so stopping here.&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;&lt;strong&gt;If there is a recorded id and it is missing, stop.&lt;/strong&gt; Silently falling through to create&lt;br&gt;
is the single worst behaviour available.&lt;/p&gt;
&lt;h3&gt;
  
  
  Verifying it
&lt;/h3&gt;

&lt;p&gt;I deliberately changed a title and pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;title: "... (Without npm Publishing) (id lookup test)"

-&amp;gt; PUT /articles/4576296
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;PUT /articles/4576296&lt;/code&gt;, not &lt;code&gt;POST /articles&lt;/code&gt;.&lt;/strong&gt; The title no longer matches and it&lt;br&gt;
still resolves to the same article. The article count did not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was keyed on the title in the first place
&lt;/h2&gt;

&lt;p&gt;Because I did not want a state file. I wanted the articles to be nothing but Markdown.&lt;/p&gt;

&lt;p&gt;I understand the impulse, and the price of it was &lt;strong&gt;inferring identity on every run.&lt;/strong&gt;&lt;br&gt;
That works right up until the material the inference is built on changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Designs that hold no state tend to become designs that infer state instead.&lt;/strong&gt; What you&lt;br&gt;
are inferring in place of storing is worth knowing explicitly.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A default's safe direction flips with the operation.&lt;/strong&gt; "Don't publish" is the safe
side on create and means "unpublish" on update&lt;/li&gt;
&lt;li&gt;Do not infer past an intent you cannot distinguish. &lt;strong&gt;Refuse, and make it explicit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Destructive operations get their own verb. &lt;strong&gt;Never as a side effect of another one&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not key identity on a value you computed&lt;/strong&gt; — fixing the computation breaks identity&lt;/li&gt;
&lt;li&gt;Record the id the other side assigned, and &lt;strong&gt;stop when a recorded id goes missing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Stateless designs tend to become inference-based designs. Know what you are inferring&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://quintetkit.github.io/en/articles/failures-that-dont-come-back.html" rel="noopener noreferrer"&gt;Bisection Narrowed It Correctly and Still Never Reached the Answer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/treating-it-worked-as-verification-ships-code-that-only-works-with-one-commit-2bk3"&gt;Treating "It Worked" as Verification Ships Code That Only Works With One Commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/what-you-refuse-to-check-decides-the-quality-of-a-linter-5dbb"&gt;What You Refuse to Check Decides the Quality of a Linter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>api</category>
      <category>softwaredevelopment</category>
      <category>cli</category>
    </item>
    <item>
      <title>The Blind Spot Was the Rule I Wrote to Avoid False Positives</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Thu, 17 Sep 2026 02:40:25 +0000</pubDate>
      <link>https://dev.to/quintetkit/the-blind-spot-was-the-rule-i-wrote-to-avoid-false-positives-4cb1</link>
      <guid>https://dev.to/quintetkit/the-blind-spot-was-the-rule-i-wrote-to-avoid-false-positives-4cb1</guid>
      <description>&lt;p&gt;I maintain a linter for a configuration format. Its stated principle is on the&lt;br&gt;
first screen of the README:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A finding exists only if the documentation says the thing errors, is skipped,&lt;br&gt;
or is ignored.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One consequence of that principle is a list of things it deliberately does not&lt;br&gt;
check. The clearest entry:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unknown keys.&lt;/strong&gt; The documentation says the published schema lags the CLI, so&lt;br&gt;
checking this would flag every new feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a good rule. I still think it is a good rule. It is also the reason my&lt;br&gt;
own products shipped a broken line in four places for several days.&lt;/p&gt;
&lt;h2&gt;
  
  
  The linter passed
&lt;/h2&gt;

&lt;p&gt;Before adding a chapter to a paid guide, I ran the linter against every&lt;br&gt;
configuration my own products ship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product/kit           No problems (5 files checked)
product/oss           No problems (4 files checked)
publish/quartet       No problems (4 files checked)
publish/mdlinkcheck   No problems (4 files checked)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green everywhere. So I opened the files and read them, which is the only reason&lt;br&gt;
this post exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .claude/agents/coder.md&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coder&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tools: *&lt;/code&gt;. I wrote that months ago meaning "this one gets everything".&lt;/p&gt;

&lt;p&gt;The documentation says the field takes exact tool names, or the server-level&lt;br&gt;
patterns &lt;code&gt;mcp__&amp;lt;server&amp;gt;&lt;/code&gt; and &lt;code&gt;mcp__&amp;lt;server&amp;gt;__*&lt;/code&gt;. A bare &lt;code&gt;*&lt;/code&gt; is not among them.&lt;br&gt;
It also says that when an entry resolves to nothing, the subagent &lt;strong&gt;refuses to&lt;br&gt;
launch&lt;/strong&gt;, returning an error naming the unresolved entries.&lt;/p&gt;

&lt;p&gt;And the documented way to say "everything" is to &lt;strong&gt;omit the field.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I cannot prove from here that &lt;code&gt;tools: *&lt;/code&gt; fails — I would have to observe a&lt;br&gt;
launch. What I can say is that it is not a documented pattern, and that omitting&lt;br&gt;
the field expresses the same intent with no ambiguity. So I removed the line.&lt;/p&gt;

&lt;p&gt;Four copies had it. Two of them were public.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the linter could not see it
&lt;/h2&gt;

&lt;p&gt;This is the part worth keeping.&lt;/p&gt;

&lt;p&gt;The rule "do not check whether a tool name exists" is correct: the set of tools&lt;br&gt;
grows, a checker can only hold a snapshot, and a snapshot would report every&lt;br&gt;
tool released after it as unknown. One false finding and nobody reads the output&lt;br&gt;
again.&lt;/p&gt;

&lt;p&gt;But that rule was written as &lt;strong&gt;"do not look at the &lt;code&gt;tools&lt;/code&gt; field"&lt;/strong&gt;, and the&lt;br&gt;
thing I needed to catch was not a name at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking whether &lt;code&gt;Bash&lt;/code&gt; is a real tool → needs a current list → goes stale&lt;/li&gt;
&lt;li&gt;Checking whether &lt;code&gt;*&lt;/code&gt; is a valid &lt;strong&gt;entry&lt;/strong&gt; → needs no list → &lt;strong&gt;cannot go stale&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;*&lt;/code&gt; is not a tool name in any version. It never will be. The question is about&lt;br&gt;
the &lt;em&gt;syntax&lt;/em&gt; of the field, and I had filed it under &lt;em&gt;identity&lt;/em&gt; of the values,&lt;br&gt;
where my own rule told me not to look.&lt;/p&gt;

&lt;p&gt;The fix is nine lines and cites the same documentation page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warn  .claude/agents/coder.md:4
      `tools: *` is not a documented pattern. Omit `tools` to inherit every
      tool available to subagents.
      why: https://code.claude.com/docs/en/sub-agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep the no-false-positives promise honest, the clean fixtures now include&lt;br&gt;
the two things that must &lt;strong&gt;not&lt;/strong&gt; warn: a file with &lt;code&gt;tools&lt;/code&gt; omitted, and a file&lt;br&gt;
with &lt;code&gt;disallowedTools: mcp__github&lt;/code&gt;, which is a documented pattern. The test&lt;br&gt;
requires zero findings on that directory. 71 tests became 73.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of this mistake
&lt;/h2&gt;

&lt;p&gt;A rule that says "don't check X because it produces false positives" quietly&lt;br&gt;
becomes "don't look at X". Those are not the same rule, and the gap between them&lt;br&gt;
is exactly where a checkable case can sit unchecked.&lt;/p&gt;

&lt;p&gt;I have started asking one question about every exclusion I have written:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Is there a sub-case here that needs no external knowledge?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For unknown settings keys, the answer is genuinely no — you cannot tell a typo&lt;br&gt;
from a new feature without the current schema. For &lt;code&gt;tools&lt;/code&gt;, the answer was yes,&lt;br&gt;
and I had not asked.&lt;/p&gt;

&lt;p&gt;The other thing I would do differently: &lt;strong&gt;a green run on your own code is not&lt;br&gt;
evidence about your own code.&lt;/strong&gt; It is evidence about the intersection of your&lt;br&gt;
code and the rules you happened to write. Reading the files took four minutes&lt;br&gt;
and found what four green runs did not.&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Scheduled Job That Works and Never Runs</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Wed, 16 Sep 2026 09:08:29 +0000</pubDate>
      <link>https://dev.to/quintetkit/a-scheduled-job-that-works-and-never-runs-10f8</link>
      <guid>https://dev.to/quintetkit/a-scheduled-job-that-works-and-never-runs-10f8</guid>
      <description>&lt;p&gt;I have a job that publishes one article a day. Over four days I rebuilt its&lt;br&gt;
scheduling three times, and each rebuild fixed a real failure that the previous&lt;br&gt;
version could not have shown me.&lt;/p&gt;

&lt;p&gt;Every version passed the test I gave it. &lt;strong&gt;The test was always "does it work&lt;br&gt;
now", never "does it work where it is going to live".&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Version 1: a loop in /tmp
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;nohup &lt;/span&gt;bash /tmp/daily-loop.sh &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It ran. It published. Four scheduled items were riding on it.&lt;/p&gt;

&lt;p&gt;Then the machine restarted and &lt;strong&gt;all four were gone.&lt;/strong&gt; &lt;code&gt;/tmp&lt;/code&gt; is not a place&lt;br&gt;
you put a program, and &lt;code&gt;nohup&lt;/code&gt; does not survive a reboot. I knew both of those&lt;br&gt;
facts and still did this, because the thing I was testing was the publishing&lt;br&gt;
logic, and the publishing logic was fine.&lt;/p&gt;

&lt;p&gt;Nothing reported the loss. The next morning there was simply no new article,&lt;br&gt;
and no error anywhere, because the process that would have logged the error did&lt;br&gt;
not exist.&lt;/p&gt;
&lt;h2&gt;
  
  
  Version 2: launchd with KeepAlive
&lt;/h2&gt;

&lt;p&gt;Moved the script into the repository, wrote a launch agent, &lt;code&gt;KeepAlive: true&lt;/code&gt; so&lt;br&gt;
it comes back if it dies.&lt;/p&gt;

&lt;p&gt;The next morning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scripts/daily-publish.sh: line 59: node: command not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A job started by launchd gets almost no PATH.&lt;/strong&gt; Mine was&lt;br&gt;
&lt;code&gt;/usr/bin:/bin:/usr/sbin:/sbin&lt;/code&gt;. Node is in &lt;code&gt;/opt/homebrew/bin&lt;/code&gt;. When I ran the&lt;br&gt;
same script from my own shell it worked, because my shell had the PATH.&lt;/p&gt;

&lt;p&gt;The part that made this expensive: the same loop also runs a reconciliation step&lt;br&gt;
written in Python, and &lt;code&gt;python3&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; in &lt;code&gt;/usr/bin&lt;/code&gt;. So that step kept working.&lt;br&gt;
The log filled up with successful runs every fifteen minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A partly working automation is harder to notice than a dead one.&lt;/strong&gt; The dead&lt;br&gt;
one has no log. This one had a log that looked healthy.&lt;/p&gt;

&lt;p&gt;Fix is boring:&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;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put it in every script the scheduler touches, not only the one that broke.&lt;br&gt;
One entry point was not the whole story: another script could invoke the same&lt;br&gt;
thing from a different parent.&lt;/p&gt;
&lt;h2&gt;
  
  
  Version 3: the loop was frozen, not running
&lt;/h2&gt;

&lt;p&gt;PATH fixed, publishing worked, and then the log went quiet for three hours.&lt;br&gt;
The process was alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ps -o pid,etime,command -p 72127
  PID     ELAPSED COMMAND
72127    02:55:52 sleep 900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;sleep 900&lt;/code&gt; had been alive for two hours and fifty-five minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a laptop. It sleeps. A &lt;code&gt;sleep&lt;/code&gt; that is supposed to last fifteen minutes&lt;br&gt;
does not necessarily advance while the system is suspended, so every suspend&lt;br&gt;
adds an unbounded gap to a &lt;code&gt;while true; do work; sleep N; done&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;The loop was not broken. It was not stuck on a network call. It was doing&lt;br&gt;
exactly what I wrote, and what I wrote assumed wall-clock time keeps flowing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Version 4: let the scheduler schedule
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;900&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The script now does &lt;strong&gt;one pass and exits.&lt;/strong&gt; No loop, no sleep. &lt;code&gt;StartInterval&lt;/code&gt;&lt;br&gt;
is the mechanism built for this: launchd fires the job on wake if the interval&lt;br&gt;
elapsed while the machine was asleep, and it will not start a second instance of&lt;br&gt;
the same label while one is running, so the lock I would otherwise need is gone.&lt;/p&gt;

&lt;p&gt;I also deleted the old looping script instead of leaving it beside the new one.&lt;br&gt;
Two entry points for the same job means never being sure which one is running.&lt;/p&gt;
&lt;h3&gt;
  
  
  One more trap on the way out
&lt;/h3&gt;

&lt;p&gt;I checked whether the interval had registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ launchctl list com.example.daily
{
    "Label" = "com.example.daily";
    "LastExitStatus" = 0;
    ...
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;StartInterval&lt;/code&gt;. I was one command away from concluding the schedule had&lt;br&gt;
silently failed to load and rewriting it again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ launchctl print gui/$(id -u)/com.example.daily
    state = not running
    runs = 4
    last exit code = 0
    run interval = 900 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;launchctl list&lt;/code&gt; does not print every key it holds.&lt;/strong&gt; &lt;code&gt;launchctl print&lt;/code&gt; does.&lt;br&gt;
The absence of a key in one tool's output is not evidence about the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the four versions have in common
&lt;/h2&gt;

&lt;p&gt;Each fix was correct. Each was verified. &lt;strong&gt;Each verification was performed in an&lt;br&gt;
environment the job would never actually run in.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I tested the publishing logic in my shell, so the reboot never came up&lt;/li&gt;
&lt;li&gt;I tested the launch agent by triggering it once, while my shell's PATH still
hung around in my head as "the" PATH&lt;/li&gt;
&lt;li&gt;I tested the loop by watching one iteration, which is exactly the case where
a sleep that does not advance looks identical to one that does&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule I ended up with is narrow and I have not managed to argue myself out of&lt;br&gt;
it yet:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When you change where something runs, run it once in the new place before&lt;br&gt;
believing anything you knew about it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not a fresh test of the logic. A test in the new environment, of the same logic,&lt;br&gt;
looking for the environment's own failure modes: what the PATH is, what happens&lt;br&gt;
across a reboot, what happens across a suspend, and what the tool you use to&lt;br&gt;
inspect it declines to tell you.&lt;/p&gt;

&lt;p&gt;The fourth version has now survived several suspends and one reboot. That is not&lt;br&gt;
proof, but it is the first version whose evidence came from the place it lives.&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>macos</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Removed an allow Rule and the Permission Was Still There</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:11:11 +0000</pubDate>
      <link>https://dev.to/quintetkit/i-removed-an-allow-rule-and-the-permission-was-still-there-19fh</link>
      <guid>https://dev.to/quintetkit/i-removed-an-allow-rule-and-the-permission-was-still-there-19fh</guid>
      <description>&lt;p&gt;I wanted to stop auto-approving a command, so I deleted its entry from&lt;br&gt;
&lt;code&gt;permissions.allow&lt;/code&gt; in my &lt;code&gt;~/.claude/settings.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was still auto-approved.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not cached, not stale — the entry existed in the project's&lt;br&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; as well, and &lt;strong&gt;list keys are combined rather than&lt;br&gt;
overridden.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Lists merge. Nothing removes another file's entries
&lt;/h2&gt;

&lt;p&gt;When the same list key is set in more than one settings file, Claude Code&lt;br&gt;
&lt;strong&gt;combines&lt;/strong&gt; the lists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ~/.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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;"allow"&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;"Bash(npm run *)"&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="c1"&gt;// .claude/settings.json  (checked into the repository)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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;"allow"&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;"Bash(git commit *)"&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="c1"&gt;// → both apply&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The documented intent is that each file can add entries without removing&lt;br&gt;
another file's. Which is reasonable, and has a consequence worth stating&lt;br&gt;
plainly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No settings file can subtract from another.&lt;/strong&gt; Not a lower level, not a higher&lt;br&gt;
one. Deleting your own entry removes your own entry and nothing else.&lt;/p&gt;

&lt;p&gt;To actually stop something, use &lt;code&gt;deny&lt;/code&gt;, or edit the file that adds it.&lt;/p&gt;

&lt;p&gt;I had spent a while looking for a caching bug.&lt;/p&gt;
&lt;h2&gt;
  
  
  The level order, and the row that is actually useful
&lt;/h2&gt;

&lt;p&gt;Highest first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1  Managed settings          (organisation policy)
2  Command line              (--settings, one session)
3  .claude/settings.local.json
4  .claude/settings.json     (checked in, shared)
5  ~/.claude/settings.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3 sits above 4&lt;/strong&gt;, which I did not expect and now rely on. A value your team&lt;br&gt;
set in the shared file can be put back for you alone in &lt;code&gt;settings.local.json&lt;/code&gt; —&lt;br&gt;
&lt;strong&gt;without editing the file everyone shares.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nothing beats managed. Not &lt;code&gt;--settings&lt;/code&gt;; and &lt;code&gt;--model&lt;/code&gt; picks only from the&lt;br&gt;
models the organisation allows.&lt;/p&gt;
&lt;h2&gt;
  
  
  Environment variables are not a level
&lt;/h2&gt;

&lt;p&gt;They are not in the stack at all, and which one wins is decided &lt;strong&gt;per key&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ANTHROPIC_MODEL           beats the model key in any file
ANTHROPIC_DEFAULT_MODEL   applies only when no file sets model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two variables that both choose a model — one wins, one yields. There is no&lt;br&gt;
general rule to learn here; you check the pair for the key you care about.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;env&lt;/code&gt; block &lt;em&gt;inside&lt;/em&gt; a settings file is an ordinary key and follows the five&lt;br&gt;
levels.&lt;/p&gt;
&lt;h2&gt;
  
  
  Seven keys where a lower level beats managed
&lt;/h2&gt;

&lt;p&gt;For seven security-sensitive keys, Claude Code honours the &lt;strong&gt;more restrictive&lt;/strong&gt;&lt;br&gt;
value from a scope that otherwise could not override managed settings — so a&lt;br&gt;
project file can tighten &lt;code&gt;remoteControlAtStartup&lt;/code&gt; or &lt;code&gt;crossSessionInbound&lt;/code&gt; even&lt;br&gt;
against organisation policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In one direction only.&lt;/strong&gt; A project &lt;code&gt;true&lt;/code&gt; for &lt;code&gt;remoteControlAtStartup&lt;/code&gt; is&lt;br&gt;
ignored: you can tighten from below, never loosen. And &lt;code&gt;enableArtifact&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;cannot be turned back on&lt;/strong&gt; — once anything sets it &lt;code&gt;false&lt;/code&gt;, nothing restores&lt;br&gt;
it, managed settings included.&lt;/p&gt;

&lt;p&gt;Four model-related keys also break the merging rule and take a whole value from&lt;br&gt;
one file instead. Both lists are on the reference page; what is worth carrying&lt;br&gt;
in your head is that &lt;strong&gt;the direction of the exception is not symmetric.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I keep writing these down
&lt;/h2&gt;

&lt;p&gt;Every one of these produces the same experience: you changed the file, the&lt;br&gt;
behaviour did not change, and nothing told you why. The causes are different —&lt;br&gt;
scope, merging, precedence, an environment variable — and the symptom is&lt;br&gt;
identical.&lt;/p&gt;

&lt;p&gt;Which is also why a checker cannot help with this particular one.&lt;br&gt;
&lt;strong&gt;Which files load is decided at run time&lt;/strong&gt;, so precedence is not knowable from&lt;br&gt;
the files in front of you. &lt;code&gt;/status&lt;/code&gt; shows what actually loaded; that is the&lt;br&gt;
tool for this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/status         &lt;span class="c"&gt;# which files were loaded&lt;/span&gt;
claude doctor   &lt;span class="c"&gt;# the resolved settings, and any skipped rules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The five levels, the merge exceptions and the seven keys, in one table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.github.io/en/reference/claude-code-settings-precedence.html" rel="noopener noreferrer"&gt;https://quintetkit.github.io/en/reference/claude-code-settings-precedence.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>configuration</category>
      <category>security</category>
      <category>devops</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>The Workflow Ran With Four Personas Instead of Five and Looked Fine</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Mon, 14 Sep 2026 01:14:16 +0000</pubDate>
      <link>https://dev.to/quintetkit/the-workflow-ran-with-four-personas-instead-of-five-and-looked-fine-2cd7</link>
      <guid>https://dev.to/quintetkit/the-workflow-ran-with-four-personas-instead-of-five-and-looked-fine-2cd7</guid>
      <description>&lt;p&gt;I run Claude Code as five separate subagents with different permissions —&lt;br&gt;
architect, coder, reviewer, conflict resolver, UI designer. One of them stopped&lt;br&gt;
loading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing said so.&lt;/strong&gt; The work still got done, by whoever was left. It got&lt;br&gt;
slower, review got softer, declared scopes stopped being respected. Every one of&lt;br&gt;
those looks like "the model is having a bad day".&lt;/p&gt;

&lt;p&gt;The symptom has the shape of &lt;em&gt;not working well&lt;/em&gt;. The cause was &lt;em&gt;not being&lt;br&gt;
there&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  A file in &lt;code&gt;.claude/agents/&lt;/code&gt; is skipped silently
&lt;/h2&gt;

&lt;p&gt;Five conditions in the frontmatter, any one of which means the file is not&lt;br&gt;
treated as a subagent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Treated as documentation — a note kept beside your agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The opening &lt;code&gt;---&lt;/code&gt; is &lt;strong&gt;not line 1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Read as having no frontmatter, so documentation again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;name&lt;/code&gt; starting with &lt;code&gt;-&lt;/code&gt;, or containing &lt;code&gt;:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The whole file is skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;name&lt;/code&gt; but no &lt;code&gt;description&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML that does not parse&lt;/td&gt;
&lt;td&gt;Nothing is read, file skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;None of these appear in the session.&lt;/strong&gt; The reason goes to the debug log, which&lt;br&gt;
you see with &lt;code&gt;--debug&lt;/code&gt;, which you are not running when things seem fine.&lt;/p&gt;

&lt;p&gt;The second row is the one I hit. I had pasted content into a persona file and&lt;br&gt;
an editor put something above the opening &lt;code&gt;---&lt;/code&gt;. From then on the file was&lt;br&gt;
prose sitting next to four agents, and Claude distributed its work to them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:&lt;/code&gt; is unavailable because it is reserved for plugin-scoped names like&lt;br&gt;
&lt;code&gt;my-plugin:reviewer&lt;/code&gt;. A leading &lt;code&gt;-&lt;/code&gt; is out for the same class of reason.&lt;/p&gt;
&lt;h2&gt;
  
  
  Copying a persona is where this gets interesting
&lt;/h2&gt;

&lt;p&gt;Adding a persona by duplicating an existing one is the obvious move, and&lt;br&gt;
forgetting to change &lt;code&gt;name&lt;/code&gt; is the obvious mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which of two files with the same name loads is not documented.&lt;/strong&gt; It is decided&lt;br&gt;
by filesystem read order.&lt;/p&gt;

&lt;p&gt;So the same repository can run a &lt;strong&gt;different persona on a different machine&lt;/strong&gt;,&lt;br&gt;
and both machines are "working". I do not have a story about being burned by&lt;br&gt;
this one, and I am fairly sure that is luck rather than care.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;claude plugin validate&lt;/code&gt; gets you part of the way
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin validate .claude/agents
claude plugin validate ~/.claude/agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It finds frontmatter that &lt;strong&gt;fails to parse.&lt;/strong&gt; Two limits worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it looks &lt;strong&gt;only at the directory you name&lt;/strong&gt; — user scope is a separate run&lt;/li&gt;
&lt;li&gt;it does &lt;strong&gt;not&lt;/strong&gt; flag a file that parses but has no &lt;code&gt;name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which is most of the list above. It is worth running and it is not sufficient.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one that was my own fault
&lt;/h2&gt;

&lt;p&gt;My coder persona had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;meaning "this one gets everything". &lt;code&gt;tools&lt;/code&gt; accepts exact tool names or the&lt;br&gt;
server patterns &lt;code&gt;mcp__&amp;lt;server&amp;gt;&lt;/code&gt; / &lt;code&gt;mcp__&amp;lt;server&amp;gt;__*&lt;/code&gt;. &lt;strong&gt;A bare &lt;code&gt;*&lt;/code&gt; is not among&lt;br&gt;
them&lt;/strong&gt;, and an entry resolving to nothing makes Claude Code refuse to launch the&lt;br&gt;
subagent.&lt;/p&gt;

&lt;p&gt;The documented way to say "everything" is to &lt;strong&gt;omit the field.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why my own linter could not see that&lt;/strong&gt; is written up separately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.github.io/en/articles/blind-spot-from-refusing-to-check.html" rel="noopener noreferrer"&gt;https://quintetkit.github.io/en/articles/blind-spot-from-refusing-to-check.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The short version is that the rule I wrote to avoid false positives was phrased&lt;br&gt;
as "don't look at the &lt;code&gt;tools&lt;/code&gt; field".&lt;/p&gt;

&lt;p&gt;That one is worse than a missing persona, because &lt;code&gt;coder&lt;/code&gt; is the one that writes&lt;br&gt;
the code. The other four keep working, so it presents as &lt;strong&gt;the coder being&lt;br&gt;
unreliable today.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;p&gt;Two checks, and I only run them twice: after setup, and after adding or copying&lt;br&gt;
a persona.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @quintetkit/ccheck        &lt;span class="c"&gt;# the five conditions, duplicate names, tools: *&lt;/span&gt;
claude &lt;span class="nt"&gt;--debug&lt;/span&gt;                &lt;span class="c"&gt;# once, to see what was skipped and why&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a habit that is cheaper than either: &lt;strong&gt;count them.&lt;/strong&gt; If the configuration&lt;br&gt;
says five personas, the thing you want to confirm is that five loaded — not that&lt;br&gt;
the work is getting done. The work getting done was never the question.&lt;/p&gt;

&lt;p&gt;The five conditions and every frontmatter field, in one table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.github.io/en/reference/claude-code-subagent-frontmatter.html" rel="noopener noreferrer"&gt;https://quintetkit.github.io/en/reference/claude-code-subagent-frontmatter.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Set a 500ms Timeout and Got 28 Hours</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Sat, 12 Sep 2026 01:16:42 +0000</pubDate>
      <link>https://dev.to/quintetkit/i-set-a-500ms-timeout-and-got-28-hours-20p3</link>
      <guid>https://dev.to/quintetkit/i-set-a-500ms-timeout-and-got-28-hours-20p3</guid>
      <description>&lt;p&gt;I had an MCP server that occasionally hung, so I gave it a short leash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"flaky"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;timeout&lt;/code&gt; is in milliseconds, so that reads as half a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values below 1000 are ignored.&lt;/strong&gt; The call falls through to&lt;br&gt;
&lt;code&gt;MCP_TOOL_TIMEOUT&lt;/code&gt;, and if that is unset, to its default — &lt;strong&gt;about 28 hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had written the tightest limit I could and produced the loosest one available.&lt;/p&gt;

&lt;p&gt;Before v2.1.162 a value under 1000 was floored to one second, which is the&lt;br&gt;
behaviour I had in my head. &lt;strong&gt;It changed, and nothing in my configuration&lt;br&gt;
changed with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three related things I only learned by reading the whole section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;timeout&lt;/code&gt; is a &lt;strong&gt;hard wall-clock limit per tool call.&lt;/strong&gt; Progress notifications
from the server do not extend it&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;timeout&lt;/code&gt; of at least 1000 also acts as a &lt;strong&gt;floor on the idle timeout&lt;/strong&gt; —
calls are never aborted for idleness sooner than that&lt;/li&gt;
&lt;li&gt;stdio and WebSocket servers have &lt;strong&gt;no per-request timer at all&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  A &lt;code&gt;url&lt;/code&gt; with no &lt;code&gt;type&lt;/code&gt; is read as a stdio server
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"example"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.example.com/mcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is no defaulting to http. &lt;strong&gt;An entry with no &lt;code&gt;type&lt;/code&gt; is a stdio server&lt;/strong&gt;,&lt;br&gt;
stdio requires &lt;code&gt;command&lt;/code&gt;, and so the server is skipped as a configuration error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP server "example" has a "url" but no "type"; add "type": "http" (or "sse" / "ws") to this entry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message exists from v2.1.202. Before it, the same mistake reported&lt;br&gt;
&lt;code&gt;command: expected string, received undefined&lt;/code&gt; — &lt;strong&gt;you wrote a url and were told&lt;br&gt;
about a command&lt;/strong&gt;, which is a long way from the cause.&lt;/p&gt;

&lt;p&gt;Under &lt;code&gt;--output-format stream-json&lt;/code&gt; it also appears in the &lt;code&gt;system/init&lt;/code&gt; event's&lt;br&gt;
&lt;code&gt;mcp_server_errors&lt;/code&gt;, so a script can detect that a server never loaded. That is&lt;br&gt;
the only machine-readable path I have found for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  A cloned repository cannot approve its own servers
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time, because it looks like a permissions bug.&lt;/p&gt;

&lt;p&gt;Servers from &lt;code&gt;.mcp.json&lt;/code&gt; need approval. You can commit&lt;br&gt;
&lt;code&gt;enableAllProjectMcpServers&lt;/code&gt; or &lt;code&gt;enabledMcpjsonServers&lt;/code&gt; to the project's&lt;br&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; — and in an &lt;strong&gt;untrusted folder they are ignored.&lt;/strong&gt; The&lt;br&gt;
server sits at &lt;code&gt;⏸ Pending approval&lt;/code&gt;, never connected, never health-checked.&lt;/p&gt;

&lt;p&gt;Which is correct, and obvious once stated: &lt;strong&gt;a repository that could approve its&lt;br&gt;
own MCP servers would be a way to run code on anyone who clones it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three sources of approval still apply in an untrusted folder: your own&lt;br&gt;
&lt;code&gt;~/.claude/settings.json&lt;/code&gt;, managed settings, and settings passed with&lt;br&gt;
&lt;code&gt;--settings&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The pattern across all of these
&lt;/h2&gt;

&lt;p&gt;Every one of them is a configuration that &lt;strong&gt;loads&lt;/strong&gt;. No error, no warning you&lt;br&gt;
would notice, and a running system that is doing something other than what the&lt;br&gt;
file says.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;timeout: 500&lt;/code&gt; is the sharpest example: the file says half a second, the system&lt;br&gt;
says a day and change, and &lt;strong&gt;both are working as designed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I want from a configuration checker, having been on the wrong end of these,&lt;br&gt;
is not more rules. It is that the ones it does have never fire on something&lt;br&gt;
correct — because the reason I stopped reading startup output in the first place&lt;br&gt;
was that nothing there had ever mattered, and by the time something did, I had&lt;br&gt;
already trained myself past it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @quintetkit/ccheck
&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;error .mcp.json:3
      Server "example" has a `url` but no `type`. Add `"type": "http"` (or "sse" / "ws").
warn  .mcp.json:9
      Server "flaky": `timeout: 500` is in milliseconds; values below 1000 are ignored.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full set, including the required field per transport:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.github.io/en/reference/claude-code-mcp-json.html" rel="noopener noreferrer"&gt;https://quintetkit.github.io/en/reference/claude-code-mcp-json.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>configuration</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Deny Rule I Wrote Was Never Consulted</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Fri, 11 Sep 2026 01:03:22 +0000</pubDate>
      <link>https://dev.to/quintetkit/the-deny-rule-i-wrote-was-never-consulted-olp</link>
      <guid>https://dev.to/quintetkit/the-deny-rule-i-wrote-was-never-consulted-olp</guid>
      <description>&lt;p&gt;I had a rule meant to keep an agent inside one directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&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;"deny"&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;"Write(src/generated/**)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is valid. It loads. &lt;strong&gt;It is never consulted.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;File path rules apply to &lt;code&gt;Read(...)&lt;/code&gt; and &lt;code&gt;Edit(...)&lt;/code&gt;. Written for &lt;code&gt;Write&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;NotebookEdit&lt;/code&gt;, &lt;code&gt;Glob&lt;/code&gt;, or the legacy &lt;code&gt;MultiEdit&lt;/code&gt;, the rule is accepted and then&lt;br&gt;
never looked at. There is a startup warning; I had not been reading startup&lt;br&gt;
output, because nothing had ever been wrong in it before.&lt;/p&gt;

&lt;p&gt;That was the cheap one. Here is the expensive one.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;Bash(git * main)&lt;/code&gt; allows every git subcommand
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;*&lt;/code&gt; in a Bash rule stands in for &lt;strong&gt;whatever text is in its place&lt;/strong&gt;. So the&lt;br&gt;
earlier it sits, the wider the rule is.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You write&lt;/th&gt;
&lt;th&gt;What it actually allows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bash(git log *)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;commands starting &lt;code&gt;git log&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bash(git * main)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;every git subcommand&lt;/strong&gt; — &lt;code&gt;git push origin main&lt;/code&gt; included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bash(* --version)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;every program on the machine&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Bash(git * main)&lt;/code&gt; reads like "one git command, on main". It permits&lt;br&gt;
&lt;code&gt;git push origin main&lt;/code&gt;. It also permits &lt;code&gt;git -c core.pager=… diff main&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;-c&lt;/code&gt; &lt;strong&gt;makes git run a program you name.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wrote &lt;code&gt;Bash(git * main)&lt;/code&gt; thinking I had allowed a diff.&lt;/p&gt;

&lt;p&gt;There is a startup warning for an allow rule with a wildcard before the rest of&lt;br&gt;
the command. Same as above: the rule still applies while the warning scrolls by.&lt;/p&gt;
&lt;h2&gt;
  
  
  Whitespace decides whether &lt;code&gt;lsof&lt;/code&gt; is allowed
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash(ls *)   matches  ls -la,  ls        does not match  lsof
Bash(ls*)    matches  ls -la,  lsof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;The space before a trailing &lt;code&gt;*&lt;/code&gt; is part of the rule.&lt;/strong&gt; And &lt;code&gt;Bash(ls *)&lt;/code&gt;&lt;br&gt;
matches bare &lt;code&gt;ls&lt;/code&gt; only because that trailing &lt;code&gt;*&lt;/code&gt; is the rule's &lt;em&gt;only&lt;/em&gt; wildcard —&lt;br&gt;
&lt;code&gt;Bash(* --help *)&lt;/code&gt; matches &lt;code&gt;npm --help x&lt;/code&gt; but not &lt;code&gt;npm --help&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three more that load and do nothing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An unanchored allow glob is skipped.&lt;/strong&gt; &lt;code&gt;"*"&lt;/code&gt;, &lt;code&gt;"B*"&lt;/code&gt;, &lt;code&gt;"mcp__*"&lt;/code&gt; in &lt;code&gt;allow&lt;/code&gt;&lt;br&gt;
auto-approve nothing. Allow rules accept a glob only after a literal&lt;br&gt;
&lt;code&gt;mcp__&amp;lt;server&amp;gt;__&lt;/code&gt; prefix, because the rule has to name a server you configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A parameter rule on a tool's own content field is ignored.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;Bash(command:rm *)&lt;/code&gt; is bypassable with a compound command, so it does not&lt;br&gt;
apply. The fields you cannot match this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash, PowerShell  → command        Grep, Glob   → path
Read, Edit, Write → file_path      WebFetch     → url
NotebookEdit      → notebook_path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write &lt;code&gt;Bash(rm *)&lt;/code&gt;, &lt;code&gt;Read(./.env)&lt;/code&gt;, &lt;code&gt;WebFetch(domain:host)&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An &lt;code&gt;mcp__&lt;/code&gt; rule with parentheses is skipped at load.&lt;/strong&gt; &lt;code&gt;mcp__memory(read)&lt;/code&gt;&lt;br&gt;
never applies. It is listed in the invalid-settings dialog and in&lt;br&gt;
&lt;code&gt;claude doctor&lt;/code&gt; — neither of which you see in CI.&lt;/p&gt;

&lt;p&gt;And one that is only a syntax trap: &lt;code&gt;Bash(ls:*)&lt;/code&gt; is the same as &lt;code&gt;Bash(ls *)&lt;/code&gt;,&lt;br&gt;
but &lt;strong&gt;only as a suffix.&lt;/strong&gt; In &lt;code&gt;Bash(git:* push)&lt;/code&gt; the colon is a literal&lt;br&gt;
character and the rule matches nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bites harder than it looks
&lt;/h2&gt;

&lt;p&gt;If you are running several agents in parallel, reduced permissions are usually&lt;br&gt;
&lt;em&gt;the&lt;/em&gt; safety design. Mine was: the implementing agent may only touch the files&lt;br&gt;
its Issue declared.&lt;/p&gt;

&lt;p&gt;I had expressed that as &lt;code&gt;Write(...)&lt;/code&gt; path rules. &lt;strong&gt;The mechanism I was relying on&lt;br&gt;
was the one that is never consulted.&lt;/strong&gt; What was actually holding scope was the&lt;br&gt;
&lt;code&gt;Read&lt;/code&gt;/&lt;code&gt;Edit&lt;/code&gt; rules, the declared scope in the Issue text, and the wording of&lt;br&gt;
the request — none of which I had been treating as the load-bearing part.&lt;/p&gt;

&lt;p&gt;The lesson I took is narrower than "read the docs":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A permission rule that is doing nothing looks exactly like one that is&lt;br&gt;
working.&lt;/strong&gt; Both are silent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Checking a config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @quintetkit/ccheck
&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;warn  .claude/settings.json:14
      `Write(src/generated/**)` is accepted but never consulted. File path rules
      apply only to `Read(...)` and `Edit(...)`.
      why: https://code.claude.com/docs/en/permissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building that checker, the tests were the interesting part. It runs the &lt;strong&gt;exact&lt;br&gt;
rule strings from the documentation&lt;/strong&gt; — six that must produce a finding, and&lt;br&gt;
&lt;strong&gt;eleven that must not&lt;/strong&gt;, including &lt;code&gt;Read(./src/**/*.ts)&lt;/code&gt; and &lt;code&gt;Bash(ls*)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Eleven-to-six, deliberately. Permission rules have many valid shapes, so calling&lt;br&gt;
a correct one broken is the likely failure, and one false finding is enough for&lt;br&gt;
the whole output to stop being read.&lt;/p&gt;

&lt;p&gt;The five forms, with what each does instead of what it looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.github.io/en/reference/claude-code-permission-rules.html" rel="noopener noreferrer"&gt;https://quintetkit.github.io/en/reference/claude-code-permission-rules.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>I Put a Setting in My Project Config and It Did Nothing</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Thu, 10 Sep 2026 01:48:41 +0000</pubDate>
      <link>https://dev.to/quintetkit/i-put-a-setting-in-my-project-config-and-it-did-nothing-2id2</link>
      <guid>https://dev.to/quintetkit/i-put-a-setting-in-my-project-config-and-it-did-nothing-2id2</guid>
      <description>&lt;p&gt;I put &lt;code&gt;autoMode&lt;/code&gt; in &lt;code&gt;.claude/settings.json&lt;/code&gt;, committed it, and it had no effect.&lt;/p&gt;

&lt;p&gt;The JSON was valid. Startup printed no warning. &lt;code&gt;--debug&lt;/code&gt; said nothing. I spent&lt;br&gt;
a while assuming I had the semantics wrong before I found the actual reason:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;that key does not apply from that file.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Settings have a scope, and the scope is not in the file
&lt;/h2&gt;

&lt;p&gt;Claude Code's settings reference has a column I had never read carefully. Every&lt;br&gt;
key names the scopes it applies from. Counting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Any file                  152 keys
Managed                    39 keys   only from managed settings
User or managed            23 keys   ~/.claude/settings.json, or managed
User, local, or managed     3 keys   settings.local.json but not settings.json
Global config               6 keys   ~/.claude.json only
------------------------------------
                          223 keys, of which 71 are scoped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;71 of 223.&lt;/strong&gt; Put one of those in a project's &lt;code&gt;.claude/settings.json&lt;/code&gt; and it is&lt;br&gt;
not an error, produces no warning, and does nothing.&lt;/p&gt;

&lt;p&gt;The ones I would expect people to hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;autoMode&lt;/span&gt;                          &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;autoMode.classifyAllShell&lt;/span&gt;         &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;skipAutoPermissionPrompt&lt;/span&gt;          &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;useAutoModeDuringPlan&lt;/span&gt;             &lt;span class="err"&gt;user,&lt;/span&gt; &lt;span class="err"&gt;local,&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;spellcheck&lt;/span&gt;                        &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;sandbox.network.strictAllowlist&lt;/span&gt;   &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;sandbox.filesystem.disabled&lt;/span&gt;       &lt;span class="err"&gt;user&lt;/span&gt; &lt;span class="err"&gt;or&lt;/span&gt; &lt;span class="err"&gt;managed&lt;/span&gt;
&lt;span class="err"&gt;diffTool&lt;/span&gt;                          &lt;span class="err"&gt;~/.claude.json&lt;/span&gt; &lt;span class="err"&gt;only&lt;/span&gt;
&lt;span class="err"&gt;autoConnectIde&lt;/span&gt;                    &lt;span class="err"&gt;~/.claude.json&lt;/span&gt; &lt;span class="err"&gt;only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run several agents at once you get more approval prompts, so loosening&lt;br&gt;
&lt;code&gt;autoMode&lt;/code&gt; is the obvious move — and the obvious place to put it is the project&lt;br&gt;
config you already have open. That is the one place it does not work.&lt;/p&gt;
&lt;h2&gt;
  
  
  The row that will cost you a debugging session
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User, local, or managed     3 keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;settings.local.json&lt;/code&gt; is a &lt;strong&gt;different scope&lt;/strong&gt; from &lt;code&gt;settings.json&lt;/code&gt;. Three keys&lt;br&gt;
apply from the local one and not from the shared one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useAutoModeDuringPlan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;syncClaudeAiSkills&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skipDangerousModePermissionPrompt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: you put one in &lt;code&gt;settings.local.json&lt;/code&gt;, it works. Later you move it into the&lt;br&gt;
shared file so the team gets it, and it silently stops. &lt;strong&gt;Nothing changed except&lt;br&gt;
which file it lives in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a manufactured "works on my machine" — with the unusual property that&lt;br&gt;
the person who wrote it is the one for whom it works.&lt;/p&gt;
&lt;h2&gt;
  
  
  Nested keys are scoped by their full dotted name
&lt;/h2&gt;

&lt;p&gt;The documentation names them the way you would write a path, not the way you&lt;br&gt;
would write JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sandbox.network.strictAllowlist        ← this is what is scoped
sandbox                                ← this is not
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which matters if you go looking. I wrote a checker for this and inspected only&lt;br&gt;
the top level of the settings object, so this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sandbox"&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;"network"&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;"strictAllowlist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;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;produced nothing at all. &lt;strong&gt;20 dotted keys were invisible to it, 12 of them&lt;br&gt;
&lt;code&gt;sandbox.*&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How I found out how wrong I was
&lt;/h2&gt;

&lt;p&gt;I had been maintaining this list by hand, copied out of the documentation, with&lt;br&gt;
a comment I wrote myself:&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="cm"&gt;/** そのファイルからは効かないスコープのキー（一部。確実なものだけ） */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Some. Only the certain ones." I knew it was partial. I did not know how partial.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             actual   I had
Managed          39      14
User or managed  23       0     ← the whole row
Global config     6       6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The &lt;code&gt;user or managed&lt;/code&gt; row was missing entirely — 23 keys, including&lt;br&gt;
&lt;code&gt;autoMode&lt;/code&gt;, the one that started this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had read the table by grouping it: &lt;code&gt;Managed&lt;/code&gt; is for organisations deploying&lt;br&gt;
policy, which is not my situation, so I skipped past it — and past the row&lt;br&gt;
sitting next to it, which turned out to mean &lt;em&gt;"applies from your own&lt;br&gt;
&lt;code&gt;~/.claude/settings.json&lt;/code&gt;"&lt;/em&gt; and was the row I most needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading it row by row would have worked. Reading it by category is what&lt;br&gt;
failed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So it is generated now, straight from the scope column, and the count comes out&lt;br&gt;
of the same place the checker reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking yours
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @quintetkit/ccheck
&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="go"&gt;warn  .claude/settings.json:63
      `autoMode` applies from user or managed settings only. It has no effect from this file.
      why: https://code.claude.com/docs/en/settings-reference
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing that took a second pass to get right: &lt;code&gt;~/.claude/settings.json&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;is&lt;/strong&gt; the user scope. Deciding "this key doesn't apply here" from the path alone&lt;br&gt;
would report a correctly placed &lt;code&gt;autoMode&lt;/code&gt; on the machine of anyone who runs it&lt;br&gt;
against their home directory — including mine, which has &lt;code&gt;autoMode&lt;/code&gt; in it. So&lt;br&gt;
the scope is decided by whether the directory being scanned is your home, not by&lt;br&gt;
what the path looks like.&lt;/p&gt;

&lt;p&gt;The full list of 71, and the same list as JSON if you would rather check the&lt;br&gt;
claim than trust it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.github.io/en/reference/claude-code-settings-scope.html" rel="noopener noreferrer"&gt;https://quintetkit.github.io/en/reference/claude-code-settings-scope.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>configuration</category>
      <category>devops</category>
      <category>ai</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Every Test Passed and It Could Not Read a Single Real Record</title>
      <dc:creator>quintetkit</dc:creator>
      <pubDate>Wed, 09 Sep 2026 01:26:53 +0000</pubDate>
      <link>https://dev.to/quintetkit/every-test-passed-and-it-could-not-read-a-single-real-record-59pn</link>
      <guid>https://dev.to/quintetkit/every-test-passed-and-it-could-not-read-a-single-real-record-59pn</guid>
      <description>&lt;p&gt;I wrote a tool that reads GitHub Issues and reports the ones whose declared file&lt;br&gt;
scopes overlap.&lt;/p&gt;

&lt;p&gt;Eight fixtures, all six rules covered, every test green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run against a real repository, it read zero Issues.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;Eleven Issues, one identical complaint for each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error #21  The scope section is present but declares no path.
error #19  The scope section is present but declares no path.
error #16  The scope section is present but declares no path.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The section is there. The contents are not being read.&lt;/p&gt;

&lt;p&gt;Here is what a real Issue looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Scope&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;README.md, .github/workflows/self-check.yml
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inside a code block, on one line, comma separated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My parser treated each line inside a code block as one path, then discarded&lt;br&gt;
anything containing whitespace on the grounds that a path has none.&lt;br&gt;
&lt;code&gt;README.md, .github/...&lt;/code&gt; contains a space, so every one of them was thrown away.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the fixtures said
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Scope&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`src/auth/**`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`src/routes/login.tsx`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;A bullet list.&lt;/strong&gt; One path per line, in backticks.&lt;/p&gt;

&lt;p&gt;Why did I write them that way? &lt;strong&gt;Because that is how I assumed people write them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also wrote the Issue template those real Issues came from. &lt;strong&gt;My tool could not&lt;br&gt;
read the format I designed myself.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why no test failed
&lt;/h2&gt;

&lt;p&gt;This is the part worth keeping.&lt;/p&gt;

&lt;p&gt;The fixtures were written by the same person who wrote the parser, on the same&lt;br&gt;
day, holding the same assumption.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parser      implemented assuming "scopes are written as a bullet list"
fixtures    written assuming "scopes are written as a bullet list"
test        the two agree, so it passes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The assumption is duplicated on both sides, so the test cannot examine it.&lt;/strong&gt;&lt;br&gt;
What it examines is that the implementation matches the fixtures.&lt;/p&gt;

&lt;p&gt;This is not an implementation bug. It is &lt;strong&gt;a bug in how the tests were designed&lt;/strong&gt;,&lt;br&gt;
and no amount of coverage finds it. More coverage adds more paths — &lt;strong&gt;through the&lt;br&gt;
same wrong premise.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix is not "write more tests"
&lt;/h2&gt;

&lt;p&gt;Ten fixtures instead of eight would still be ten fixtures written by me, holding&lt;br&gt;
the same assumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add one input you did not write.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api &lt;span class="s2"&gt;"repos/owner/name/issues?state=closed&amp;amp;per_page=100"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'[.[] | select(.pull_request|not) | {number,title,body}]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/fixtures/real-issues.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then pin it as a regression test:&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="c1"&gt;// The real Scope sections were a single line inside a code block. The parser&lt;/span&gt;
&lt;span class="c1"&gt;// as written read none of them and called every Issue scope-empty.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&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;span class="nf"&gt;readFileSync&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;F&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;real-issues.json`&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="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;({&lt;/span&gt; &lt;span class="na"&gt;id&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&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="na"&gt;title&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="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&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="nx"&gt;body&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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;real&lt;/span&gt;&lt;span class="p"&gt;,&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="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scope-empty&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;reads a Scope written inside a code block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That test sits outside my assumptions&lt;/strong&gt;, because I did not author the data. Past&lt;br&gt;
reality did.&lt;/p&gt;

&lt;p&gt;The parser side:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inFence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Inside a code block these are usually listed on one line, comma or space&lt;/span&gt;
  &lt;span class="c1"&gt;// separated. Real Issues were written that way, and treating the line as a&lt;/span&gt;
  &lt;span class="c1"&gt;// single path read none of them.&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;tok&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;line&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\s]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="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;tok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;continue&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;h2&gt;
  
  
  The same shape again, the same day
&lt;/h2&gt;

&lt;p&gt;The README said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Requires Node 22.6 or newer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CI failed.&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;SyntaxError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cannot&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="nx"&gt;outside&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;"type": "module"&lt;/code&gt;, Node loads a &lt;code&gt;.ts&lt;/code&gt; file as CommonJS. &lt;strong&gt;It worked&lt;br&gt;
locally because the local Node is 25.&lt;/strong&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;README    claims it runs on 22.6
CI        verifies on 25
verified      that it runs on 25
not verified  that it runs on 22.6 — which is the claim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The claim and the place it was checked were not the same place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix is to test at the boundary you claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;22.18"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;24"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(22.6 turned out to need a flag for type stripping, so the floor moved to 22.18.&lt;br&gt;
&lt;strong&gt;I only looked that up because CI failed.&lt;/strong&gt; Had it passed, the claim would have&lt;br&gt;
shipped false.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalising
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data you produce yourself reproduces your assumptions along with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The exposure is highest here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anything that interprets input from outside (parsers, importers, webhook receivers)&lt;/li&gt;
&lt;li&gt;anything handling text other people wrote&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;any format you defined yourself&lt;/strong&gt; — the worst case, because you believe people
write it the way you specified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule covers it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Anything that interprets input gets one test with input you did not write.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Take one real record, freeze it, make it a regression test. That is one foothold&lt;br&gt;
outside your own assumptions.&lt;/p&gt;

&lt;p&gt;And for claims:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verify at the boundary you claim.&lt;/strong&gt; If it says "N or newer", test on N.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;Fixtures &lt;strong&gt;carry the same assumptions as the implementation&lt;/strong&gt;, so they cannot test
those assumptions&lt;/li&gt;
&lt;li&gt;Coverage does not help. &lt;strong&gt;It adds paths through the wrong premise&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add one input you did not write.&lt;/strong&gt; Take it from production and freeze it&lt;/li&gt;
&lt;li&gt;The riskiest format is &lt;strong&gt;the one you defined&lt;/strong&gt;, because you trust it is followed&lt;/li&gt;
&lt;li&gt;A README claim is &lt;strong&gt;unverified until it is tested at the boundary it claims&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;I only checked because CI broke. &lt;strong&gt;Had it stayed green, the false claim would have shipped&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/treating-it-worked-as-verification-ships-code-that-only-works-with-one-commit-2bk3"&gt;Treating "It Worked" as Verification Ships Code That Only Works With One Commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/quintetkit/what-you-refuse-to-check-decides-the-quality-of-a-linter-5dbb"&gt;What You Refuse to Check Decides the Quality of a Linter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://quintetkit.github.io/en/articles/one-decision-two-places.html" rel="noopener noreferrer"&gt;Write the Same Decision in Two Places and Only One of Them Gets Fixed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I publish the configuration for splitting Claude Code into separate personas —&lt;br&gt;
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run&lt;br&gt;
&lt;code&gt;./setup.sh&lt;/code&gt;, and it works. It does not depend on your tech stack.&lt;/p&gt;

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

&lt;p&gt;I built one real tool using nothing but this workflow. Every Issue, PR, review&lt;br&gt;
and merge is still there. &lt;strong&gt;The parts that went wrong were not deleted.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The version that adds a UI Designer persona, review criteria, a per-Issue&lt;br&gt;
parallel execution script and a 11-chapter guide is on the&lt;br&gt;
&lt;a href="https://quintetkit.github.io/index.en.html" rel="noopener noreferrer"&gt;product page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full kit — five personas, the scripts and the complete guide — is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://quintetkit.gumroad.com/l/quintet" rel="noopener noreferrer"&gt;https://quintetkit.gumroad.com/l/quintet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>typescript</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
