<?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: LazyDev_OH</title>
    <description>The latest articles on DEV Community by LazyDev_OH (@lazydev_oh).</description>
    <link>https://dev.to/lazydev_oh</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868972%2F81efd9f9-64e0-4189-93c4-9a8b3a18fff8.png</url>
      <title>DEV Community: LazyDev_OH</title>
      <link>https://dev.to/lazydev_oh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lazydev_oh"/>
    <language>en</language>
    <item>
      <title>Teaching Claude to Play Tetris with 100 App Store Characters</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Wed, 15 Apr 2026 17:30:08 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/teaching-claude-to-play-tetris-with-100-app-store-characters-3o9k</link>
      <guid>https://dev.to/lazydev_oh/teaching-claude-to-play-tetris-with-100-app-store-characters-3o9k</guid>
      <description>&lt;p&gt;The App Store keyword field is exactly 100 characters. Commas only, no spaces, no duplicates. You need to pack 15–20 keywords inside.&lt;/p&gt;

&lt;p&gt;I tried writing those by hand for a dozen apps. Every time I'd leave characters on the table — a rogue space after a comma, a singular/plural duplicate Apple would auto-match anyway. Manual packing is tedious enough that most indie developers just don't iterate on ASO.&lt;/p&gt;

&lt;p&gt;So I built an AI that does it. This post is the actual implementation — prompts, JSON schemas, validation, and the gotchas that killed my first three attempts. I ship this in my ASO tool for iOS developers (&lt;a href="https://apsity.com" rel="noopener noreferrer"&gt;Apsity&lt;/a&gt;), but the approach works for any tight-constraint text-generation problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraints That Break Generic LLMs
&lt;/h2&gt;

&lt;p&gt;When you ask any LLM "generate App Store keywords for my budget app," you get something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;budget tracker, expense manager, spending analysis,
money manager, personal finance, bill tracker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Readable. Useless. Two characters wasted on every &lt;code&gt;,&lt;/code&gt; (space after comma). Four characters wasted on &lt;code&gt;personal finance&lt;/code&gt; because Apple auto-matches &lt;code&gt;personal&lt;/code&gt; + &lt;code&gt;finance&lt;/code&gt; separately. Total wasted: roughly 30% of your 100.&lt;/p&gt;

&lt;p&gt;The rules that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exactly ≤100 characters&lt;/strong&gt; (including commas)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single comma separators, no spaces&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No duplicate tokens&lt;/strong&gt; (Apple ignores them anyway)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No singular+plural pairs&lt;/strong&gt; (Apple auto-matches)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shorter tokens &amp;gt; compound words&lt;/strong&gt; (Apple combines them for you)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No competitor brand names&lt;/strong&gt; (trademark rejection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No category names, and no &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;free&lt;/code&gt;, &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;best&lt;/code&gt;, &lt;code&gt;iPhone&lt;/code&gt;, &lt;code&gt;iPad&lt;/code&gt;&lt;/strong&gt; (Apple auto-indexes all of these)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mix function + situation + alternative keywords&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An LLM without these constraints spelled out won't enforce them. Generic "write keywords" prompts fail rules 1–4 consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Claude Sonnet
&lt;/h2&gt;

&lt;p&gt;I tested GPT-5, Gemini 2.0 Pro, and Claude Sonnet 4.6 on the same task. Three metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Character compliance&lt;/strong&gt; — stays under 100 chars without excess whitespace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON schema adherence&lt;/strong&gt; — returns exactly the structured output I asked for&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge case handling&lt;/strong&gt; — catches duplicates, plural forms, category name leaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Sonnet won on all three, but the meaningful gap was edge case handling. When I explicitly said "no duplicates including singular/plural pairs," Claude filtered them out. The others listed &lt;code&gt;budget&lt;/code&gt; and &lt;code&gt;budgets&lt;/code&gt; and called it done — which is wrong, because Apple's algorithm auto-indexes plurals from the singular form anyway. A keyword duplicated across singular/plural just wastes characters.&lt;/p&gt;

&lt;p&gt;I'm also passing a lot of context — competitor review snippets, current rankings, market-specific search trends. Sonnet 4.6's 1M-token context window handles it without trimming.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt Structure
&lt;/h2&gt;

&lt;p&gt;The prompt is in three layers: system prompt (the rules), user prompt (the app context), and a JSON schema Claude must match.&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;// lib/keyword-generator.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&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;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
You are an ASO (App Store Optimization) keyword specialist.
Generate keywords for the app store "Keywords" field, which has
a STRICT 100-character limit. Characters include commas.

Rules (apply in order):
1. Total output length MUST be ≤100 characters
2. Use ONLY commas as separators, no spaces after commas
3. No duplicate tokens
4. No singular+plural pairs (Apple auto-matches both)
5. Prefer short atomic tokens over compound words
   (Apple combines A + B into "A B" automatically)
6. No competitor brand names (trademark violation)
7. No category names and no words Apple already indexes automatically:
   app, free, new, best, iPhone, iPad, or any category label
8. Blend three keyword types:
   - Function (what the app does)
   - Situation (when users need it)
   - Alternative (different names for the same thing)

Return JSON with this schema:
{
  "keywords": string[],         // individual tokens, no commas inside
  "joined": string,             // comma-joined, must be ≤100 chars
  "char_count": number,         // .length of "joined"
  "coverage_notes": string[]    // which search queries this covers
}
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;KeywordOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;char_count&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="nl"&gt;coverage_notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON schema isn't just for structure. &lt;code&gt;char_count&lt;/code&gt; forces Claude to count the output itself — models aren't great at counting, but self-reporting forces a pass where the model checks its own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Keywords
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateKeywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;app_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;competitors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;existing_keywords&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;target_market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KeywordOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
App: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Description: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Target market: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_market&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Competitor apps (do NOT use these names): &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;competitors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;existing_keywords&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Currently underperforming keywords to replace: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;existing_keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

Generate an optimal 100-character keyword field.
Before finalizing, count your characters and confirm it fits.
`&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;:&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\{[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No JSON in response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;KeywordOutput&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;Straightforward Anthropic SDK call. Two things worth noting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_tokens: 1024&lt;/code&gt;&lt;/strong&gt; — keywords are short, so we don't need more. Capping reduces cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON extraction via regex&lt;/strong&gt; — Claude sometimes wraps JSON in explanation text. Grabbing the first &lt;code&gt;{...}&lt;/code&gt; block is more reliable than asking for raw JSON.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Validation Is Where Production Code Lives
&lt;/h2&gt;

&lt;p&gt;Claude gets the constraints right ~85% of the time. Production code has to handle the other 15%.&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;// lib/validate-keywords.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&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;KeywordSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="na"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;char_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;coverage_notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateKeywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;KeywordOutput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;KeywordSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid JSON shape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;char_count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Length check&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;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;issues&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="s2"&gt;`joined is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; chars, exceeds 100`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Trust but verify char_count&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;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;char_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;issues&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="s2"&gt;`char_count mismatch: claimed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;char_count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, actual &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Commas only, no spaces&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;joined&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;, &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;issues&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contains ', ' — spaces after commas waste characters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Reconstruct and compare&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reconstructed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reconstructed&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;issues&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keywords array doesn't match joined string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Duplicate detection (case-insensitive)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;issues&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="s2"&gt;`duplicate token: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 6. Singular/plural detection (basic)&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;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plural&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;s&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;singular&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/s$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plural&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;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;plural&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;issues&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="s2"&gt;`singular/plural pair: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&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;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;When validation fails, I retry with the specific issue appended to the prompt:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeywordContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KeywordOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;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;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed after 3 attempts&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateKeywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateKeywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Feed issues back to Claude for a targeted retry&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generateWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;existing_keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Add validation issues into a correction prompt here&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, 94% succeed on the first attempt, 5% on the second, 1% fall through (usually when the concept genuinely can't fit in 100 chars — time to simplify the app description, not the prompt).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Output Nobody Asks For But Everyone Needs
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;coverage_notes&lt;/code&gt; field in the schema looks optional. It's the most useful part.&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;"keywords"&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;"budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"expense"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"payday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"wallet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"debt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"bills"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"money"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"savings"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"joined"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"budget,expense,payday,wallet,debt,bills,money,savings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"char_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coverage_notes"&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="s2"&gt;"Matches: 'budget', 'expense tracker', 'payday planner', 'wallet app'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Covers 'money management' via money + bills combo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Skipped 'finance' because it's the category — App Store auto-indexes that"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Skipped 'mint' (Mint.com trademark)"&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;Now the app developer can audit &lt;em&gt;why&lt;/em&gt; each keyword was picked. When someone asks "why isn't my app showing up for X?" you have a record. Without &lt;code&gt;coverage_notes&lt;/code&gt;, the output is a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Failures I Hit Along the Way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1&lt;/strong&gt;: "Generate 15-20 keywords under 100 characters." Result: the model wrote a nice list, counted wrong, and delivered 112 characters. No self-verification step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2&lt;/strong&gt;: Added &lt;code&gt;"Do not exceed 100 characters"&lt;/code&gt; — model now refused to output more than 10 keywords to stay safe. Under-coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3&lt;/strong&gt;: JSON schema with &lt;code&gt;char_count&lt;/code&gt; field. Model started counting. Characters dropped into range but duplicates appeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 4 (shipped)&lt;/strong&gt;: Enumerated every rule with "apply in order," asked for &lt;code&gt;coverage_notes&lt;/code&gt; to force reasoning, and added validation with retry.&lt;/p&gt;

&lt;p&gt;Each failure mode came from underspecifying the rules. The LLM isn't "wrong" — it's doing exactly what the prompt asked. Getting production-grade output means writing the prompt like a spec, not a request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Lives Now
&lt;/h2&gt;

&lt;p&gt;I packaged this into &lt;a href="https://apsity.com" rel="noopener noreferrer"&gt;Apsity&lt;/a&gt;'s AI Growth Agent — it runs on every keyword field update across the apps it tracks, compares against real-time search rankings, and flags underperforming tokens for replacement. Free tier covers 1 app and 5 keywords if you want to poke at it.&lt;/p&gt;

&lt;p&gt;More importantly, the pattern generalizes. Any time you have "generate text inside tight constraints" — tweet drafts with character limits, SMS messages, ad headlines, product names — the structure is the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enumerate every constraint as a numbered rule&lt;/li&gt;
&lt;li&gt;Force a JSON schema with self-reported metrics&lt;/li&gt;
&lt;li&gt;Ask for a reasoning field so you can audit&lt;/li&gt;
&lt;li&gt;Validate in code, feed failures back for retry&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Writing the spec as a prompt beats writing it as docs — because you can actually run it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally written for &lt;a href="https://gocodelab.com/en/blog/en-apsity-ai-growth-agent-keyword-optimization" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Deeper writeups on building indie SaaS with Claude are in the &lt;a href="https://gocodelab.com/en/blog" rel="noopener noreferrer"&gt;Lazy Developer series&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The Claude Code Skill Set I Actually Run — Mapped by Dev Task</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Wed, 15 Apr 2026 16:11:05 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/the-claude-code-skill-set-i-actually-run-mapped-by-dev-task-5afm</link>
      <guid>https://dev.to/lazydev_oh/the-claude-code-skill-set-i-actually-run-mapped-by-dev-task-5afm</guid>
      <description>&lt;p&gt;A type error detonated five minutes before deploy. Claude had just declared "completed." I trusted the line and hit &lt;code&gt;vercel --prod&lt;/code&gt;. Preview was green; the Production build failed. Four hotfix commits later, the evening was gone.&lt;/p&gt;

&lt;p&gt;Without that incident, I wouldn't have bothered organizing my Skills. The next day I pinned &lt;code&gt;/verification-before-completion&lt;/code&gt; as an always-on gate. Task by task I added similar guardrails. I ended up with seven Skills in active use.&lt;/p&gt;

&lt;p&gt;This post is the Skill and plugin set I actually run — grouped by dev task. UI / Backend · API / Data · DB / Deploy · Infra / Planning · Research / Review · Debug / Process · Docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skill vs Plugin
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Skill&lt;/strong&gt; is a single markdown file — an SOP that says "this task runs in this order." One file per Skill.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;plugin&lt;/strong&gt; is a bundle of Skills. Anthropic launched the official marketplace in January 2026, and since then &lt;code&gt;/plugin install &amp;lt;name&amp;gt;&lt;/code&gt; pulls in a whole set. Updates ride the same command.&lt;/p&gt;

&lt;p&gt;With 3–4 Skills, manual copy is easier. Past that, plugins are the sensible path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 Plugins I Run
&lt;/h2&gt;

&lt;p&gt;Most of my dev routine lives inside these four. The rest gets filled by two or three project-specific Skills I wrote myself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Base install&lt;/span&gt;
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;superpowers@claude-plugins-official
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;vercel@claude-plugins-official
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;frontend-design@claude-plugins-official
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;bkit@claude-plugins-official

&lt;span class="c"&gt;# Check&lt;/span&gt;
/plugin list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;superpowers&lt;/strong&gt; — methodology Skill bundle. 20+ Skills: brainstorming, TDD, systematic-debugging, verification-before-completion. obra's open-source project, now on the official marketplace. 94k+ stars. Most battle-tested set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vercel&lt;/strong&gt; — infra and framework. Next.js App Router, Server Components, Vercel Functions, AI SDK, deploy CLI. Keeps you on the latest syntax without memorizing release notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;frontend-design&lt;/strong&gt; — UI drafts + React code quality. Steers away from generic AI-looking UIs. After editing multiple TSX files, react-best-practices auto-kicks a quality checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;bkit&lt;/strong&gt; — PDCA and docs layer. If superpowers is "how to work," bkit is "what to record and in what stage." Overkill for solo work; essential when client or team docs matter. Plan → Design → Do → Check → Act each has a Skill. &lt;code&gt;gap-detector&lt;/code&gt; catches design-vs-implementation gaps; &lt;code&gt;pdca-iterator&lt;/code&gt; runs auto-improvement loops.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapd3a38eq0y93r94a6zn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapd3a38eq0y93r94a6zn.png" alt="4-layer plugin stack + custom Skills — each layer has a clear job" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Task-to-Skill Map (7 Tasks × Set)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Primary Skills&lt;/th&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI / Frontend&lt;/td&gt;
&lt;td&gt;frontend-design · shadcn · react-best-practices&lt;/td&gt;
&lt;td&gt;frontend-design · vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend · API&lt;/td&gt;
&lt;td&gt;nextjs · vercel-functions · ai-sdk · phase-4-api&lt;/td&gt;
&lt;td&gt;vercel · bkit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data · DB&lt;/td&gt;
&lt;td&gt;vercel-storage · runtime-cache · next-cache-components&lt;/td&gt;
&lt;td&gt;vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy · Infra&lt;/td&gt;
&lt;td&gt;deployments-cicd · env-vars · verification&lt;/td&gt;
&lt;td&gt;vercel · superpowers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planning · Research&lt;/td&gt;
&lt;td&gt;brainstorming · writing-plans · pdca (plan/design) · bkit-templates&lt;/td&gt;
&lt;td&gt;superpowers · bkit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review · Debug&lt;/td&gt;
&lt;td&gt;systematic-debugging · verification-before-completion · requesting-code-review · code-analyzer · gap-detector&lt;/td&gt;
&lt;td&gt;superpowers · bkit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process · Docs&lt;/td&gt;
&lt;td&gt;pdca (do/check/act) · report-generator · pdca-iterator&lt;/td&gt;
&lt;td&gt;bkit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some Skills overlap. &lt;code&gt;verification-before-completion&lt;/code&gt; runs across deploy, review, and test tasks. I keep it globally on and call the rest per task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvztd59vfmiirvbr797t7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvztd59vfmiirvbr797t7.png" alt="7 tasks × 4 plugins matrix — Skills called per crossing cell" width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  UI / Frontend — Draft to Review
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/brainstorming → /frontend-design draft → shadcn components
  → react-best-practices auto-trigger on TSX save
  → /verification-before-completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the draft stage, &lt;code&gt;frontend-design&lt;/code&gt; produces production-grade drafts without the generic AI-tailwind look. If the project uses shadcn, &lt;code&gt;vercel:shadcn&lt;/code&gt; attaches — component installs, theming, custom registries.&lt;/p&gt;

&lt;p&gt;After implementation, &lt;code&gt;vercel:react-best-practices&lt;/code&gt; detects multiple TSX edits and runs a review checklist — hooks usage, accessibility, performance, TypeScript patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend · API — Next.js Fullstack
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vercel:nextjs&lt;/code&gt; holds the freshest patterns — Server Component fetch, Server Actions for forms, Middleware for request interception. Write Next.js 16 code and it won't silently regress to 15-era syntax.&lt;/p&gt;

&lt;p&gt;When serverless functions enter, &lt;code&gt;vercel:vercel-functions&lt;/code&gt; attaches — Edge vs Node runtime, Fluid Compute streaming, Cron Jobs.&lt;/p&gt;

&lt;p&gt;AI features bring in &lt;code&gt;vercel:ai-sdk&lt;/code&gt;: chat UI, structured output, tool calls, agents, MCP integration. When working against the Anthropic SDK directly, &lt;code&gt;claude-api&lt;/code&gt; takes priority.&lt;/p&gt;

&lt;p&gt;At the early API-design stage, &lt;code&gt;bkit:phase-4-api&lt;/code&gt; helps — endpoint conventions, error payload shapes, Zero Script QA (validate via structured JSON logs, not test scripts).&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy · Infra — On Top of Vercel
&lt;/h2&gt;

&lt;p&gt;Two Skills always attached:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vercel:deployments-cicd&lt;/code&gt;&lt;/strong&gt; — deploys, rollbacks, promotions, prebuilt builds. Even writes GitHub Actions workflow files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vercel:env-vars&lt;/code&gt;&lt;/strong&gt; — syncing &lt;code&gt;.env&lt;/code&gt; with Vercel env vars, OIDC tokens, per-env separation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last gate is always &lt;strong&gt;&lt;code&gt;superpowers:verification-before-completion&lt;/code&gt;&lt;/strong&gt;. Confirms build, type, and tests actually pass before deploy. This Skill prevented most of my production incidents — including the "5 minutes before deploy" one from the intro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9u90rp5yabn0kjcoqru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9u90rp5yabn0kjcoqru.png" alt="Feature flow — 7 steps with Skills per step and artifacts" width="800" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning · Research — Before the Code
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;superpowers:brainstorming&lt;/code&gt; for "build vs. skip." Requirement structuring, edge-case discovery, decision trees.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;superpowers:writing-plans&lt;/code&gt; converts brainstorming into an execution plan — split by file, by step. When the plan file is ready, &lt;code&gt;/execute-plan&lt;/code&gt; picks it up.&lt;/p&gt;

&lt;p&gt;When planning docs must live in the team repo, &lt;code&gt;bkit:pdca&lt;/code&gt;'s plan/design stages run alongside. Writes to &lt;code&gt;docs/plans/{feature}.md&lt;/code&gt; in template form. &lt;code&gt;bkit:bkit-templates&lt;/code&gt; brings planning and design doc templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review · Debug — The Last Gate
&lt;/h2&gt;

&lt;p&gt;Three review/debug Skills hold the last gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;systematic-debugging&lt;/code&gt;&lt;/strong&gt; kicks in on bugs and failing tests. Forces a sequence — reproduce → three hypotheses → eliminate two with evidence → minimal fix. Cuts the impulse to "just fix it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;verification-before-completion&lt;/code&gt;&lt;/strong&gt; runs before "done." Confirms type, build, tests before allowing completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requesting-code-review&lt;/code&gt;&lt;/strong&gt; is for handing off to another session for review.&lt;/p&gt;

&lt;p&gt;Two bkit Skills attach here. &lt;code&gt;bkit:code-analyzer&lt;/code&gt; produces pre-commit quality, security, and performance reports. &lt;code&gt;bkit:gap-detector&lt;/code&gt; catches gaps between design docs and actual implementation — PDCA's Check stage. If Match Rate drops below 90%, &lt;code&gt;pdca-iterator&lt;/code&gt; launches an auto-improvement loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process · Docs — When Docs Become Necessary
&lt;/h2&gt;

&lt;p&gt;On solo work, process and docs Skills stay off. When a teammate joins, when progress reports go to a client, or when future-me asks "why did I build it this way?" — bkit pays off.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bkit:pdca&lt;/code&gt; splits Plan → Design → Do → Check → Act into slash commands. &lt;code&gt;/pdca plan {feature}&lt;/code&gt; writes the planning doc, &lt;code&gt;/pdca analyze&lt;/code&gt; writes the post-implementation analysis.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bkit:report-generator&lt;/code&gt; fires after one PDCA cycle. Pulls Plan/Design/Do/Check docs and actual code into a single-page completion report — ready to hand to stakeholders.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bkit:pdca-iterator&lt;/code&gt; is the Evaluator-Optimizer pattern. Max 5 iterations, hands off to report-generator past 90% match rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapped Onto the EP.19 5-Agent Team
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;planner   → brainstorming · writing-plans · pdca plan/design
coder     → executing-plans · nextjs · frontend-design
reviewer  → requesting-code-review · react-best-practices · code-analyzer
tester    → test-driven-development · verification-before-completion
debugger  → systematic-debugging · gap-detector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the codebase isn't Next.js, swap &lt;code&gt;nextjs&lt;/code&gt; for whatever framework Skill fits. Skills are bundled per plugin, so changing stack doesn't require redesigning the team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjyz0bdntxeb2s77bjjv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjyz0bdntxeb2s77bjjv3.png" alt="Skill cards per agent — swap only the framework Skill to port across stacks" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills I Tried and Dropped
&lt;/h2&gt;

&lt;p&gt;Not every Skill is worth keeping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;using-git-worktrees&lt;/strong&gt; — useful in theory, didn't fit my workflow. I switch branches fast and don't have enough parallel work to justify worktrees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dispatching-parallel-agents&lt;/strong&gt; — set it aside as I moved to running the 5-agent team directly. The external DB state-sharing structure is more stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bkit:enterprise&lt;/strong&gt; / &lt;strong&gt;bkit:infra-architect&lt;/strong&gt; — built for microservices + k8s + Terraform. Doesn't match my stack (Vercel + Supabase). Off unless enterprise-grade infra design actually applies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Build Your Own Skill
&lt;/h2&gt;

&lt;p&gt;Start with what plugins ship. Only build your own when "I keep typing the same thing" repeats three times.&lt;/p&gt;

&lt;p&gt;My three custom Skills: &lt;code&gt;publish-post&lt;/code&gt; (blog publish pipeline), &lt;code&gt;screenshot-ppt&lt;/code&gt; (Puppeteer capture template), &lt;code&gt;wp-media-upload&lt;/code&gt; (WordPress media API call). All blog-operations only.&lt;/p&gt;

&lt;p&gt;When writing your own, &lt;code&gt;superpowers:writing-skills&lt;/code&gt; helps — authoring conventions, examples, metadata format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Skills don't make Claude smarter. They show Claude how I work, repeatedly. Four plugins cover most of it; the remainder goes to two or three custom Skills. First setup is under 10 minutes.&lt;/p&gt;

&lt;p&gt;80% first, fix the remaining 20% as real problems show up. Skills grow the same way. Start with the four base plugins. When "this repeats" happens three times, that's the moment to write a new Skill. Don't force it earlier.&lt;/p&gt;

&lt;p&gt;The last gate is always a human. &lt;code&gt;verification-before-completion&lt;/code&gt; passing doesn't guarantee the feature works — build, type, and tests get the automated pass; business logic still needs a human check. Hold that line, and a Skill set demonstrably speeds up development.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-claude-code-skills-plugin-set-by-task-ep20" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Same Claude, Different Roles — My 5-Agent Dev Team</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Wed, 15 Apr 2026 16:11:00 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/same-claude-different-roles-my-5-agent-dev-team-3jlc</link>
      <guid>https://dev.to/lazydev_oh/same-claude-different-roles-my-5-agent-dev-team-3jlc</guid>
      <description>&lt;p&gt;I pushed a PR. The Claude I'd spun up as Reviewer flagged 3 edge cases and 1 memory leak. In the previous session, the Claude I'd spun up as Coder had called the same code "complete."&lt;/p&gt;

&lt;p&gt;Same Claude 4.6. Only the role was different.&lt;/p&gt;

&lt;p&gt;That gap is why I built an agent team from scratch. Writing and validating solo meant obvious issues slipped through. A session set up as Reviewer doesn't hand out "looks fine" easily. Tell it to nitpick and it nitpicks.&lt;/p&gt;

&lt;p&gt;EP.17 laid down harness engineering (Rules, Commands, Hooks). Sitting on top of it now is a 5-person team — &lt;strong&gt;Planner, Coder, Reviewer, Tester, Debugger&lt;/strong&gt;. Claude Code subagents isolate context by design, so they don't share cumulative state across sessions. My routine needed that sharing, so I layered MCP servers for Agent-to-Agent communication and Supabase for task state on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub957pqm0ywhv84o2590.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub957pqm0ywhv84o2590.png" alt="5-agent team structure — role, Skills, and MCP tools all separated" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Quality Shifts With the Role
&lt;/h2&gt;

&lt;p&gt;The principle is simple. Claude answers in line with the role you give it.&lt;/p&gt;

&lt;p&gt;Tell it "you're the developer who wrote this code," and it answers defensively. Switch to "you're a reviewer, your job is to find mistakes," and it goes straight for edge cases. Same model, different output distribution.&lt;/p&gt;

&lt;p&gt;The problem was writing and validating in the same session. Ask Claude "did this go well?" and it tends to protect what it just wrote. Humans do the same. Reviewing your own code has blind spots.&lt;/p&gt;

&lt;p&gt;Role separation doesn't end with one line of system prompt. Each role reads different Skills, has different tool permissions, and produces different output formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 Agents (Copy-Paste Ready)
&lt;/h2&gt;

&lt;p&gt;Each Agent is a single markdown file following the official Claude Code subagent format — YAML frontmatter with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, and the system prompt in the body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;~/.claude/agents/planner.md&lt;/code&gt;&lt;/strong&gt;&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="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;planner&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;Designs&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;specs,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;implementation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;plans,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;risks.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Writes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;outputs&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;spec.json&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only."&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Glob, mcp__supabase__query, mcp__docs_search__search&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;

You are the Planner. You do not write code. You write the spec.
Input: user's natural-language request.
Output: spec.json (schema below).

spec.json required fields:
&lt;span class="p"&gt;  -&lt;/span&gt; goal: one-line summary
&lt;span class="p"&gt;  -&lt;/span&gt; user_stories: array
&lt;span class="p"&gt;  -&lt;/span&gt; api_endpoints: method, path, I/O
&lt;span class="p"&gt;  -&lt;/span&gt; components: new / modified components
&lt;span class="p"&gt;  -&lt;/span&gt; data_model: new tables / columns
&lt;span class="p"&gt;  -&lt;/span&gt; risks: N+1, missing caching, race conditions
&lt;span class="p"&gt;  -&lt;/span&gt; test_outline: scenarios the Tester will use

Forbidden:
&lt;span class="p"&gt;  -&lt;/span&gt; modifying files, running git, running migrations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;~/.claude/agents/coder.md&lt;/code&gt;&lt;/strong&gt;&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="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="s"&gt;Reads Planner's spec.json and implements it. Stops before commit.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Write, Edit, Bash, Grep, Glob, mcp__supabase__query&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;

You are the Coder. Read spec.json and implement it.
Do not add features outside the spec.

Sequence:
&lt;span class="p"&gt;  1.&lt;/span&gt; Read spec.json in full
&lt;span class="p"&gt;  2.&lt;/span&gt; List files affected
&lt;span class="p"&gt;  3.&lt;/span&gt; Implement — record rationale in implementation_notes.json
&lt;span class="p"&gt;  4.&lt;/span&gt; Run build / type check (exit into debugger state on failure)

Forbidden:
&lt;span class="p"&gt;  -&lt;/span&gt; committing, git push
&lt;span class="p"&gt;  -&lt;/span&gt; running migrations (if not in spec)
&lt;span class="p"&gt;  -&lt;/span&gt; accessing .env or production secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;~/.claude/agents/reviewer.md&lt;/code&gt;&lt;/strong&gt;&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="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;reviewer&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Full review of changed code. Edge cases, security, performance. No modifications.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Glob, Bash(git diff:*), mcp__docs_search__search&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;

You are the Reviewer. You do not modify code. You nitpick.
"Looks fine" is only allowed after three review passes.

Review checklist:
&lt;span class="p"&gt;  -&lt;/span&gt; edge cases (null, empty, overflow)
&lt;span class="p"&gt;  -&lt;/span&gt; race conditions, concurrency
&lt;span class="p"&gt;  -&lt;/span&gt; memory leaks, resource cleanup
&lt;span class="p"&gt;  -&lt;/span&gt; security (XSS, SQL injection, missing permissions)
&lt;span class="p"&gt;  -&lt;/span&gt; naming, consistency
&lt;span class="p"&gt;  -&lt;/span&gt; missing error handling

Output: review_findings.json
&lt;span class="p"&gt;  -&lt;/span&gt; severity: critical / major / minor
&lt;span class="p"&gt;  -&lt;/span&gt; file, line, description, suggested_fix

Forbidden — modifying files, auto-formatting, refactoring suggestions outside spec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tester&lt;/code&gt; and &lt;code&gt;debugger&lt;/code&gt; follow the same pattern — each strictly scoped, each with explicit "do not" rules. What you do &lt;strong&gt;not&lt;/strong&gt; do is stated explicitly. Reviewer — no edits. Debugger — no edits. Coder — no commits. Keeping each role inside its lane is half of the quality story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Session State via Supabase
&lt;/h2&gt;

&lt;p&gt;The moment I split roles, I hit a wall. Coder sessions didn't know about the spec Planner had written. Different sessions don't share memory.&lt;/p&gt;

&lt;p&gt;Solution: an &lt;code&gt;agent_state&lt;/code&gt; table in Supabase. Each Agent writes only to its own slot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;agent_state&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;current_owner&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- planner | coder | ...&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'planning'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;implementation_notes&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;review_findings&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;test_results&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;debug_trace&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- planning → coding → reviewing → testing → debugging? → done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns are split so it's obvious which stage broke. If a session dies mid-turn, &lt;code&gt;status=reviewing, review_findings is null&lt;/code&gt; tells me the Reviewer turn died.&lt;/p&gt;

&lt;p&gt;State-transition guards live in a Supabase Edge Function. Moving from "planning" to "coding" requires that the spec column is populated. The DB enforces it.&lt;/p&gt;

&lt;p&gt;Files get left behind too. &lt;code&gt;docs/tasks/{task_id}.md&lt;/code&gt; holds human-readable output per Agent. DB is the state record, files are the reading record.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpr0hjp2462qacsjxomzd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpr0hjp2462qacsjxomzd.png" alt="CLAUDE.md layers — merged top-down through global → project → agent scope" width="800" height="649"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Servers for Per-Role Permissions
&lt;/h2&gt;

&lt;p&gt;Five agents working on the same project share tools but need different permission levels. MCP servers handle this — each Agent hits an MCP endpoint, the server checks the role, and serves only allowed operations.&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;// ~/.claude/mcp/supabase-server.ts (core excerpt)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;coder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;tester&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test_*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supabase.query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_role&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;role&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;insert&lt;/span&gt;&lt;span class="dl"&gt;'&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&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;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has no write`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&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;No need to repeat prohibition rules in every system prompt — the server rejects anything outside role permissions. Agent prompts stay readable; security lives on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Routine
&lt;/h2&gt;

&lt;p&gt;Turns are driven by one shell script. Claude Code auto-loads subagent files at &lt;code&gt;~/.claude/agents/&amp;lt;name&amp;gt;.md&lt;/code&gt;. The script tells the main session "read the current state for this task_id and delegate to that subagent."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.claude/bin/agent-run.sh&lt;/span&gt;

&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;AGENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;

&lt;span class="nv"&gt;STATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SUPABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/rest/v1/agent_state?task_id=eq.&lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: &lt;/span&gt;&lt;span class="nv"&gt;$SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

claude &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mcp-config&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/mcp.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--append-system-prompt&lt;/span&gt; &lt;span class="s2"&gt;"Task: &lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;. State: &lt;/span&gt;&lt;span class="nv"&gt;$STATE&lt;/span&gt;&lt;span class="s2"&gt;. Delegate to the '&lt;/span&gt;&lt;span class="nv"&gt;$AGENT&lt;/span&gt;&lt;span class="s2"&gt;' subagent only."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Use the &lt;/span&gt;&lt;span class="nv"&gt;$AGENT&lt;/span&gt;&lt;span class="s2"&gt; subagent to handle task &lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;. Read the current agent_state, perform only your role, write back to your own column, and exit."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn trace for a single feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[me]       → agent-run.sh task-001 planner
              "add a usage chart to the dashboard"

planner    → spec.json → status=coding
coder      → implement → status=reviewing
reviewer   → 1 critical, 2 major → status=coding (rework)
coder      → apply findings → status=reviewing (round 2)
reviewer   → OK → status=testing
tester     → 2 failures → status=debugging
debugger   → root cause → status=coding
coder      → minimal fix → status=testing
tester     → all pass → status=done

[me]       → review diff → commit myself
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key point: turns are closed. A single Agent session does only its job, leaves output in files/DB, and exits. The next Agent doesn't inherit the previous session — it reads artifacts.&lt;/p&gt;

&lt;p&gt;A human gates between Agents. I don't auto-launch the next Agent. Tried full auto once; it wandered off course. Manual gating is the current setup. Commits stay with me — no Agent has commit permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed, What Didn't (Honestly)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt; edge cases get caught earlier. Reviewer is built to nitpick, so "looks good" doesn't come cheap. Same code Coder called fine, Reviewer finds three problems in.&lt;/p&gt;

&lt;p&gt;Debugging sped up. Debugger is a separate session, so the context is clean. No "I just wrote this, it should be fine" bias from Coder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't change:&lt;/strong&gt; final review is still mine. Five Agents pass the code, I still skim the diff. Tester sometimes writes meaningless tests. Reviewer sometimes demands a bar nothing can pass. Debugger sometimes names the wrong cause. Team or not, the last gate is a human.&lt;/p&gt;

&lt;p&gt;Cost went up too. About 2–3x the tokens of a single-session run. On the other hand, rework rounds dropped, so total wall-clock time actually went down. A token-for-time trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Role separation is less prompt engineering than workflow engineering. Same model, same codebase — split the session, and the result shifts. Ask the Coder to review, it shields its own work. Split off a Reviewer session, and it nitpicks.&lt;/p&gt;

&lt;p&gt;80% first, then fix the remaining 20% as real problems show up. This team grew that way. Planner and Coder first. Adding Reviewer showed clearly how quality changed. Test automation was thin, so Tester joined. Debugger got split out last to kill debugging bias. Teams should grow by need.&lt;/p&gt;

&lt;p&gt;Final review is still mine. Even after five Agents, I skim the code myself. Take that as a given, and role separation alone changes code quality — measurably.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-dev-agent-team-role-separation-code-quality-ep19" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>axios npm Supply Chain Attack (March 31, 2026) — What Happened and How to Check Your Lock File Right Now</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Tue, 14 Apr 2026 04:44:20 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/axios-npm-supply-chain-attack-march-31-2026-what-happened-and-how-to-check-your-lock-file-coh</link>
      <guid>https://dev.to/lazydev_oh/axios-npm-supply-chain-attack-march-31-2026-what-happened-and-how-to-check-your-lock-file-coh</guid>
      <description>&lt;p&gt;On &lt;strong&gt;March 31, 2026&lt;/strong&gt;, malicious versions of &lt;code&gt;axios&lt;/code&gt; — a package with &lt;strong&gt;70M+ weekly downloads&lt;/strong&gt; — were published to npm after the maintainer's account was hijacked via social engineering. Versions &lt;code&gt;1.14.1&lt;/code&gt; and &lt;code&gt;0.30.4&lt;/code&gt; were pushed back-to-back, both carrying a &lt;code&gt;plain-crypto-js@^4.2.1&lt;/code&gt; dependency that deploys a &lt;strong&gt;cross-platform RAT&lt;/strong&gt; through a postinstall hook.&lt;/p&gt;

&lt;p&gt;The malicious releases sat on the registry for roughly &lt;strong&gt;3 hours&lt;/strong&gt;. In that window, an estimated &lt;strong&gt;600,000 installs&lt;/strong&gt; occurred.&lt;/p&gt;

&lt;p&gt;If you use axios, check your lock file. Now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Malicious: &lt;code&gt;axios@1.14.1&lt;/code&gt;, &lt;code&gt;axios@0.30.4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Safe: &lt;code&gt;axios@1.14.0&lt;/code&gt;, &lt;code&gt;axios@0.30.3&lt;/code&gt; (pre-incident), &lt;code&gt;1.15.0+&lt;/code&gt; / &lt;code&gt;0.30.5+&lt;/code&gt; (post-incident)&lt;/li&gt;
&lt;li&gt;Attribution: North Korea — Sapphire Sleet (Microsoft) / UNC1069 (Google)&lt;/li&gt;
&lt;li&gt;Action: wipe &lt;code&gt;node_modules&lt;/code&gt;, reinstall, &lt;strong&gt;rotate all credentials&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

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




&lt;h2&gt;
  
  
  How to Check Right Now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Installed axios version&lt;/span&gt;
npm list axios

&lt;span class="c"&gt;# Check lock file for malicious versions&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"axios@(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)|plain-crypto-js"&lt;/span&gt; package-lock.json

&lt;span class="c"&gt;# Monorepo-wide scan&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"package-lock.json"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;1.14.1&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;0.30.4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If grep returns a match, remediate immediately. No output means you're probably fine — but also check git history. If the malicious version was ever installed in the past, the postinstall hook has already run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Did the malicious version ever land in lock file history?&lt;/span&gt;
git log &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; package-lock.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4|plain-crypto-js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;pnpm&lt;/code&gt;, use &lt;code&gt;pnpm list axios&lt;/code&gt;; with &lt;code&gt;yarn&lt;/code&gt;, &lt;code&gt;yarn list --pattern axios&lt;/code&gt;. The lock-file grep pattern applies regardless of package manager.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3-Hour Timeline
&lt;/h2&gt;

&lt;p&gt;Independent reconstructions from &lt;a href="https://www.aikido.dev/blog/axios-npm-compromised-maintainer-hijacked-rat" rel="noopener noreferrer"&gt;Aikido Security&lt;/a&gt;, Arctic Wolf, and &lt;a href="https://www.elastic.co/security-labs/axios-one-rat-to-rule-them-all" rel="noopener noreferrer"&gt;Elastic Security Labs&lt;/a&gt; largely agree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (UTC)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2026-03-31 00:21&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@1.14.1&lt;/code&gt; published — targets 1.x line, adds &lt;code&gt;plain-crypto-js@^4.2.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;+39 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Attacker stages the 0.x legacy release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;01:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@0.30.4&lt;/code&gt; published — 0.x branch compromised&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;~03:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Socket.dev / Aikido detect anomalous postinstall hook, community alerts begin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;~04:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;npm force-unpublishes both versions, exposure totals ~3 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Only 3 hours" is a dangerous framing. Vercel, GitHub Actions, CircleCI, and similar CI environments pull fresh versions on cache misses every 10~30 seconds. Globally, tens of thousands of builds ran in that window. Several regions also reported CDN cache serving the malicious version briefly after the unpublish.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Malicious Code Works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;plain-crypto-js&lt;/code&gt; disguises itself as a crypto utility. &lt;strong&gt;It is never imported anywhere in axios source&lt;/strong&gt; — it exists solely to execute its postinstall hook.&lt;/p&gt;

&lt;p&gt;During install, npm runs &lt;code&gt;postinstall&lt;/code&gt; automatically. That hook contacts the attacker's C2 server and pulls a second-stage payload. The payload detects the host OS (macOS / Windows / Linux) and drops a matching RAT (Remote Access Trojan).&lt;/p&gt;

&lt;p&gt;Per Elastic Security Labs, the C2 protocol rides on HTTPS with a custom command set designed to blend into normal API traffic, making network-level detection difficult.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Attack Vector — Maintainer Account Hijack
&lt;/h2&gt;

&lt;p&gt;Per SANS Institute and &lt;a href="https://thehackernews.com/2026/04/unc1069-social-engineering-of-axios.html" rel="noopener noreferrer"&gt;The Hacker News&lt;/a&gt;, the axios maintainer account was hijacked through a &lt;strong&gt;targeted social engineering campaign&lt;/strong&gt;. The attacker changed the account email to &lt;code&gt;ifstap@proton.me&lt;/code&gt;, then abused publish permissions to push the two malicious releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attribution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Threat Intelligence&lt;/strong&gt;: &lt;a href="https://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/" rel="noopener noreferrer"&gt;Sapphire Sleet&lt;/a&gt; — North Korea state actor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google GTIG&lt;/strong&gt;: &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package" rel="noopener noreferrer"&gt;UNC1069&lt;/a&gt; — same actor, tracked independently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Joint attribution confirmed&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;UNC1069 / Sapphire Sleet has a track record of targeting developers through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fake job offers with malicious coding-test files&lt;/li&gt;
&lt;li&gt;Fake recruiter outreach via LinkedIn or Telegram&lt;/li&gt;
&lt;li&gt;Phishing open-source maintainers directly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This axios case appears to fall into the third pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Don't just upgrade — wipe and rebuild.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Wipe node_modules + lock file&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules package-lock.json

&lt;span class="c"&gt;# 2. Clean cache&lt;/span&gt;
npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# 3. Reinstall latest safe version&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;axios@latest

&lt;span class="c"&gt;# 4. Verify&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js"&lt;/span&gt; package-lock.json
&lt;span class="c"&gt;# → No output = clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the same to deployment environments (Vercel / Netlify / GitHub Actions caches). A stale cache can still serve the compromised artifact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rotate All Credentials — Not Just Env Vars
&lt;/h2&gt;

&lt;p&gt;If a malicious version ever reached your machines, the RAT may still be resident. The attacker has system-level access, not just &lt;code&gt;process.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotation checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] AWS / GCP / Azure access keys&lt;/li&gt;
&lt;li&gt;[ ] AI API keys — OpenAI / Anthropic / Gemini&lt;/li&gt;
&lt;li&gt;[ ] Database passwords — PostgreSQL, MySQL, MongoDB&lt;/li&gt;
&lt;li&gt;[ ] Payment API keys — Stripe, LemonSqueezy, Paddle&lt;/li&gt;
&lt;li&gt;[ ] GitHub Personal Access Token + SSH keys&lt;/li&gt;
&lt;li&gt;[ ] App secrets — &lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt;, &lt;code&gt;SESSION_SECRET&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Webhook secrets for external services&lt;/li&gt;
&lt;li&gt;[ ] Infected-machine SSH public keys — remove from &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; on any servers they reached&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revoke old keys immediately after issuing new ones. Keeping the old key alive defeats the rotation.&lt;/p&gt;

&lt;p&gt;For machines with high suspicion of compromise, an OS reinstall is the safest option. CI runner images should be rebuilt clean. Local dev machines should at minimum clear browser sessions, SSH keys, and saved AWS CLI profiles, then reconfigure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prevention Routines
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Commit lock files.&lt;/strong&gt; Without a lock file, every build can pull a different version. If &lt;code&gt;package-lock.json&lt;/code&gt; is in &lt;code&gt;.gitignore&lt;/code&gt;, remove it now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Put &lt;code&gt;npm audit&lt;/code&gt; in CI.&lt;/strong&gt; Run it on every PR. &lt;code&gt;npm audit --audit-level=high&lt;/code&gt; catches high-severity issues at minimum. Caveat: audit only sees what's public in the CVE database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tighten version range specifiers.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Too&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;loose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;opens&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;door&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;auto-updates&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.13.0"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Exact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.14.0"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Patch-only&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~1.14.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Monitor beyond CVE.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependabot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built into GitHub&lt;/td&gt;
&lt;td&gt;CVE-based, limited against fresh attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Socket.dev&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Behavioral analysis&lt;/td&gt;
&lt;td&gt;Flagged this axios incident early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aikido Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time behavioral&lt;/td&gt;
&lt;td&gt;Published first public analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snyk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scan + remediation&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;npm audit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;CVE-based limits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Realistic combo: Dependabot + Socket.dev. Single-tool reliance leaves blind spots.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Keeps Happening
&lt;/h2&gt;

&lt;p&gt;The npm ecosystem has a low publishing bar. A single account compromise can poison a package used by hundreds of millions of developers. That structural fact isn't changing fast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XZ Utils&lt;/strong&gt; (2024-03) — compromised Linux distribution backdoor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;event-stream&lt;/strong&gt; (2018) — crypto wallet stealer hidden in dependency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ua-parser-js&lt;/strong&gt; (2021) — malicious versions with credential stealer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;axios&lt;/strong&gt; (2026-03) — this incident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;axios isn't the first and won't be the last.&lt;/p&gt;

&lt;p&gt;Following this incident, npm is reportedly considering mandatory 2FA expansion and a 24-hour cooldown on maintainer email changes. GitHub already required 2FA for top npm maintainers since 2024, but &lt;strong&gt;this hijack went through the email recovery flow&lt;/strong&gt;. Security chains only hold as strong as the weakest link.&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check your lock file right now&lt;/strong&gt; — don't assume you're fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wipe, don't just upgrade&lt;/strong&gt; — stale caches and remnant RATs are real risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate credentials broadly&lt;/strong&gt; — system-level access means everything is suspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put behavioral analysis in your CI&lt;/strong&gt; — CVE-based tools can't catch fresh attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin exact versions for critical packages&lt;/strong&gt; — range specifiers are attack surface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Trusting a popular package and verifying it are different things. If you use axios, put a version check in your routine starting today.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/axios/axios/issues/10636" rel="noopener noreferrer"&gt;axios Official Post-Mortem (GitHub #10636)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/" rel="noopener noreferrer"&gt;Microsoft Security Blog — Mitigating the Axios npm supply chain compromise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package" rel="noopener noreferrer"&gt;Google Cloud Threat Intelligence — UNC1069 analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.aikido.dev/blog/axios-npm-compromised-maintainer-hijacked-rat" rel="noopener noreferrer"&gt;Aikido Security — first public analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.elastic.co/security-labs/axios-one-rat-to-rule-them-all" rel="noopener noreferrer"&gt;Elastic Security Labs — RAT technical analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://socket.dev/blog/axios-npm-package-compromised" rel="noopener noreferrer"&gt;Socket.dev — plain-crypto-js analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://snyk.io/blog/axios-npm-package-compromised-supply-chain-attack-delivers-cross-platform/" rel="noopener noreferrer"&gt;Snyk Security Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-axios-npm-supply-chain-attack-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt; — April 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vercel vs Netlify vs Cloudflare Pages 2026 — Deep Comparison with Real Numbers</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Tue, 14 Apr 2026 04:25:43 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/vercel-vs-netlify-vs-cloudflare-pages-2026-deep-comparison-with-real-numbers-8pl</link>
      <guid>https://dev.to/lazydev_oh/vercel-vs-netlify-vs-cloudflare-pages-2026-deep-comparison-with-real-numbers-8pl</guid>
      <description>&lt;p&gt;The web deployment landscape crystallized into a clear three-way split in 2026. Vercel for Next.js full-stack. Cloudflare Pages for static sites and edge workloads. Netlify for the Jamstack middle ground. All three ship with &lt;code&gt;git push&lt;/code&gt;-to-deploy out of the box.&lt;/p&gt;

&lt;p&gt;The real story is in billing and performance. In February 2026, Vercel shipped Fluid Compute to GA and announced &lt;strong&gt;up to 95% cost savings across 45 billion weekly requests&lt;/strong&gt;. Cloudflare Workers hold cold starts under 5ms. Netlify migrated to credit-based billing in September 2025. The same app gets billed differently, responds at different speeds, and feels different to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version&lt;/strong&gt;: Next.js ecosystem → Vercel. High-traffic static or edge-heavy → Cloudflare Pages. Forms and adapter ecosystem → Netlify.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmlnxs4r4cj8i789ctyc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmlnxs4r4cj8i789ctyc1.png" alt="Vercel vs Netlify vs Cloudflare Pages comparison" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt;: Hobby free (100GB · 1M invocations), Pro $20/user/mo, Fluid Compute saves up to 95%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify&lt;/strong&gt;: Free 100GB · 300 build min, Pro $19/user/mo, credit-based since Sept 2025&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt;: unlimited bandwidth, 500 builds/mo free, Workers Paid $5/mo bundles ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts&lt;/strong&gt;: Cloudflare &amp;lt; 5ms &amp;gt; Vercel Fluid ~0ms (warm) &amp;gt; Netlify 150~3,000ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js support&lt;/strong&gt;: Vercel native &amp;gt; Netlify adapter (30~60% slower builds) &amp;gt; Cloudflare OpenNext (constraints)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge PoPs&lt;/strong&gt;: Cloudflare 330+ / Vercel 40+ / Netlify 8-region multi-cloud&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare ecosystem&lt;/strong&gt;: KV · D1 · R2 · Durable Objects · Hyperdrive bundled at $5&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Each Platform Actually Is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Vercel&lt;/strong&gt; was founded by Guillermo Rauch in 2015 — the same person behind Next.js. As of 2026, the company sits around $3.2B valuation. The core edge: native Next.js integration. ISR, Image Optimization, Middleware, Server Actions, Cache Components — all of it works with zero config. Hobby plan is personal / non-commercial only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify&lt;/strong&gt; was founded in 2014 and coined the term "Jamstack." Framework adapters span Astro, Next.js, SvelteKit, Nuxt, Gatsby, Hugo — the widest ecosystem of the three. Forms, serverless functions, and Edge Functions come built in. In September 2025, they migrated to credit-based billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; runs on Cloudflare's global edge network. The headline features are 330+ PoPs and unlimited bandwidth. Workers Paid ($5/month) alone bundles Workers, Pages Functions, KV, D1, R2, Durable Objects, and Hyperdrive. Next.js runs through OpenNext and inherits edge runtime constraints — some Node.js modules unavailable, ISR limited.&lt;/p&gt;




&lt;h2&gt;
  
  
  Free Tier Deep Dive
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Vercel Hobby&lt;/th&gt;
&lt;th&gt;Netlify Free&lt;/th&gt;
&lt;th&gt;Cloudflare Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bandwidth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlimited deploys&lt;/td&gt;
&lt;td&gt;300 min/mo&lt;/td&gt;
&lt;td&gt;500 builds/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Function invocations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M/mo&lt;/td&gt;
&lt;td&gt;125K/mo&lt;/td&gt;
&lt;td&gt;100K/day (~3M/mo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4h Active CPU&lt;/td&gt;
&lt;td&gt;Credit-based&lt;/td&gt;
&lt;td&gt;10ms/request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1GB Blob&lt;/td&gt;
&lt;td&gt;10GB&lt;/td&gt;
&lt;td&gt;R2 10GB / KV 1GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Usage restriction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Personal / non-commercial&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial OK&lt;/td&gt;
&lt;td&gt;Commercial OK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare dominates on raw bandwidth — traffic spikes don't trigger overage invoices. Vercel Hobby's decisive constraint is the no-commercial clause. A single advertisement can put you in violation. Netlify's 300 build-minute cap is the actual bottleneck — a medium Next.js project often builds in 5~8 minutes, hitting the ceiling at 40~60 deploys/month.&lt;/p&gt;




&lt;h2&gt;
  
  
  Paid Plans and Overage Simulation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Pro&lt;/strong&gt;: $20/user/month + 16 CPU-hours, 1,440 GB-hours memory — overage Active CPU $0.128/hour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify Pro&lt;/strong&gt;: $19/user/month + 1TB bandwidth, 25K build minutes — $7 per 500 extra build min, $20 per 100GB extra bandwidth, $25 per 1M extra invocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Workers Paid&lt;/strong&gt;: $5/month + 10M requests, 30M CPU-ms — $0.30 per extra 1M requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenario A — Small blog (100K monthly visits, 50GB bandwidth, 500K function calls)&lt;/strong&gt;&lt;br&gt;
Vercel Hobby $0 / Pro $20. Netlify Free possible. Cloudflare Pages $0. → &lt;strong&gt;Cloudflare Pages wins&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B — Next.js SaaS (500K monthly visits, 5M function calls, DB-heavy)&lt;/strong&gt;&lt;br&gt;
Vercel Pro ~$20~30 (Fluid keeps CPU overage near zero). Netlify Pro $19 + function overage $100 = &lt;strong&gt;$119&lt;/strong&gt;. Cloudflare Workers Paid $5 + extra requests $1.50 = &lt;strong&gt;$6.50&lt;/strong&gt;. → Order: Cloudflare, Vercel, Netlify. If Next.js compatibility is non-negotiable, Vercel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario C — Image hosting (2TB monthly downloads)&lt;/strong&gt;&lt;br&gt;
Vercel Pro $20 + 1.9TB overage $380 = &lt;strong&gt;$400&lt;/strong&gt;. Netlify Pro $19 + 1TB overage $200 = &lt;strong&gt;$219&lt;/strong&gt;. Cloudflare R2 $0 egress + $30 storage = &lt;strong&gt;$30&lt;/strong&gt;. → For egress-heavy workloads, Cloudflare is effectively the only option.&lt;/p&gt;


&lt;h2&gt;
  
  
  Vercel Fluid Compute — Real Savings
&lt;/h2&gt;

&lt;p&gt;Fluid Compute hit GA in February 2026. Per Vercel's figures: 45B weekly requests, customers seeing up to 95% savings, 75%+ of all functions now on Fluid. The old model billed the entire function duration. Fluid only bills &lt;strong&gt;Active CPU windows&lt;/strong&gt; — when your code is actually executing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: Next.js API handler (I/O-bound)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 100ms — JSON parsing, validation (Active CPU)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// 400ms — Supabase query wait (I/O, Fluid bills nothing)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// 30ms — response serialization (Active CPU)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Total wall time: 530ms&lt;/span&gt;
&lt;span class="c1"&gt;// Legacy billing: 530ms (all of it)&lt;/span&gt;
&lt;span class="c1"&gt;// Fluid billing: 130ms (Active CPU only) → 75% saved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Vercel's case studies: &lt;em&gt;"Many of our API endpoints were lightweight and involved external requests, resulting in idle compute time. By leveraging in-function concurrency, we were able to share compute resources between invocations, cutting costs by over 50% with zero code changes."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For typical Next.js apps, expect function invocation counts to drop 30~50% with proportional cost reduction. The benefit is limited for CPU-heavy workloads (ML inference, image resizing) where Active CPU dominates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cold Start Benchmarks — 5ms vs 250ms vs 3 Seconds
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Cold start&lt;/th&gt;
&lt;th&gt;Warm response&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Workers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 5ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1ms&lt;/td&gt;
&lt;td&gt;V8 Isolates + Shard-and-Conquer (99.99% warm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel Fluid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0ms (warm)&lt;/td&gt;
&lt;td&gt;20~50ms&lt;/td&gt;
&lt;td&gt;Instance pre-warming + in-function concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel legacy serverless&lt;/td&gt;
&lt;td&gt;~250ms&lt;/td&gt;
&lt;td&gt;50~80ms&lt;/td&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify Functions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;150~3,000ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80~150ms&lt;/td&gt;
&lt;td&gt;AWS Lambda (high variance)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare Workers' sub-5ms comes from V8 Isolates. Instead of spinning up a container, the platform runs your function directly inside the JavaScript engine. Initialization overhead is near zero. Shard-and-Conquer consistent hashing routes same-request traffic to the same node, keeping warm-hit rate at 99.99%.&lt;/p&gt;

&lt;p&gt;Vercel Fluid keeps instances warm with in-function concurrency — a single instance handles multiple concurrent requests. Near-zero cold starts for active functions.&lt;/p&gt;

&lt;p&gt;Netlify, running on AWS Lambda, is the slowest. Cold starts up to 3 seconds in benchmarks. For low-traffic sites or early-morning first requests, users feel the wait.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next.js Feature Compatibility Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Next.js feature&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Netlify&lt;/th&gt;
&lt;th&gt;Cloudflare (OpenNext)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server Components (RSC)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server Actions&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISR (revalidate)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;On-Demand only&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Optimization&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Adapter&lt;/td&gt;
&lt;td&gt;Cloudflare Images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full (edge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache Components&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial Prerendering (PPR)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge Runtime&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Edge Functions&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Node.js modules&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Some blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build speed (same project)&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;30~60% slower&lt;/td&gt;
&lt;td&gt;20% slower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Next.js' latest features (Cache Components, PPR) only ship fully on Vercel. Netlify covers most of it via adapter, but ISR semantics differ and builds run noticeably longer. Cloudflare Pages inherits edge-runtime constraints — can't use &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;net&lt;/code&gt;, or &lt;code&gt;child_process&lt;/code&gt;, and ISR requires wiring Incremental Cache into KV separately.&lt;/p&gt;

&lt;p&gt;On the flip side, Cloudflare's Image Optimization routes through Cloudflare Images (faster CDN), and Edge Runtime is native. For edge-friendly codebases, Cloudflare Pages can actually win.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloudflare Ecosystem — KV · D1 · R2 · Durable Objects
&lt;/h2&gt;

&lt;p&gt;Cloudflare's real edge: $5/month Workers Paid bundles 6+ data services. Each as a standalone SaaS would run into hundreds of dollars.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Price (Paid)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workers KV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Global key-value, config/session/personalization&lt;/td&gt;
&lt;td&gt;Reads 10M $0.50, writes 1M $5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed SQLite, lightweight relational DB&lt;/td&gt;
&lt;td&gt;Reads 25M $1, writes 50K $1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;R2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3-compatible object storage, &lt;strong&gt;zero egress&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;$0.015/GB storage, Class A 1M $4.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Durable Objects&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WebSockets, collaboration, locks, rate limiters&lt;/td&gt;
&lt;td&gt;1M requests $0.15, $0.20/GB/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Message queue, async work&lt;/td&gt;
&lt;td&gt;1M operations $0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hyperdrive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;External PostgreSQL pooling&lt;/td&gt;
&lt;td&gt;Included in Workers Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Practical combo: sessions/config on KV, user data on D1, images/files on R2, chat rooms on Durable Objects, background jobs via Queues. Everything at the same $5.&lt;/p&gt;

&lt;p&gt;AWS equivalent stack: RDS ($15) + DynamoDB ($10) + S3 ($5) + &lt;strong&gt;egress ($100+)&lt;/strong&gt; + SQS ($2) = &lt;strong&gt;$130+/month minimum&lt;/strong&gt;. R2's zero-egress policy alone makes file-heavy services land in a completely different cost range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable Objects is the only practical choice for stateful edge computing.&lt;/strong&gt; WebSocket chat rooms, Google Docs-style real-time collaboration, distributed locks, rate limiters. Vercel and Netlify have no equivalent, forcing external services (Pusher, Ably) to fill the gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edge Network and Global TTFB
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt;: 330+ PoPs across 120+ countries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt;: own edge network (40+ regions) + AWS/GCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify&lt;/strong&gt;: multi-cloud AWS/GCP/Azure (8 main regions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TTFB benchmarks from Korea (static content): Cloudflare Seoul PoP 30~50ms, Vercel Tokyo/Seoul region 80~120ms, Netlify US-West default 250~400ms. For global apps with APAC users, Cloudflare is overwhelmingly the fastest experience.&lt;/p&gt;

&lt;p&gt;Vercel's Tokyo/Singapore regions can reach ~100ms in Korea when explicitly configured. Hobby has limited region pinning; Pro enables per-project region selection. Setting &lt;code&gt;regions&lt;/code&gt; in &lt;code&gt;vercel.json&lt;/code&gt; is important — defaults often point to US regions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Netlify Credit-Based Pricing
&lt;/h2&gt;

&lt;p&gt;Since September 2025, Netlify uses a unified credit pool. Approximate conversions: 1 build minute = 1 credit, 1,000 function invocations = 1 credit, 1 GB bandwidth = 1 credit. Pro includes 500 credits/month — in theory 100 deploys if builds are 5 minutes each, but practical ceiling drops to 50~70 after other usage.&lt;/p&gt;

&lt;p&gt;The complaint is predictability. &lt;em&gt;"My build ran long and drained my credits"&lt;/em&gt; posts keep showing up in dev forums. Accounts created before September 4, 2025 can stay on the legacy plan.&lt;/p&gt;

&lt;p&gt;Netlify's strengths still hold — Forms built in (100 submissions/mo free), Identity, Large Media, Split Testing. Features Vercel and Cloudflare don't match natively.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Netlify&lt;/th&gt;
&lt;th&gt;Cloudflare Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free bandwidth&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid starting&lt;/td&gt;
&lt;td&gt;$20/user/mo&lt;/td&gt;
&lt;td&gt;$19/user/mo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;~0ms (warm)&lt;/td&gt;
&lt;td&gt;150~3,000ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 5ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js support&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Native (full)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adapter (mostly)&lt;/td&gt;
&lt;td&gt;OpenNext (constrained)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless billing&lt;/td&gt;
&lt;td&gt;Active CPU (Fluid)&lt;/td&gt;
&lt;td&gt;Credit-based&lt;/td&gt;
&lt;td&gt;Per-request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global PoPs&lt;/td&gt;
&lt;td&gt;40+ edge&lt;/td&gt;
&lt;td&gt;8 regions&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;330+ PoPs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial free use&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;Next.js + Postgres/KV/Blob&lt;/td&gt;
&lt;td&gt;Forms, Identity, Split Testing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;KV, D1, R2, DO, Queues&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build speed (Next.js)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fastest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30~60% slower&lt;/td&gt;
&lt;td&gt;20% slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DX / dashboard&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;Deep but learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress cost&lt;/td&gt;
&lt;td&gt;Deducts from bandwidth&lt;/td&gt;
&lt;td&gt;Deducts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R2 $0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Combining Platforms — Real-World Patterns
&lt;/h2&gt;

&lt;p&gt;No reason to pick one and stick with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Pattern A — Subdomain split (most common)
static.example.com  → Cloudflare Pages (images, docs, heavy assets)
app.example.com     → Vercel Pro (Next.js full-stack)
forms.example.com   → Netlify (form intake)

# Pattern B — Cloudflare as front CDN, Vercel as origin
Cloudflare (CDN/WAF/DDoS) → Vercel (serverless origin)
# Cloudflare absorbs egress, Vercel handles execution only

# Pattern C — Full Cloudflare stack (AWS alternative)
Cloudflare Pages + Workers + D1 + R2 + Durable Objects
# Full-stack infra starting at $5/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Recommendations by Scenario
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js full-stack SaaS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vercel Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fluid Compute 95% savings, Cache Components/PPR native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image / video hosting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare + R2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero egress, 330+ PoPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro / SvelteKit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adapter ecosystem, built-in forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time / WebSocket&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare + DO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only edge stateful solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global TTFB matters&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Largest edge network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermittent traffic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vercel Fluid / Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low cold start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal (no revenue)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vercel Hobby&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All Next.js features free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form-heavy marketing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forms built in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;All three platforms are mature as of 2026. "Which is better" is the wrong frame — "which fits your stack" is the real question.&lt;/p&gt;

&lt;p&gt;Three trends worth watching as of April 2026:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Fluid Compute&lt;/strong&gt; now powers 75%+ of all Vercel Functions and has measurably dropped Next.js full-stack bills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare D1&lt;/strong&gt; moved past GA with real production references, making AWS RDS replacement a concrete option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify's credit-based pricing&lt;/strong&gt; is driving heavy users to reconsider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The right choice shifts each year. Review your workload periodically.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Official sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Vercel Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/fluid-compute" rel="noopener noreferrer"&gt;Vercel Fluid Compute docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/introducing-active-cpu-pricing-for-fluid-compute" rel="noopener noreferrer"&gt;Vercel Active CPU pricing blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/pricing/" rel="noopener noreferrer"&gt;Netlify Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/" rel="noopener noreferrer"&gt;Netlify credit-based pricing docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/workers/platform/pricing/" rel="noopener noreferrer"&gt;Cloudflare Workers pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/durable-objects/platform/pricing/" rel="noopener noreferrer"&gt;Cloudflare Durable Objects pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/unpacking-cloudflare-workers-cpu-performance-benchmarks/" rel="noopener noreferrer"&gt;Cloudflare Workers CPU benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-vercel-vs-netlify-vs-cloudflare-pages-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt; — April 2026 pricing. Plans and policies change frequently; verify with official docs before committing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vercel</category>
      <category>cloudflare</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Catalogued the Security Patterns That Keep Showing Up in AI Code</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 04:03:48 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/i-catalogued-the-security-patterns-that-keep-showing-up-in-ai-code-2jla</link>
      <guid>https://dev.to/lazydev_oh/i-catalogued-the-security-patterns-that-keep-showing-up-in-ai-code-2jla</guid>
      <description>&lt;p&gt;Across the Apsity App Store dashboard, the FeedMission SaaS, and a dozen side projects, more than half the code I touch is AI-generated. After &lt;a href="https://gocodelab.com/en/blog/en-feedmission-saas-7days-mvp-ep04" rel="noopener noreferrer"&gt;shipping a SaaS in 7 days&lt;/a&gt;, vibe coding has been the default workflow.&lt;/p&gt;

&lt;p&gt;Run it long enough and the patterns show up. AI-generated code keeps producing the same classes of security holes. One FeedMission review surfaced &lt;a href="https://gocodelab.com/en/blog/en-feedmission-nextjs-security-email-debug-ep06" rel="noopener noreferrer"&gt;seven criticals at the same time&lt;/a&gt; — a Slack webhook URL bundled into the frontend, an unsubscribe endpoint that any email address could trigger, an admin reply leaking through a public API, routes missing team-member auth checks. None of that was bad luck. Industry research lists these as the highest-frequency patterns, and they had effectively reproduced themselves in our codebase.&lt;/p&gt;

&lt;p&gt;So now I run the same seven checks before every deploy, the same way each time. This post is the pattern catalogue plus the routine.&lt;/p&gt;

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

&lt;p&gt;This isn't a vibe check. Multiple groups in 2026 (Georgia Tech, Cloud Security Alliance, Checkmarx) analyzed AI-generated code and found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;40–62%&lt;/strong&gt; of samples contain security issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.74×&lt;/strong&gt; more vulnerable than human-written code on equivalent tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;86%&lt;/strong&gt; failed XSS defenses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;88%&lt;/strong&gt; vulnerable to log injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;35 new CVEs&lt;/strong&gt; tied to AI-generated code in March 2026 alone&lt;/li&gt;
&lt;li&gt;One AI app leaked &lt;strong&gt;1.5M API keys&lt;/strong&gt; post-launch — shipped without security review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody's quitting vibe coding because of these numbers. I'm not. But the 10 minutes you spend before deploy is what decides production's fate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI skips security
&lt;/h2&gt;

&lt;p&gt;Beginners get this wrong. The AI didn't make a mistake — it built what you asked for. "Make a user profile API" → it makes one. Auth wasn't requested, so it's not there. It leaves &lt;code&gt;// TODO: add auth here&lt;/code&gt; and moves on.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;put security in the prompt from the start.&lt;/strong&gt; "Include JWT auth middleware, read secrets only from env, no raw SQL, no TODO comments, ship complete code." One line changes the output quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Top 7 mistakes — in the order I hit them
&lt;/h2&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;Mistake&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Red flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hardcoded API keys&lt;/td&gt;
&lt;td&gt;Scraped by bots within seconds&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sk_&lt;/code&gt;, &lt;code&gt;api_key=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Auth-less API routes&lt;/td&gt;
&lt;td&gt;URL-only access to your DB&lt;/td&gt;
&lt;td&gt;no session/auth/token references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; misuse&lt;/td&gt;
&lt;td&gt;Service-role key in browser bundle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NEXT_PUBLIC_*_SECRET/KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Raw SQL interpolation&lt;/td&gt;
&lt;td&gt;SQL injection → full DB exfil&lt;/td&gt;
&lt;td&gt;&lt;code&gt;`SELECT ... ${}`&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;CORS wildcards&lt;/td&gt;
&lt;td&gt;Any domain hits your API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow-Origin: *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Missing XSS / log-injection defense&lt;/td&gt;
&lt;td&gt;User input straight into HTML/logs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, raw-string logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Phantom packages (slopsquatting)&lt;/td&gt;
&lt;td&gt;Malicious package under hallucinated name&lt;/td&gt;
&lt;td&gt;unfamiliar packages, low downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  1 and #3 hit fastest. The moment you push to GitHub, scraper bots scoop the key and burn your API quota. If you've never been hit, you've only been lucky.
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Slopsquatting warning&lt;/strong&gt; — when AI says &lt;code&gt;npm install some-plausible-package&lt;/code&gt;, check npmjs.com first. About &lt;strong&gt;20% of AI-generated code references nonexistent packages&lt;/strong&gt;. Attackers register those names with malicious payloads, and you install them instantly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What could have happened at FeedMission
&lt;/h2&gt;

&lt;p&gt;From the 7 above, FeedMission had #2, #3, #6, plus a few app-specific issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slack webhook URL&lt;/strong&gt; rode on ProjectContext into the frontend bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsubscribe API&lt;/strong&gt; took just an email address. Anyone's email → instant unsubscribe. Switched to an &lt;code&gt;unsubscribeToken&lt;/code&gt; flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/api/feedback/mine&lt;/code&gt;&lt;/strong&gt; returned the full admin reply text. Now &lt;code&gt;hasReply: boolean&lt;/code&gt; only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team member auth checks&lt;/strong&gt; missing across several APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; wasn't in &lt;code&gt;.vercelignore&lt;/code&gt; — almost shipped via symlink in a Vercel build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All fixed in one commit (&lt;code&gt;52efb89&lt;/code&gt;). None of these are "too edge-case to happen to me."&lt;/p&gt;

&lt;h2&gt;
  
  
  My 10-minute pre-deploy routine
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Three grep lines — 5 seconds&lt;/span&gt;
&lt;span class="c"&gt;# Unfinished security code&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"TODO&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;FIXME&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;implement.*later&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;add.*auth"&lt;/span&gt; ./src

&lt;span class="c"&gt;# Hardcoded secrets&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"sk_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;api_key&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*="&lt;/span&gt; ./src

&lt;span class="c"&gt;# Client-exposed env vars&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"NEXT_PUBLIC_.*(SECRET|KEY|TOKEN)"&lt;/span&gt; ./src

&lt;span class="c"&gt;# 2. SQL interpolation and CORS wildcards&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;SELECT&lt;/span&gt;&lt;span class="se"&gt;\|\`&lt;/span&gt;&lt;span class="s2"&gt;INSERT&lt;/span&gt;&lt;span class="se"&gt;\|\`&lt;/span&gt;&lt;span class="s2"&gt;UPDATE&lt;/span&gt;&lt;span class="se"&gt;\|\`&lt;/span&gt;&lt;span class="s2"&gt;DELETE"&lt;/span&gt; ./src
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"Allow-Origin.*&lt;/span&gt;&lt;span class="se"&gt;\*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all pass, paste the generated code back to the AI and ask: &lt;em&gt;"Review this code against OWASP Top 10 for vulnerabilities."&lt;/em&gt; Imperfect but a fine first-pass filter.&lt;/p&gt;

&lt;p&gt;GitHub side, turn on three things: &lt;strong&gt;Secret Scanning, Push Protection, CodeQL Code Scanning&lt;/strong&gt;. Plus Dependabot/npm audit in CI for package vulns.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My prompt tail (every code-generation request):&lt;/strong&gt; &lt;em&gt;"Include auth middleware; read secrets only from process.env and use NEXT_PUBLIC only for public values; always validate user input; no raw SQL; ship complete code without TODO/FIXME."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bonus — Using Supabase? RLS is its own chapter
&lt;/h2&gt;

&lt;p&gt;Next.js + Supabase is the default vibe-coder stack, so RLS gets a dedicated section. RLS (Row Level Security) is PostgreSQL's row-level access control. &lt;em&gt;"This row is readable only by the user whose user_id matches"&lt;/em&gt; — enforced at the database layer.&lt;/p&gt;

&lt;p&gt;Why this matters: &lt;strong&gt;when you create a table in Supabase Studio, RLS is OFF by default.&lt;/strong&gt; Ship &lt;code&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/code&gt; to the client in that state and anyone with that key can read or write every row in every table. The anon key effectively becomes a service-role key. Whatever assurance "client-side anon key is safe" gave you, it's gone.&lt;/p&gt;

&lt;p&gt;Turning RLS on isn't enough either. &lt;strong&gt;Without policies, every access is denied.&lt;/strong&gt; You write separate policies per action: &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;. The most frequent mistake is writing &lt;code&gt;USING&lt;/code&gt; (the read/delete-time filter) but forgetting &lt;code&gt;WITH CHECK&lt;/code&gt; (the post-write validation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ✗ Risky — USING only&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"own rows"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- WITH CHECK forgotten!&lt;/span&gt;

&lt;span class="c1"&gt;-- ✓ Safe — both&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"own rows"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without WITH CHECK, user_a can INSERT or UPDATE rows claiming user_b's user_id — planting rows or hijacking existing ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three review queries to save in your Supabase SQL Editor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Tables with RLS still off&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. RLS on but no policies — everything is rejected&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policyname&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. INSERT/UPDATE policies missing WITH CHECK&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'INSERT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UPDATE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these after every migration. Empty results on all three = you're clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-4 BaaS-specific mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RLS off&lt;/strong&gt; — anon key becomes a master key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing WITH CHECK&lt;/strong&gt; — attackers plant rows under someone else's user_id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;service_role key shipped to client&lt;/strong&gt; — &lt;code&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/code&gt; must never be &lt;code&gt;NEXT_PUBLIC&lt;/code&gt;. Server routes / Edge Functions only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissive anon-role policies&lt;/strong&gt; — &lt;code&gt;auth.uid() = user_id&lt;/code&gt; missing means unauthenticated callers reach every row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same principle applies to Firebase Security Rules, Appwrite Permissions, PocketBase Collection rules: &lt;strong&gt;if the client talks to the database directly, the database is the last line of defense.&lt;/strong&gt; Leave that line empty and no upstream security matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Vibe coding didn't make security worse. The habit of deploying without review did. AI raised the speed. Raise your review speed with it. Three grep lines, one AI review, three GitHub settings, the RLS check if you're on Supabase. Ten minutes.&lt;/p&gt;

&lt;p&gt;Skip those ten minutes and "1.5M API keys leaked" stops being someone else's story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-top-ten/" rel="noopener noreferrer"&gt;OWASP Top 10&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://checkmarx.com/blog/security-in-vibe-coding/" rel="noopener noreferrer"&gt;Checkmarx — Security in Vibe Coding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-ai-generated-code-vulnerability-surge-2026/" rel="noopener noreferrer"&gt;Cloud Security Alliance — AI-Generated CVE Surge 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning" rel="noopener noreferrer"&gt;GitHub Secret Scanning docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-vibecoding-security-checklist-for-beginners-ep18" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Lazy Developer EP.18.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Upgraded to Tailwind v4 — Config Files Are Gone</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:23 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/upgraded-to-tailwind-v4-config-files-are-gone-1o09</link>
      <guid>https://dev.to/lazydev_oh/upgraded-to-tailwind-v4-config-files-are-gone-1o09</guid>
      <description>&lt;p&gt;Tailwind CSS v4 shipped in January 2025 and &lt;code&gt;tailwind.config.js&lt;/code&gt; is gone. Configuration now lives inside the CSS file itself. I migrated a Next.js project — unfamiliar at first, but simpler once you're through it.&lt;/p&gt;

&lt;p&gt;The actual transition is faster than expected. &lt;strong&gt;The official CLI handles about 80% of it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tailwind.config.js&lt;/code&gt; → replaced by a CSS &lt;code&gt;@theme&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;Rust-based &lt;strong&gt;Oxide compiler&lt;/strong&gt; — up to &lt;strong&gt;5x faster&lt;/strong&gt; full builds, up to &lt;strong&gt;100x faster&lt;/strong&gt; incremental&lt;/li&gt;
&lt;li&gt;Automatic content detection — no more manual &lt;code&gt;content&lt;/code&gt; array&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@tailwind base/components/utilities&lt;/code&gt; → single &lt;code&gt;@import "tailwindcss"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Plugins declared in CSS via &lt;code&gt;@plugin "..."&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real-world number from Tailwind's own benchmark: a design system with 15,000 utility classes saw cold builds drop from 840ms to 170ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config Moved into CSS
&lt;/h2&gt;

&lt;p&gt;v3 kept everything in JS. v4 does it all in one CSS file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* v4 — configure directly in CSS */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--breakpoint-3xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1920px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;68%&lt;/span&gt; &lt;span class="m"&gt;0.19&lt;/span&gt; &lt;span class="m"&gt;245&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter Variable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&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;@theme&lt;/code&gt; uses CSS variables. Design tokens are visible in DevTools at runtime. One less JS dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/theme"&gt;@theme&lt;/a&gt; Naming Convention
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--color-{name}&lt;/code&gt;, &lt;code&gt;--font-{name}&lt;/code&gt;, &lt;code&gt;--spacing-{name}&lt;/code&gt;. Tailwind reads the namespace and generates utility classes automatically. Define &lt;code&gt;--color-brand&lt;/code&gt; and &lt;code&gt;text-brand&lt;/code&gt;, &lt;code&gt;bg-brand&lt;/code&gt;, &lt;code&gt;border-brand&lt;/code&gt; light up immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oxide Compiler
&lt;/h2&gt;

&lt;p&gt;Rust, not Node. Replaces the old PostCSS plugin. Content path detection is automatic — no more &lt;code&gt;content: ['./src/**/*.tsx']&lt;/code&gt;. Oxide ships inside the &lt;code&gt;tailwindcss&lt;/code&gt; v4 package, no separate install. Integrates with Vite and PostCSS pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A — one command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tailwindcss/upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handles config conversion and class renames for projects without custom plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B — manual (Next.js / PostCSS)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss@latest @tailwindcss/postcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// postcss.config.js (v4)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tailwindcss/postcss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* globals.css (v4) */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&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;tailwind.config.js&lt;/code&gt; can be deleted or kept — v4 doesn't read it. Deleting it is cleaner for team repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins Now Live in CSS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/typography"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/forms"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"./plugins/my-plugin.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&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 &lt;code&gt;plugins&lt;/code&gt; array in &lt;code&gt;tailwind.config.js&lt;/code&gt; is gone. Pass a package name or a file path to &lt;code&gt;@plugin&lt;/code&gt; and it works. Existing &lt;code&gt;addUtilities&lt;/code&gt; and &lt;code&gt;addComponents&lt;/code&gt; APIs mostly still apply, but parts of the plugin API changed — verify behavior after migrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;outline-none&lt;/code&gt; Gotcha
&lt;/h2&gt;

&lt;p&gt;v3: &lt;code&gt;outline-none&lt;/code&gt; rendered as &lt;code&gt;outline: 2px solid transparent&lt;/code&gt; — still accessible.&lt;br&gt;
v4: &lt;code&gt;outline-none&lt;/code&gt; renders as &lt;code&gt;outline: none&lt;/code&gt; — actually removes the outline.&lt;/p&gt;

&lt;p&gt;If you used &lt;code&gt;outline-none&lt;/code&gt; to hide focus rings on buttons or inputs, swap in &lt;code&gt;outline-hidden&lt;/code&gt;. Expect this to surface during accessibility checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  v3 vs v4 at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;v3&lt;/th&gt;
&lt;th&gt;v4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;@theme&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import&lt;/td&gt;
&lt;td&gt;three &lt;code&gt;@tailwind&lt;/code&gt; lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@import "tailwindcss"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content detection&lt;/td&gt;
&lt;td&gt;manual array&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;PostCSS (Node)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oxide (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugins&lt;/td&gt;
&lt;td&gt;&lt;code&gt;plugins: [...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@plugin "..."&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outline-none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;transparent outline&lt;/td&gt;
&lt;td&gt;actual &lt;code&gt;none&lt;/code&gt; (use &lt;code&gt;outline-hidden&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Should You Upgrade Now?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New project&lt;/strong&gt; → v4. No reason not to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing v3 project&lt;/strong&gt; → no rush. v3 is still supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy custom-plugin stack&lt;/strong&gt; → stay on v3 until you've tested each plugin against the v4 API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build times biting&lt;/strong&gt; → v4 is worth the migration cost just for the Oxide numbers.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Q. Do I need to delete &lt;code&gt;tailwind.config.js&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No — v4 doesn't read it. The upgrade CLI handles conversion. Delete for cleanliness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Separate Oxide install?&lt;/strong&gt;&lt;br&gt;
No. Included in the &lt;code&gt;tailwindcss&lt;/code&gt; v4 package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. How long does migration take?&lt;/strong&gt;&lt;br&gt;
Small Next.js projects: 30 minutes including manual review. Larger ones with custom plugins and dynamic class composition (&lt;code&gt;bg-${color}-500&lt;/code&gt; patterns): a couple hours, because those aren't auto-migrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4-alpha" rel="noopener noreferrer"&gt;Open-sourcing progress on Tailwind CSS v4.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4" rel="noopener noreferrer"&gt;Tailwind CSS v4.0 release post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-tailwind-css-v4-migration-guide-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Gemma 4 vs Llama 4 vs Mistral Small 4: The 2026 Open-Source LLM Picks</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:22 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/gemma-4-vs-llama-4-vs-mistral-small-4-the-2026-open-source-llm-picks-20e7</link>
      <guid>https://dev.to/lazydev_oh/gemma-4-vs-llama-4-vs-mistral-small-4-the-2026-open-source-llm-picks-20e7</guid>
      <description>&lt;p&gt;Three heavyweights dropped this year: Gemma 4 (Google), Llama 4 (Meta), Mistral Small 4 (Mistral). All free to run. All structurally different. Here's which one fits which job.&lt;/p&gt;

&lt;p&gt;Short answer: long context → &lt;strong&gt;Llama 4 Scout&lt;/strong&gt;. License-clean commercial use → &lt;strong&gt;Mistral Small 4&lt;/strong&gt;. On-device → &lt;strong&gt;Gemma 4 E2B / E4B&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Take
&lt;/h2&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;Gemma 4 (31B / 26B MoE)&lt;/th&gt;
&lt;th&gt;Llama 4 Scout&lt;/th&gt;
&lt;th&gt;Mistral Small 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dense (31B) · MoE (26B/A4B)&lt;/td&gt;
&lt;td&gt;MoE (17B active / 109B)&lt;/td&gt;
&lt;td&gt;MoE (~22B active / 119B)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;E2B/E4B 128K · 31B/26B 256K&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Gemma ToU&lt;/td&gt;
&lt;td&gt;Llama 4 Community&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apache 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multimodal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;text + image + video + OCR (E2B/E4B add &lt;strong&gt;audio&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;text + image (early fusion)&lt;/td&gt;
&lt;td&gt;text + image (first in Small series)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent (E2B/E4B)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low (multi-GPU even quantized)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  MoE vs Dense
&lt;/h2&gt;

&lt;p&gt;MoE is a bank of specialized tellers — only the relevant experts fire per input. Llama 4 Scout: 109B total, 17B active. Mistral Small 4: 119B total across 128 experts, ~22B active. Gemma 4 26B: the "small MoE" path — 26B total, ~3.8B active, targeting 4B-speed with bigger-model intelligence.&lt;/p&gt;

&lt;p&gt;Gemma 4 E2B, E4B, and 31B are Dense. Every parameter fires on every token. Higher compute per parameter, but memory requirements scale linearly and planning is easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One MoE trap people hit:&lt;/strong&gt; inference compute drops, but all weights still need to sit in memory. Llama 4 Scout in fp16 = ~218GB VRAM. 4-bit = ~55GB. "Only 17B active so it's lightweight" is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Window — 10M, 256K, 128K
&lt;/h2&gt;

&lt;p&gt;Llama 4 Scout's 10M is the outlier. Meta got there via &lt;strong&gt;iRoPE&lt;/strong&gt; — interleaved RoPE that holds accuracy past the training sequence length. Practical impact: you can drop an entire monorepo into one prompt and skip the RAG pipeline altogether.&lt;/p&gt;

&lt;p&gt;Mistral Small 4 sits at 256K. Gemma 4's small variants (E2B/E4B) are 128K; the medium 31B and 26B MoE jump to 256K. For normal-scale work — books, research paper batches, long meeting transcripts — 128K is already more than enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Maverick&lt;/strong&gt; on SWE-bench: 76.8 to 80.8 depending on the evaluation variant. Open-source top tier — but not "absolute #1." GLM-5 (77.8) shows up right next to it on SWE-bench Verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Scout&lt;/strong&gt; is smaller than Maverick but wins on repo-scale analysis thanks to 10M context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 31B&lt;/strong&gt; shines on multimodal tasks relative to its size class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4&lt;/strong&gt; (per Mistral's evals) matches or surpasses GPT-OSS 120B and Qwen-class models on several key benchmarks — at ~22B active.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benchmarks and day-to-day use diverge. Run them yourself before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multimodal — Images, Video, Audio
&lt;/h2&gt;

&lt;p&gt;None of these three is text-only in 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4&lt;/strong&gt; is natively multimodal across every variant: text, image, video, OCR. E2B and E4B add &lt;strong&gt;native audio input&lt;/strong&gt; — voice assistants and on-device transcription become direct use cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Scout/Maverick&lt;/strong&gt; use early fusion — text and vision tokens unified inside the foundation model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4&lt;/strong&gt; is the first in the Mistral Small series to support native vision. Images ride in the normal API message array alongside text, inside the same 256K window.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Licenses (Actually Read Before Shipping)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4 / Apache 2.0&lt;/strong&gt; — zero restrictions. Fine-tune, redistribute, embed in SaaS, ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Community&lt;/strong&gt; — commercial use fine below 700M MAU, but Meta's approval is required above that (sole discretion). Also: mandatory &lt;strong&gt;"Built with Llama"&lt;/strong&gt; badge on a related web or in-app page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 / Google Gemma ToU&lt;/strong&gt; — you can't use Gemma outputs to train competing LLMs, and AI-adjacent services need to read the clauses carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Edge Deployment Reality
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;fp16 VRAM&lt;/th&gt;
&lt;th&gt;4-bit VRAM&lt;/th&gt;
&lt;th&gt;Realistic hardware&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;~8GB&lt;/td&gt;
&lt;td&gt;~3GB&lt;/td&gt;
&lt;td&gt;Laptop / phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;~62GB&lt;/td&gt;
&lt;td&gt;~16GB&lt;/td&gt;
&lt;td&gt;RTX 4090 / M2 Max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 4 Scout&lt;/td&gt;
&lt;td&gt;~218GB&lt;/td&gt;
&lt;td&gt;~55GB&lt;/td&gt;
&lt;td&gt;Multi-GPU / H100 at Int4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral Small 4&lt;/td&gt;
&lt;td&gt;~238GB&lt;/td&gt;
&lt;td&gt;~60GB&lt;/td&gt;
&lt;td&gt;Multi-GPU / high-end workstation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gemma 4 E4B at 4-bit = ~3GB. Runs on a laptop. For smartphone deployments E2B is the target. Llama 4 Scout and Mistral Small 4 stay in server territory even quantized — the full MoE weights have to fit in memory regardless of active count.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Combine All Three
&lt;/h2&gt;

&lt;p&gt;Routing by request type is more realistic than picking one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request type                    → model
-------------------------------------------
whole-doc / whole-repo analysis → Llama 4 Scout (10M context)
image + video + audio input     → Gemma 4
commercial API traffic          → Mistral Small 4 (Apache 2.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using hosted APIs (Together AI, Groq, Fireworks) on top of this routing lets you optimize both cost and capability together.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q. How does Scout actually handle 10M tokens?&lt;/strong&gt;&lt;br&gt;
iRoPE — Meta's interleaved version of RoPE position encoding. Extends accuracy well past training length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Which is most commercial-friendly?&lt;/strong&gt;&lt;br&gt;
Mistral Small 4. Apache 2.0. No MAU cap, no branding requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Is MoE always better than Dense?&lt;/strong&gt;&lt;br&gt;
No. Inference compute drops, but memory scales with total parameters. Edge = Dense small or compact MoE like Gemma 4 26B. MoE only pays off with multi-GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Best at coding?&lt;/strong&gt;&lt;br&gt;
Llama 4 Maverick (76.8–80.8 on SWE-bench) — top tier, not #1. GLM-5 (77.8) is right there too. Mistral Small 4 is fine for general code review; Scout's 10M wins whole-repo work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/blog/gemma4" rel="noopener noreferrer"&gt;Hugging Face — Welcome Gemma 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.meta.com/blog/llama-4-multimodal-intelligence/" rel="noopener noreferrer"&gt;Meta AI — The Llama 4 herd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.llama.com/llama4/license/" rel="noopener noreferrer"&gt;Llama 4 Community License&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mistral.ai/news/mistral-small-4" rel="noopener noreferrer"&gt;Mistral Small 4 announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-gemma-4-vs-llama-4-vs-mistral-small-4-llm-comparison-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Always read each model's official license before commercial deployment — this post is not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I Engineered How AI Works for Me — My Claude Code Harness Setup</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sun, 12 Apr 2026 17:30:46 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/i-engineered-how-ai-works-for-me-my-claude-code-harness-setup-5a50</link>
      <guid>https://dev.to/lazydev_oh/i-engineered-how-ai-works-for-me-my-claude-code-harness-setup-5a50</guid>
      <description>&lt;h2&gt;
  
  
  What Is Harness Engineering
&lt;/h2&gt;

&lt;p&gt;A harness is originally the gear you put on a horse. Here it means the work framework you put on AI. It's not just writing better prompts — it's building a system that defines how Claude works.&lt;/p&gt;

&lt;p&gt;There are three components. &lt;strong&gt;Rules&lt;/strong&gt; is the CLAUDE.md at the project root — the rules Claude must always follow in this project. &lt;strong&gt;Commands&lt;/strong&gt; are saved files for repeated task requests: things like /workflow and /plan-and-spec. &lt;strong&gt;Hooks&lt;/strong&gt; are logic that runs automatically before or after specific actions. This is the most powerful layer of the three.&lt;/p&gt;

&lt;p&gt;Rules define "what to do." Commands define "how to do it." Hooks enforce "what must never happen." The three layers combine into a single framework.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Background on This Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Commands and Hooks concepts exist in Anthropic's official Claude Code documentation. Harness engineering is a method of combining these features to automate an entire personal development workflow. It uses official features — not hidden ones.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Full Structure at a Glance
&lt;/h2&gt;

&lt;p&gt;The file structure is the fastest way to understand it. It's split into a global area and a per-project area. The global area is shared across every project on your machine. Set it up once and it carries over to every project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/                        ← shared across machine (set up once)
  commands/
    init-harness.md               ← auto-build harness (this file)
    new-project.md                ← idea → project setup
    session-resume.md             ← restore context on session resume
  hooks/
    block-dangerous.sh            ← block dangerous commands
  settings.json                  ← hook wiring

project/                          ← auto-generated per project
  CLAUDE.md                      ← project rules
  progress.md                    ← current task state
  dev-log.md                     ← feature-by-feature dev log
  docs/                          ← store design documents
  screenshots/                   ← store screenshots
  .claude/commands/
    workflow.md                    ← full pipeline orchestrator
    plan-and-spec.md               ← design + fact-check (Planner)
    tdd.md                         ← implementation (Generator)
    ui-ux.md                       ← UI/UX research
    verify.md                      ← verification (Evaluator)
    commit.md                      ← commit + merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fat01tgypyzvkld7bnlc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fat01tgypyzvkld7bnlc1.png" alt="Full harness structure — global + project two-layer architecture" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key is separation. What can be used in any project goes global; what is only meaningful in this project goes inside the project. init-harness automatically creates the entire project area.&lt;/p&gt;

&lt;h2&gt;
  
  
  init-harness: From Analysis to Generation
&lt;/h2&gt;

&lt;p&gt;Whether starting a new project or attaching a harness to an existing one, you just run /init-harness. It proceeds automatically through five steps. No confirmation prompts in between. It shows one analysis summary, then moves straight to generation.&lt;/p&gt;

&lt;p&gt;Step 1 is project analysis. It reads package.json to identify the stack, checks the folder structure 3 levels deep, reads git log for commit patterns, and checks .env.example for integrated services. It only reads. It changes nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: project analysis (read only)&lt;/span&gt;

git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;     &lt;span class="c"&gt;# identify commit patterns&lt;/span&gt;
git branch &lt;span class="nt"&gt;-a&lt;/span&gt;             &lt;span class="c"&gt;# check branch strategy&lt;/span&gt;

&lt;span class="c"&gt;# output format after analysis&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;project analysis]
- stack:           Next.js 15 / TypeScript / Supabase
- structure:       App Router, no src/, feature-based folders
- commit pattern:  feat/fix/refactor prefix
- branch strategy: feature/&lt;span class="k"&gt;*&lt;/span&gt; → direct merge to main
- integrations:    Supabase, Resend, LemonSqueezy
- commands to generate: workflow, plan-and-spec, tdd, ui-ux, verify, commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 is CLAUDE.md generation. It fills in the tech stack, architecture rules, and absolute prohibitions from the analysis results. It's not a blank template — it's a file built from actually reading the project. Items like "i18n keys only for multilingual text," "no hardcoding," and "no force push to main" are inserted automatically.&lt;/p&gt;

&lt;p&gt;Step 3 is command file generation. workflow, plan-and-spec, tdd, ui-ux, verify, and commit are created inside .claude/commands/. Step 4 is generating the project base files: progress.md, dev-log.md, docs/, and screenshots/. Step 5 is checking global files. If session-resume, new-project, or block-dangerous.sh don't exist, it creates them; if they already exist, it leaves them alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5rxs537cfdq6t9kxao0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5rxs537cfdq6t9kxao0.png" alt="/workflow pipeline — branch → design → implement → verify → commit 5 steps" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Command Pipeline
&lt;/h2&gt;

&lt;p&gt;workflow.md is the orchestrator for the entire pipeline. Running /workflow "feature name" calls the rest in order. Each step automatically moves to the next when complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /workflow "feedback auto-classify feature"&lt;/span&gt;

0. assess current state    &lt;span class="c"&gt;# progress.md + git log --oneline -10&lt;/span&gt;
1. branch setup            &lt;span class="c"&gt;# feature/feedback-auto-classify&lt;/span&gt;
2. design + fact-check     &lt;span class="c"&gt;# run /plan-and-spec → Planner&lt;/span&gt;
3. implement               &lt;span class="c"&gt;# run /tdd → Generator&lt;/span&gt;
4. verify                  &lt;span class="c"&gt;# run /verify → Evaluator&lt;/span&gt;
5. commit + merge          &lt;span class="c"&gt;# run /commit&lt;/span&gt;

&lt;span class="c"&gt;# if verification fails → go back to step 3, fix, then re-verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plan-and-spec forces a web-search fact-check before implementation. It first confirms whether the library actually exists and whether the API can actually be implemented. If anything is 100% impossible, it proposes an alternative. The design document is saved to docs/spec-featurename.md.&lt;/p&gt;

&lt;p&gt;tdd breaks things into feature units and implements them one at a time. It includes a step where mock data is injected into each completed screen to make it look real. This bakes in the pattern from EP.08 where I took a screenshot every time I finished a platform-specific widget.&lt;/p&gt;

&lt;p&gt;verify is the step that compares the design document against the actual implementation. It checks for missing features, untranslated strings, and build errors. If there are failures, it outputs the fix method alongside them and returns to tdd. It's not just "does it run" — it's "was it built as designed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Planner → Generator → Evaluator
&lt;/h2&gt;

&lt;p&gt;The core of the command pipeline is the three-role separation. A single Claude switches roles as it works. plan-and-spec takes the Planner role, tdd takes Generator, verify takes Evaluator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Planner&lt;/strong&gt; designs and fact-checks before implementation. It first verifies "is this even possible." It web-searches to confirm the library actually exists, checks similar implementation examples, and reviews API constraints — then produces a design document. This step exists to prevent the situation where you start implementing without fact-checking and get stuck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generator&lt;/strong&gt; implements feature by feature while reading the design document. If the design changes during implementation, it immediately updates docs/spec-*.md and states the reason. Keeping design and code in sync is the key. There is no "I'll update it later."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluator&lt;/strong&gt; compares the design document against the actual implementation. It checks for missing features, untranslated strings, and missing error handling. The Generator does not self-verify. By separating roles, things the implementor is likely to miss get caught by different eyes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What Actually Happened With the EP.05 Clustering Feature&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When building the automatic feedback clustering, the Planner fact-checked the actual parameters for pgvector and the Voyage AI embedding API. The Generator implemented clustering.ts — 188 lines — feature by feature. The Evaluator compared it against the design document and caught a missing cosine similarity threshold handler. Before this, that kind of omission was the type I only caught after shipping.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Hooks Enforce
&lt;/h2&gt;

&lt;p&gt;Commands are optional, but hooks run automatically. Before Claude executes any bash command, block-dangerous.sh runs first. If it returns exit 2, the command is blocked. It blocks two things.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# block direct force push to main&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"push.*--force.*main&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;push.*main.*--force"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"main force push blocked"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# block .env commit&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"git add.*&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;env&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env file commit blocked"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is wired up as a PreToolUse hook in settings.json. It's automatically called before Claude executes the Bash tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;~/.claude/settings.json&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;"hooks"&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;"PreToolUse"&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;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash ~/.claude/hooks/block-dangerous.sh"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantage of hooks is that they block mistakes at the source. Even if a force push to main slips in by accident, the system stops it. It's not a human checking — it's the system blocking. Having just these two in place defends against the most catastrophic mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Automation Mode
&lt;/h2&gt;

&lt;p&gt;With the harness set up, running Claude Code with the --dangerously-skip-permissions flag lets it run to completion on its own without asking for confirmation. No "is it okay to do this" in the middle. Run /workflow and everything from branch creation to commit and merge happens automatically.&lt;/p&gt;

&lt;p&gt;This mode is not the dangerous part. Using it without hooks is dangerous. The hooks block main force push and .env commits, so the two critical mistakes are automatically defended against. Set up the harness first, then use it — that's the order.&lt;/p&gt;

&lt;p&gt;When a session drops and resumes, I use /session-resume. It reads the last 30 lines of progress.md and dev-log.md, plus git log and git status, then summarizes "how far we got and what's next" before picking right back up. It pairs with the CLAUDE.md automation from EP.01. One maintains the rules; the other restores the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;A few things changed after building the harness. In numbers, it looks like this.&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;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starting a new project&lt;/td&gt;
&lt;td&gt;explaining rules every time (10–15 min)&lt;/td&gt;
&lt;td&gt;/init-harness, one line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature dev order&lt;/td&gt;
&lt;td&gt;repeating the order every time&lt;/td&gt;
&lt;td&gt;/workflow "feature name"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session resume&lt;/td&gt;
&lt;td&gt;re-explaining context&lt;/td&gt;
&lt;td&gt;/session-resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI dev research&lt;/td&gt;
&lt;td&gt;requesting separately each time&lt;/td&gt;
&lt;td&gt;built into ui-ux.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design docs&lt;/td&gt;
&lt;td&gt;forgotten, not written&lt;/td&gt;
&lt;td&gt;auto-generated + auto-updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There was an unexpected result. I built this system because I was lazy, but I ended up working more carefully. I started writing design documents. I started doing fact-checks. The steps I used to skip because they were annoying are now baked into the commands — so they just happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q. If I run init-harness, do I not need to write CLAUDE.md myself?
&lt;/h3&gt;

&lt;p&gt;It auto-generates one, but it doesn't fully replace writing it yourself. What init-harness creates is a draft based on the project analysis. Team conventions, specific library constraints, and deployment environment details still need to be added manually. Use it as: auto-generate, then fill in the gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Do I have to use commands like /workflow every time?
&lt;/h3&gt;

&lt;p&gt;No. Simple bug fixes or small changes can just be asked in plain language. Commands are only for feature development that needs to go all the way from start to finish properly. Only hooks are always running in the background — everything else is optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Isn't the --dangerously-skip-permissions flag dangerous?
&lt;/h3&gt;

&lt;p&gt;It's dangerous without hooks. The hooks block main force push and .env commits, so the order is: set up the harness first, then use it. It's not the flag that's dangerous — using it without hooks is. With just these two in place, full automation mode is usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Can this structure be used for team projects?
&lt;/h3&gt;

&lt;p&gt;This structure was designed for solo development. To use it on a team, you'd need to modify the direct-to-main merge section and the branch strategy. Update CLAUDE.md to match team conventions and add a PR creation step to commit.md. The structure itself can be adapted for teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

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

&lt;p&gt;Setting up this structure took a day or two. Writing command files, making hooks, testing the flow. At first I wasn't sure it was right. Now, every time I start a new project, /init-harness handles everything — so that time wasn't wasted.&lt;/p&gt;

&lt;p&gt;The harness is never finished. As you use it, the gaps become visible, and you update the command files each time. It's not about building a perfect framework — it's about cutting out annoying things one by one. That's the Lazy Developer way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-claude-code-harness-engineering-setup-ep17" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Harness Engineering — The Environment Matters More Than the Prompt</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sun, 12 Apr 2026 16:37:09 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/harness-engineering-the-environment-matters-more-than-the-prompt-3cod</link>
      <guid>https://dev.to/lazydev_oh/harness-engineering-the-environment-matters-more-than-the-prompt-3cod</guid>
      <description>&lt;h2&gt;
  
  
  What Is a Harness
&lt;/h2&gt;

&lt;p&gt;The word comes from a horse harness. The harness is the entire apparatus that guides the horse called AI in the desired direction. It's the concept of designing not the model itself, but the entire environment in which that model works. It's not about a single prompt line — it's about setting up the entire board.&lt;/p&gt;

&lt;p&gt;Context files, skill files, MCP servers, and execution loops are all included. Verification processes and human intervention points are all part of the harness. Humans are part of the harness too. It is not a system where AI runs alone.&lt;/p&gt;

&lt;p&gt;It's not about what to ask AI. It's about how to set up the board where AI can work. The core argument is that the environment is the bottleneck, not the model. That's why results can differ 10x with the same model depending on environment setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limits of Prompt Engineering
&lt;/h2&gt;

&lt;p&gt;Once, the single phrase "think step by step" (Chain-of-Thought) boosted math accuracy by 39%p (Wei et al. 2022, GSM8K benchmark). A 2025 Wharton GAIL study remeasured with the latest models and found it dropped to about 3%p. Evidence that as models advance, the effect of prompt tricks is disappearing.&lt;/p&gt;

&lt;p&gt;The more advanced the model, the more built-in reasoning it already has. There is less room for prompt manipulation. No matter how sophisticated a system prompt is, there are fundamental limits. Designing structure yields better long-term returns than spending time hunting for tricks.&lt;/p&gt;

&lt;p&gt;A harness, by contrast, can be reused even as models are upgraded. The structure itself is not tied to a specific model. A CLAUDE.md written this year will work the same with next year's models. This is why investing in harness rather than prompts is the right move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-Stage Evolution: Prompt → Context → Harness
&lt;/h2&gt;

&lt;p&gt;There are three stages. Prompt engineering → context engineering → harness engineering. Each maps to "What to ask → What to show → How to design." We are now at the transition into the third stage.&lt;/p&gt;

&lt;p&gt;Context engineering is deciding what ingredients to give AI. Harness is designing the entire kitchen that holds those ingredients. Good ingredients alone are not enough. Tool placement and task sequence must be designed together.&lt;/p&gt;

&lt;p&gt;There is a case from the medical field that demonstrates this difference. Providing relevant data and restricting scope reduced hallucinations from 40% to 0%. Without changing the model. Changing the context alone changed the results. Harness is a broader concept that includes this context design.&lt;/p&gt;

&lt;h2&gt;
  
  
  5 Core Components
&lt;/h2&gt;

&lt;p&gt;The 5 core elements of a harness are Agent.md (CLAUDE.md), Skills, MCP, Hooks, and Sub-agents. Each has a different role and they are used in combination. What makes the difference is not which tool you use, but how well you design these 5 structures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;Key Caution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent.md&lt;/td&gt;
&lt;td&gt;Documenting project rules&lt;/td&gt;
&lt;td&gt;New employee onboarding manual&lt;/td&gt;
&lt;td&gt;Keep under 300 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;Separating task-specific expertise&lt;/td&gt;
&lt;td&gt;Department-specific reference files&lt;/td&gt;
&lt;td&gt;Separate file per task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;Connecting external tools&lt;/td&gt;
&lt;td&gt;USB hub&lt;/td&gt;
&lt;td&gt;Connect only what's needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks&lt;/td&gt;
&lt;td&gt;Mandatory execution rules&lt;/td&gt;
&lt;td&gt;Automatic checklist&lt;/td&gt;
&lt;td&gt;Use instead of prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-agents&lt;/td&gt;
&lt;td&gt;Parallel task processing&lt;/td&gt;
&lt;td&gt;Team member division of labor&lt;/td&gt;
&lt;td&gt;Verify dependencies first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftii4wwy2e44auki7qr0x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftii4wwy2e44auki7qr0x.png" alt="Harness Engineering 5 Core Elements — Agent.md, Skills, MCP, Hooks, Sub-Agent" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This structure can be applied equally in both Claude Code and Cursor. The harness structure works the same regardless of the tool. Whether you have this structure matters more than which AI coding tool you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Write CLAUDE.md Properly
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md (or Agent.md) is the foundation file of the harness. It documents project structure, coding rules, and standards AI must follow. The general consensus is under 300 lines. HumanLayer stated they keep it under 60 lines in their own projects. An ETH Zurich study found that auto-generated CLAUDE.md by LLMs actually increased token costs by 20% and lowered performance. Writing it yourself is better.&lt;/p&gt;

&lt;p&gt;The key is not writing things that Claude can figure out by reading the code. Standard language conventions, per-file descriptions — these are left out. Conversely, things that cannot be known from code alone must be written. Bash commands, branch rules, project-specific architecture decisions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What to Write&lt;/th&gt;
&lt;th&gt;What to Skip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bash commands Claude cannot infer&lt;/td&gt;
&lt;td&gt;Things derivable by reading the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code style rules that differ from defaults&lt;/td&gt;
&lt;td&gt;Standard language conventions (Claude already knows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test runner / how to run tests&lt;/td&gt;
&lt;td&gt;Detailed API docs (just link them)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch naming, PR rules&lt;/td&gt;
&lt;td&gt;Frequently changing information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment quirks, required env vars&lt;/td&gt;
&lt;td&gt;"Write clean code"-style obvious things&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scope of application varies by file location. &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; applies to all projects. &lt;code&gt;CLAUDE.md&lt;/code&gt; at the project root applies only to that project. &lt;code&gt;CLAUDE.local.md&lt;/code&gt; is for personal settings and goes in .gitignore. In a monorepo, placing separate CLAUDE.md files in subdirectories applies them hierarchically.&lt;/p&gt;

&lt;p&gt;Not everything goes into CLAUDE.md. The Progressive Disclosure pattern of referencing separate documents is key. Here's what it looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Code style
- Use ES modules (import/export), not CommonJS (require)
- Destructure imports when possible

# Workflow
- Typecheck when done making code changes
- Run single tests, not the full suite

# References
See @README.md for project overview
Git workflow: @docs/git-instructions.md
Personal overrides: @~/.claude/my-project-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CLAUDE.md Writing Principles&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 300 lines is the general consensus. Shorter is better. Longer makes AI slower&lt;/li&gt;
&lt;li&gt;Not a manual, but an index. Write only "where to find what"&lt;/li&gt;
&lt;li&gt;Don't write things Claude can figure out by reading the code&lt;/li&gt;
&lt;li&gt;Separate detailed rules into separate documents and reference with &lt;code&gt;@path&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove stale documents immediately. AI follows outdated rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Skill File Design
&lt;/h2&gt;

&lt;p&gt;Skill files are task-specific expertise separated into the &lt;code&gt;.claude/skills/&lt;/code&gt; folder. The difference from CLAUDE.md is "always loaded vs. loaded only when needed." Putting everything in CLAUDE.md wastes the context window. Skills are only loaded when that task is being performed.&lt;/p&gt;

&lt;p&gt;The actual directory structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/skills/
  api-conventions/
    SKILL.md        # main (required)
    reference.md    # detailed docs (loaded on demand)
    examples.md     # usage examples
  fix-issue/
    SKILL.md
  deploy/
    SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; in the SKILL.md frontmatter lets you invoke it as a slash command. Setting &lt;code&gt;disable-model-invocation: true&lt;/code&gt; means it won't be invoked automatically by AI — it only runs when a human calls it directly. There are two types: reference skills and task skills:&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;# Reference — API rules (auto-loaded by Claude)&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;api-conventions&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REST API design conventions&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Use kebab-case for URL paths&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Use camelCase for JSON properties&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Always include pagination for list endpoints&lt;/span&gt;

&lt;span class="c1"&gt;# Task — Fix GitHub issue (/fix-issue 123)&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;fix-issue&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fix a GitHub issue&lt;/span&gt;
&lt;span class="na"&gt;disable-model-invocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;Analyze and fix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$ARGUMENTS&lt;/span&gt;
&lt;span class="s"&gt;1. Check issue with `gh issue view`&lt;/span&gt;
&lt;span class="s"&gt;2. Search code → fix → test → create PR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a case of applying this to video production automation. Researcher, script, subtitles, voice, scene design, rendering, QA — split into 7 agent skills. Work that took 9 hours was reduced to 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks — Mandatory Rules AI Cannot Ignore
&lt;/h2&gt;

&lt;p&gt;Hooks are mechanisms that force-execute tasks that AI might ignore even when instructed via prompt. Registered in &lt;code&gt;settings.json&lt;/code&gt;, they run automatically when specific events occur. Prompts can be skipped. Hooks cannot. Exit code 0 means allow, 2 means block.&lt;/p&gt;

&lt;p&gt;I documented the 5 most commonly used hooks in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;settings.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;practical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Hooks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;config&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;"hooks"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Auto-format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;edit&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&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;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_input.file_path' | xargs npx prettier --write"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dangerous&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(rm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-rf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--hard)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&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;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".claude/hooks/pre-bash-firewall.sh"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Auto-verify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;completion&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&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;"hooks"&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;"prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Check if all tasks are complete. Continue if incomplete."&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;macOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;desktop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notification&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Notification"&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;"hooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osascript -e 'display notification &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Claude Code&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; with title &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Needs attention&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hooks that enforce package managers are also useful. In a pnpm project, if AI tries to use npm, it gets blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash — enforce pnpm Hook&lt;/span&gt;
&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // ""'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pnpm-lock.yaml &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="s1"&gt;'\bnpm\b'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This repo uses pnpm. Replace npm with pnpm."&lt;/span&gt; 1&amp;gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2  &lt;span class="c"&gt;# block&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0  &lt;span class="c"&gt;# allow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Things to Watch When Connecting MCP
&lt;/h2&gt;

&lt;p&gt;MCP is an adapter that connects AI agents to external tools. Like a USB hub — it attaches external capabilities like Gmail, browser automation, and document search to AI. In practice, it's used to automatically send a report email via Gmail MCP after completing Excel work.&lt;/p&gt;

&lt;p&gt;There is a caveat. Connecting more MCPs is not better. It wastes tokens and creates confusion. The principle is to connect only what's needed. If it feels like MCP is being forced into use, removing it is the right call.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MCP Connection Principles&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect only what's immediately needed&lt;/li&gt;
&lt;li&gt;The more connections, the higher the chance of AI judgment errors&lt;/li&gt;
&lt;li&gt;Disconnect MCPs that are not actually being used&lt;/li&gt;
&lt;li&gt;Excessively connected MCPs waste the context window&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Practical Application — Difference Seen in Before/After
&lt;/h2&gt;

&lt;p&gt;The OpenAI Codex experiment is the representative case. 5 months, 1 million lines of code, 0 lines written directly by humans. 1,500 PRs merged. One engineer completed an average of 3.5 tasks per day. In Terminal Bench 2.0, the same model (Opus 4.6) ranked 40th with the default harness (Claude Code defaults) and 1st with an optimized harness. The harness, not the model, determined the ranking.&lt;/p&gt;

&lt;p&gt;Mitchell Hashimoto (HashiCorp founder) summarized the core principle this way. "Every time the agent makes a mistake, engineer it so that mistake can never happen again." Not fixing mistakes, but building a structure that makes mistakes impossible.&lt;/p&gt;

&lt;p&gt;I summarized specifically how it differs in a Before/After breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (Prompts Only)&lt;/th&gt;
&lt;th&gt;After (Harness Applied)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Make an email validation function"&lt;/td&gt;
&lt;td&gt;"Write validateEmail. Tests: &lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt; → true, invalid → false. Run tests after implementing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Make the dashboard look nice"&lt;/td&gt;
&lt;td&gt;[Screenshot attached] "Implement this design. Take a screenshot of the result and compare with the original"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Build failed"&lt;/td&gt;
&lt;td&gt;"Failed with this error: [paste error]. Don't suppress the error — fix the root cause"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full test output of 4,000 lines printed&lt;/td&gt;
&lt;td&gt;Hooks silence success, show only failures (Back-Pressure pattern)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common Anti-Patterns to Avoid&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kitchen Sink Session&lt;/strong&gt; — mixing unrelated tasks in one session → reset context with &lt;code&gt;/clear&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same fix repeated 3 times&lt;/strong&gt; — learning failure → new prompt reflecting the failure cause after &lt;code&gt;/clear&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLAUDE.md exceeding 200 lines&lt;/strong&gt; — context waste → keep only essentials, move the rest to Skills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endless exploration&lt;/strong&gt; — investigation request without scope → split into sub-agents&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fylja8v1sxds03fh1y6ow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fylja8v1sxds03fh1y6ow.png" alt="Same Model, Different Results — Before/After comparison with Terminal Bench and Codex stats" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is one common thread. The initial setup takes time. Once built, subsequent iterations automatically accelerate. The setup cost is one-time; the effect is cumulative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Principles in Design
&lt;/h2&gt;

&lt;p&gt;Giving only the final goal is not enough. Intermediate stage verification criteria must also be specified. Not "just produce the output" but "must pass this criteria at this stage to proceed to the next." When goals are vague, AI looks for shortcuts.&lt;/p&gt;

&lt;p&gt;Semi-automatic structures with explicit approval gates are more realistic than fully automatic. In the video production automation case, approval gates were placed at 4 points: script, voice, scene design, and QA. Not AI running to the end alone, but humans checking in between. At the current level of technology, this is the realistic choice.&lt;/p&gt;

&lt;p&gt;Fix code patterns before they go wrong. Lock good rules in with linters and tests. Remove stale rules quickly. If AI follows incorrect past rules, problems accumulate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Design Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context file (CLAUDE.md etc.): under 300 lines, short table-of-contents style&lt;/li&gt;
&lt;li&gt;MCP: only what's needed. More connections means more confusion&lt;/li&gt;
&lt;li&gt;Remove stale documents. If AI follows old rules, work goes wrong&lt;/li&gt;
&lt;li&gt;Design human intervention gates explicitly&lt;/li&gt;
&lt;li&gt;Specify verification criteria for each intermediate stage&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  You Don't Have to Be a Developer
&lt;/h2&gt;

&lt;p&gt;Harness engineering is not a concept exclusive to development. The same structure works for general tasks like Excel automation, video production, and data analysis. Even people unfamiliar with vibe-coding can design a harness. You can start by writing a single CLAUDE.md file first.&lt;/p&gt;

&lt;p&gt;The role of developers is changing. From people who write code to people who design environments where agents can work well. The core of AI development has shifted from prompts to harness. If you're just starting out, begin with a single CLAUDE.md.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q. What is the difference between harness engineering and prompt engineering?
&lt;/h3&gt;

&lt;p&gt;If prompt engineering is the craft of refining what and how to ask AI, harness engineering is the craft of designing the entire environment in which AI works. It includes context files, skills, MCP, hooks, execution loops, verification processes, and human intervention points. A prompt is part of the harness. Harness is a bigger concept than prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. How long should CLAUDE.md be?
&lt;/h3&gt;

&lt;p&gt;Under 300 lines is recommended. The longer it is, the more context window it consumes, degrading AI performance. Keeping it short like a table of contents, not a long manual, is key. Writing only "where to find what" is enough. Separate detailed rules into skill files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Is it better to connect more MCPs?
&lt;/h3&gt;

&lt;p&gt;No. Connecting more MCPs leads to token waste and confusion. The principle is to connect only what's needed. The more connections, the higher the cost of AI deciding which tool to use. If it feels like it's being forced, removing it is the right move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Can non-developers apply harness engineering?
&lt;/h3&gt;

&lt;p&gt;Yes. Harness engineering is not a concept exclusive to development. The same structure works for general tasks like Excel automation, video production, and data analysis. Even people unfamiliar with vibe-coding can design a harness. It's simply a matter of documenting what rules AI should follow to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Where should harness design start?
&lt;/h3&gt;

&lt;p&gt;Starting by writing a CLAUDE.md (or Agent.md) file is the fastest approach. Documenting project structure, code rules, and completion criteria in under 300 lines is the first step. MCP or skill files come after. There's no need to have all five elements from the start.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Official Documentation and References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;Anthropic — Claude Code Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Anthropic — Hooks Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;Anthropic — Skills Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md" rel="noopener noreferrer"&gt;HumanLayer — Writing a good CLAUDE.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents" rel="noopener noreferrer"&gt;HumanLayer — Skill Issue: Harness Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;The figures in this article (OpenAI Codex 1 million lines, Terminal Bench 2.0, Wharton GAIL CoT study, etc.) are cited from public announcements and papers. Some figures require separate verification from the original papers or official blog posts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last updated: April 2026. Harness engineering is a rapidly evolving concept. Tools, APIs, and configuration methods can change at any time.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-ai-agent-harness-engineering-guide-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Was Tired of Sorting User Feedback, So I Let AI Classify It</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sat, 11 Apr 2026 01:00:03 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/i-was-tired-of-sorting-user-feedback-so-i-let-ai-classify-it-5dl5</link>
      <guid>https://dev.to/lazydev_oh/i-was-tired-of-sorting-user-feedback-so-i-let-ai-classify-it-5dl5</guid>
      <description>&lt;p&gt;&lt;em&gt;April 2026 · Lazy Developer EP.05&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In EP.04, I built FeedMission in 7 days. Feedback started coming in. At first, it was great. But once it starts piling up, a different problem emerges. "Please add dark mode." "It hurts my eyes at night." "Add a background color option." Three people said three different things, but the request is the same. Manually grouping these is fine when there are 10. Past 50, just reading them eats up your time.&lt;/p&gt;

&lt;p&gt;I asked Claude: "Can you automatically group similar feedback?" The answer was "embeddings."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick overview&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert feedback text into a 1024-number array with Voyage AI (embeddings)&lt;/li&gt;
&lt;li&gt;Simultaneously analyze sentiment scores (-1.0 to 1.0) with Claude Haiku&lt;/li&gt;
&lt;li&gt;Enable the pgvector extension on PostgreSQL for vector storage + similarity search&lt;/li&gt;
&lt;li&gt;Cosine similarity &amp;gt;= 0.85 means same group; below that creates a new group&lt;/li&gt;
&lt;li&gt;When a group grows, Claude automatically re-generates the name and summary&lt;/li&gt;
&lt;li&gt;The entire pipeline runs in the background via the after() pattern&lt;/li&gt;
&lt;li&gt;It all fits in 188 lines of clustering.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  "Group similar feedback" — but how?
&lt;/h2&gt;

&lt;p&gt;"Please add dark mode" and "It hurts my eyes at night" share zero words. But they're the same request. How do you tell a computer that? You convert the sentence into 1024 numbers. Similar meanings produce similar numbers. Think of it like a food delivery app — searching "late night cravings" finds both fried chicken and pork feet at the same time. It searches by meaning, not by matching words.&lt;/p&gt;

&lt;p&gt;"Why not just send two pieces of feedback to Claude and ask if they're similar?" Of course that works. But with 100 feedback items, that's 4,950 comparison pairs. Calling the Claude API for each one is unmanageable in both time and cost. With embeddings, you create them once and the DB handles comparisons with math. Measuring distances between numbers finishes in milliseconds.&lt;/p&gt;

&lt;p&gt;Embeddings aren't a silver bullet, though. They're weak with numbers. "I need 3 buttons" and "I need 30 buttons" have completely different meanings, but their embedding coordinates come out nearly identical. Negation is the same problem. "Dark mode is great" and "Dark mode is terrible" land on similar coordinates. I'm aware of this. But given the nature of feedback, these cases weren't common. Build a structure that covers 80% quickly, and fix the remaining 20% when actual problems arise.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first embedding with Voyage AI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/ai/embeddings.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.voyageai.com/v1/embeddings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&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;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voyage-3-lite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please add dark mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// → [0.0234, -0.0891, 0.0412, ...] (1024 numbers)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran sentiment analysis at the same time with Claude Haiku. "Please add dark mode" came back as 0.1 (neutral), "Why isn't this available yet?" came back as -0.4 (negative). Same request, different temperature. Both tasks run simultaneously with &lt;code&gt;Promise.all&lt;/code&gt;. Sequential takes 500ms; parallel takes 300ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  pgvector — storing 1024 numbers in the DB
&lt;/h2&gt;

&lt;p&gt;There are dedicated vector DBs like Pinecone. But I was already using Supabase PostgreSQL. pgvector lets you store and compare vectors in your existing DB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// prisma/schema.prisma
model Feedback {
  embedding  Unsupported("vector(1024)")?  // ← this
  sentiment  Float?
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Prisma doesn't officially support pgvector. Declaring it as &lt;code&gt;Unsupported&lt;/code&gt; creates the schema, but you can't read or write this column through the normal Prisma API. You have to write raw SQL — a hybrid approach. Not elegant, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Find things similar to this" — similarity search
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&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;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"Feedback"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"projectId"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;  &lt;span class="c1"&gt;-- exclude self (important!)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&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;vector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;AND id != $3&lt;/code&gt;. Forgetting this one cost me quite a while. The item matched itself with similarity 1.0. Obviously. It's the most similar to itself. Every new piece of feedback joined an existing cluster, and new clusters never got created. Fixed with one line, but took a while to find.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The first trap in vector similarity search&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't exclude the item itself from results, you always get similarity 1.0. Every piece of feedback gets classified as "identical to something that already exists," and new groups never get created. A single line — &lt;code&gt;AND id != $3&lt;/code&gt; — determines the correctness of the entire logic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  0.85 was the answer — how I chose the threshold
&lt;/h2&gt;

&lt;p&gt;I created 20 pieces of test feedback and experimented directly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Dark mode request" and "UI color change" in the same group — related but different requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"CSV export" and "data download" in the same group — borderline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.85&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Please add dark mode" and "It hurts my eyes at night" in the same group — correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only nearly identical sentences matched — too strict&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Cluster assignment — join or create
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// clustering.ts:87&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;similar&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;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;similarity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clusterId&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;bestMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// join existing group&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ask Claude for a name and create new group&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time a group grows, the name gets regenerated at multiples of 3 (or at 2). Calling Claude every time would inflate API costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Claude names the groups
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Claude response example
{ "title": "Dark Mode / Theme Custom",
  "summary": "Users are requesting dark mode and theme color options" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked well. But occasionally Claude wrapped the JSON in a code block. Running &lt;code&gt;JSON.parse&lt;/code&gt; on that obviously throws an error. I added a defensive regex to strip the code block markers. AI responses are never 100% predictable. You always need a fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Priority — what to look at first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// clustering.ts — priority formula
votes 50% + feedback count 30% + recency 20%

// 50 votes = max score, 10 feedbacks = max score
// recency hits 0 after 50 days since last feedback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This formula produces a score between 0 and 100. 70+ shows in red, 40+ in yellow, below that in green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzw8o9bl8fcdkrhzjk2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzw8o9bl8fcdkrhzjk2q.png" alt="FeedMission cluster list" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;AI auto-classified cluster list / GoCodeLab&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The after() pattern — users don't wait
&lt;/h2&gt;

&lt;p&gt;This entire pipeline runs inside the feedback submission API. Making the user wait 2 seconds after pressing "Submit Feedback" is bad UX.&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;// app/api/feedback/route.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// ↑ immediately return "received"&lt;/span&gt;

&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processFeedbackAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// ↑ embedding + clustering runs in background&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzms02m3bm1e4xgiar2ef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzms02m3bm1e4xgiar2ef.png" alt="FeedMission AI pipeline" width="800" height="814"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Feedback → AI clustering pipeline full flow / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems I ran into
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-similarity of 1.0&lt;/strong&gt; — Forgot &lt;code&gt;AND id != $3&lt;/code&gt;, so new groups never got created&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NaN mixed into embeddings&lt;/strong&gt; — Without &lt;code&gt;isFinite()&lt;/code&gt; validation, the DB gets corrupted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority score of 0 for new groups&lt;/strong&gt; — Only called &lt;code&gt;recalculatePriority()&lt;/code&gt; when joining an existing group&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude wrapping JSON in code blocks&lt;/strong&gt; — Regex stripping + try-catch + fallback are essential&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automated feedback classification in 188 lines
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clustering.ts&lt;/code&gt; — the entire pipeline fits in 188 lines. 2 external APIs (Voyage + Claude), 5-7 DB queries, 1 branch. All in one file, so the flow is easy to follow.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What is an embedding?&lt;/strong&gt;&lt;br&gt;
It converts a sentence into an array of numbers. Sentences with similar meaning produce similar number patterns, and comparing the numbers lets you calculate "how similar" they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is pgvector needed?&lt;/strong&gt;&lt;br&gt;
It's an extension that adds vector storage + similarity search to existing PostgreSQL. No separate vector DB needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How did you decide on 0.85?&lt;/strong&gt;&lt;br&gt;
Experimented with 20 pieces of feedback. 0.7 was too broad, 0.9 was too narrow. At 0.85, "the same request worded differently" grouped well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you use pgvector with Prisma?&lt;/strong&gt;&lt;br&gt;
No official support yet. Declare the vector column as Unsupported and use raw SQL for reads/writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the after() pattern?&lt;/strong&gt;&lt;br&gt;
A pattern that sends the response first and runs additional work in the background. The user doesn't wait.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-feedmission-ai-feedback-clustering-ep05" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>feedback</category>
      <category>postgres</category>
    </item>
    <item>
      <title>I Built a SaaS in 7 Days</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sat, 11 Apr 2026 01:00:02 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/i-built-a-saas-in-7-days-25e4</link>
      <guid>https://dev.to/lazydev_oh/i-built-a-saas-in-7-days-25e4</guid>
      <description>&lt;p&gt;&lt;em&gt;April 2026 · Lazy Developer EP.04&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After building Apsity in EP.02, feedback from 12 apps started pouring in. Emails, reviews, DMs. At first I organized them in a spreadsheet. But "please add dark mode" and "my eyes hurt at night" are the same request, just written by different people in different words. Grouping similar requests, assigning priorities, notifying when they're resolved. All manual. The spreadsheet kept growing but never got organized.&lt;/p&gt;

&lt;p&gt;There's a service called Canny. It does exactly this. Feedback collection, voting, roadmap. But the pricing starts at $79/month. Too much for an indie developer. If existing tools are too expensive or don't fit, I build my own.&lt;/p&gt;

&lt;p&gt;I decided to build a SaaS with everything: feedback collection, AI auto-classification, public roadmap, changelog, voting, and email notifications. Named it FeedMission. This post is the record of how it started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttyx873hto49jd503td9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttyx873hto49jd503td9.png" alt="FeedMission landing page" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The finished FeedMission landing page / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Overview&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canny at $79/mo → too expensive for indie devs → decided to build it myself&lt;/li&gt;
&lt;li&gt;Designed AI clustering on top of the Next.js + Supabase stack I learned from Apsity&lt;/li&gt;
&lt;li&gt;Handed Claude a structured spec → MVP of 10,742 lines in 52 minutes&lt;/li&gt;
&lt;li&gt;9 DB models, 12 APIs, 8 dashboard pages, widget, AI pipeline — all in one commit&lt;/li&gt;
&lt;li&gt;The real work came after the MVP — structural changes, performance, and security took far more time&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  I defined what I was building
&lt;/h2&gt;

&lt;p&gt;I didn't just tell Claude "make me a feedback tool." I had the Apsity experience. I knew that specific requirements produce specific results. I signed up for Canny, Nolt, and Fider and used them myself. Features they all shared: feedback boards, voting, roadmap, changelog. That's the baseline. But I wanted one more thing — when feedback piles up, automatically group similar items together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// My FeedMission requirements
Core: Feedback collection widget + public board + voting
Management: Roadmap kanban + changelog + email notifications
AI: Auto-classify similar feedback (embeddings + clustering)
AI: Sentiment analysis + auto-generated insights
Revenue: FREE / STARTER $9 / PRO $19 plans
Platforms: Script + React + iOS + Android + iframe + GTM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The MVP came out in 52 minutes
&lt;/h2&gt;

&lt;p&gt;March 26, 9:41 AM. Started the project with &lt;code&gt;create-next-app&lt;/code&gt;. Fed Claude the organized requirements and started building.&lt;/p&gt;

&lt;p&gt;10:33 AM. Pushed the commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;234006b feat: FeedMission full MVP implementation
73 files changed, 10742 insertions(+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;52 minutes. 73 files. 10,742 lines. Vibe coding is fast, but the reason isn't "Claude wrote the code" — it's "I knew exactly what I was building." When requirements are clear, Claude's output is precise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was inside the MVP
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 9 DB models (Prisma)
User, Project, Feedback, Cluster, RoadmapItem,
Changelog, Vote, NotificationLog, Subscription

// 12 API routes
/api/feedback — feedback CRUD + widget CORS
/api/clusters — AI cluster view/edit
/api/roadmap — roadmap kanban CRUD
/api/changelog — changelog + auto email on publish
/api/dashboard — stats aggregation (8 queries in parallel)
/api/insights — AI insight card generation

// 8 dashboard pages
Overview, Feedback, Clusters, Roadmap,
Changelog, Notifications, Widget, Settings

// 3 AI pipeline files
clustering.ts — feedback → embedding → cluster assignment
embeddings.ts — Voyage AI vector generation + Claude sentiment analysis
summaries.ts — Claude generates cluster titles/summaries + insights
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Feedback model has an &lt;code&gt;embedding vector(1024)&lt;/code&gt; column. Feedback text gets converted into 1024 numbers via Voyage AI and stored. pgvector handles similarity search on these numbers. "Please add dark mode" and "my eyes hurt at night" end up with similar number patterns and automatically get grouped together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzduauvm3vnkajs0yfxcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzduauvm3vnkajs0yfxcu.png" alt="FeedMission dashboard" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;FeedMission Dashboard Overview / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between "working code" and "product"
&lt;/h2&gt;

&lt;p&gt;It built successfully. No type errors either. But the moment I actually used it, things to fix started piling up.&lt;/p&gt;

&lt;p&gt;First: the sidebar was eating too much screen space. Switching to a top navigation took 4 minutes. Second: UUIDs were baked into the URLs. I refactored to slug-based routing — 13 files were referencing &lt;code&gt;params.projectId&lt;/code&gt;. Third: after deploying to production, it was slow. The Vercel Function was running in the US, while the Supabase DB was in Seoul. Every query was crossing the Pacific Ocean.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The reality of vibe coding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why you can't ship AI-generated code as-is. Region settings, middleware optimization, security vulnerabilities, CLS — these only become visible when you actually run and use the code. Claude generates the first draft quickly, and I spend my time asking: "Why is this slow?", "Is this URL structure right?", "Should this data really be exposed?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What happened over the next few days
&lt;/h2&gt;

&lt;p&gt;By midnight on Day 1, I had 5 performance-related commits stacked up. Changed the Vercel region to Seoul (icn1), skipped unnecessary auth calls for public routes in middleware, added Prisma singleton caching, and matched skeleton heights to eliminate layout shift.&lt;/p&gt;

&lt;p&gt;For 5 days I didn't touch the code and just used it myself.&lt;/p&gt;

&lt;p&gt;On Day 6: improved 38 files in one go. 7 security patches, 6 DB indexes, dashboard parallel query optimization. Expanded the widget SDK to 5 types, built iOS SwiftUI and Android Kotlin native widgets. Integrated LemonSqueezy payments and pivoted pricing from KRW to USD. Along the way, I accidentally committed 686K lines of node_modules and pushed a deletion commit 28 seconds later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7awazvzxtrq967z612pd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7awazvzxtrq967z612pd.png" alt="FeedMission 7-day commit timeline" width="800" height="577"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;7-day timeline of 51 commits / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Commits&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Co-Authored&lt;/td&gt;
&lt;td&gt;37 (72.5%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active coding days&lt;/td&gt;
&lt;td&gt;3 (out of 7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVP generation time&lt;/td&gt;
&lt;td&gt;52 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  72.5% was AI, the rest was judgment
&lt;/h2&gt;

&lt;p&gt;37 out of 51 commits have the Claude Co-Authored-By tag. 72.5%. This doesn't mean "Claude built 72.5% of it." I organize the requirements, Claude generates code, I review, modify, and commit.&lt;/p&gt;

&lt;p&gt;This is why vibe coding isn't "letting AI do everything." Build fast, use it fast, decide fast. What speeds up isn't code generation — it's the entire feedback loop.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is a 52-minute MVP actually usable?&lt;/strong&gt;&lt;br&gt;
"Working code" came out in 52 minutes. But bringing it to product quality took the remaining 6 days. The MVP is a starting point, not the finish line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is building your own better than using Canny?&lt;/strong&gt;&lt;br&gt;
Depends on team size and budget. If $79/month is a stretch and you need custom features like AI auto-classification, building your own might be the way to go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does AI clustering work?&lt;/strong&gt;&lt;br&gt;
Feedback text gets converted into 1024 numbers (embeddings). Sentences with similar meanings produce similar number patterns. Comparing them and grouping items with similarity above 0.85 into the same cluster. Covered in detail in EP.05.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is code quality from vibe coding acceptable?&lt;/strong&gt;&lt;br&gt;
It works at the MVP stage, but you can't ship it as-is. I separately fixed 7 security vulnerabilities and 4 performance issues. AI generates the first draft, but human review is always required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-feedmission-saas-7days-mvp-ep04" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>startup</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
