<?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: James Anderson</title>
    <description>The latest articles on DEV Community by James Anderson (@james_anderson_h).</description>
    <link>https://dev.to/james_anderson_h</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3968038%2Fe70ff7fe-85b0-4c3a-8a68-b2269cd108b7.png</url>
      <title>DEV Community: James Anderson</title>
      <link>https://dev.to/james_anderson_h</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/james_anderson_h"/>
    <language>en</language>
    <item>
      <title>Most 'AI Agents' Are Just If-Statements in a Trench Coat</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Tue, 08 Sep 2026 06:12:03 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/most-ai-agents-are-just-if-statements-in-a-trench-coat-3960</link>
      <guid>https://dev.to/james_anderson_h/most-ai-agents-are-just-if-statements-in-a-trench-coat-3960</guid>
      <description>&lt;p&gt;I built an agent last year, and I was proud of it.&lt;/p&gt;

&lt;p&gt;It had a planner. It had tools. It had a reasoning loop that decided what to do next, reflected on its own output, and chained steps together to get real work done. In the demo, it was genuinely impressive — the kind of thing that makes a room go "ooh."&lt;/p&gt;

&lt;p&gt;Then it went to production, and it was slow, expensive, and failed in ways I couldn't reproduce. Same input on Tuesday, different behavior on Wednesday. When it broke, the cause was three "autonomous decisions" upstream that I didn't control and couldn't see.&lt;/p&gt;

&lt;p&gt;So I did the unglamorous thing: I rewrote it as a boring, linear pipeline. Fixed steps. No reasoning loop. And it was better on every axis that mattered — faster, cheaper, testable, debuggable.&lt;/p&gt;

&lt;p&gt;Then I looked at the logs from the old "agent" and felt slightly sick. It did &lt;strong&gt;the same three steps every single time.&lt;/strong&gt; Extract, transform, respond. Every run. It never once used its precious autonomy to do anything different. I had built a for-loop, given it a system prompt, and called it an agent.&lt;/p&gt;

&lt;p&gt;I don't think I'm alone. I think most of what's being called an "agent" in 2026 is a pipeline in a trench coat — and I want to make the case that this is not an insult. It's a relief.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an "agent" actually is (the definition nobody pins down)
&lt;/h2&gt;

&lt;p&gt;"Agent" has become one of those words that means everything and therefore nothing. So let me pin the one distinction that actually matters, because the whole argument rests on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent decides its own control flow at runtime.&lt;/strong&gt; Which tool to call, which step comes next, whether to loop again, when to stop — the &lt;em&gt;model&lt;/em&gt; chooses the path, dynamically, based on what it sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pipeline has that control flow fixed by you, at design time.&lt;/strong&gt; Step one, then step two, then step three. Same path every time. The LLM does work &lt;em&gt;inside&lt;/em&gt; the steps, but it doesn't get to choose the steps.&lt;/p&gt;

&lt;p&gt;That's the entire difference. And here's the part people skip: &lt;strong&gt;an LLM doing something smart inside a fixed step is not agency.&lt;/strong&gt; Extracting fields, classifying a ticket, generating a summary — that's just &lt;em&gt;using an LLM&lt;/em&gt;. It's a smart function call. Agency is specifically when the model is handed the steering wheel and gets to pick the route.&lt;/p&gt;

&lt;p&gt;Most "agents" never actually hand over the wheel. They just narrate the fixed route in fluent natural language and call the narration "reasoning."&lt;/p&gt;




&lt;h2&gt;
  
  
  The test: can you draw the flowchart in advance?
&lt;/h2&gt;

&lt;p&gt;Here's the one-line litmus that does all the work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you can draw the flowchart of what your system does &lt;em&gt;before&lt;/em&gt; it runs, you don't have an agent. You have a pipeline.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sit with your "agent" for a second. Step 1: it retrieves some context. Step 2: it calls a tool. Step 3: it formats a response. Could you have drawn that on a whiteboard before writing a line of code? Then it's a pipeline. The model isn't deciding the path — you already decided it. The model is just doing the work at each node while &lt;em&gt;sounding&lt;/em&gt; like it's deciding.&lt;/p&gt;

&lt;p&gt;You only need real agency when the flowchart genuinely &lt;em&gt;cannot&lt;/em&gt; be drawn ahead of time — when the next step depends on discovering something you couldn't have known in advance. That's rare. Most business tasks have a shape you already understand. You know the steps. You're just letting the model improvise them, at great expense, for no benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the costume is expensive
&lt;/h2&gt;

&lt;p&gt;"Fine," you might say, "so it's technically a pipeline. But it works, so who cares?" Here's who cares: everyone who has to run it, pay for it, or debug it at 2 a.m. Pretending a pipeline is an agent has a real bill, and it's itemized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nondeterminism.&lt;/strong&gt; When the model chooses the path, the same input can take different paths on different runs. Great for a demo, miserable in production, because now your bugs don't reproduce. "It worked when I tried it" becomes a permanent state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debuggability collapse.&lt;/strong&gt; When a fixed pipeline breaks, you know exactly which step failed. When an agent breaks, it failed at step 12 because of a decision it made at step 4 that you didn't control and can't easily replay. You're not debugging code anymore; you're doing forensics on a choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiplied failure surface.&lt;/strong&gt; Every autonomous decision is another place to go wrong, and the failures &lt;em&gt;compound&lt;/em&gt; across steps. A pipeline with five fixed steps has five things to check. An agent that makes five decisions has five decisions each of which can be wrong, in combination, in an order that changes each run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost and latency.&lt;/strong&gt; A reasoning loop makes many more model calls than a fixed sequence — it thinks, it re-thinks, it reflects, it decides to loop again. You're paying per token to have the model deliberate about a route you already knew.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't test it.&lt;/strong&gt; Regression testing needs a fixed set of paths to test against. An agent, by definition, doesn't have one. So the thing making autonomous decisions in production is also the thing you can't write reliable tests for. Fantastic.&lt;/p&gt;

&lt;p&gt;Add it up and the punchline is brutal: you paid all of that — the nondeterminism, the debugging nightmares, the token bill — to let the model decide something &lt;em&gt;you already knew the answer to&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you actually wanted was a pipeline
&lt;/h2&gt;

&lt;p&gt;Here's the boring thing that wins.&lt;/p&gt;

&lt;p&gt;A pipeline is a fixed sequence of steps, with LLM calls at the specific points where a model genuinely adds value, and deterministic control flow that &lt;em&gt;you&lt;/em&gt; own. It's reproducible: same input, same path. It's testable: fixed paths mean real regression tests. It's cheap: no reasoning loop burning tokens to re-decide the obvious. It's debuggable: when step 3 fails, you look at step 3.&lt;/p&gt;

&lt;p&gt;And here's the thing people miss — &lt;strong&gt;the LLM still does all the smart parts.&lt;/strong&gt; It still extracts, classifies, reasons about content, generates language. You haven't dumbed anything down. You've just stopped letting it improvise the &lt;em&gt;structure&lt;/em&gt; of the work, because the structure was never the part that needed intelligence. The structure was the part you already understood.&lt;/p&gt;

&lt;p&gt;Look closely at the "agentic" systems that actually work in production and you'll usually find this: a mostly-fixed pipeline with one or two carefully-constrained decision points, not a free-roaming reasoning loop. The good ones minimized the autonomy to the smallest possible surface. They're pipelines that occasionally, deliberately, ask the model to make one bounded choice — not agents that were trusted to run the whole show.&lt;/p&gt;




&lt;h2&gt;
  
  
  When you &lt;em&gt;do&lt;/em&gt; need a real agent
&lt;/h2&gt;

&lt;p&gt;Now let me argue against myself, because "agents are always bad" would be as dumb as "everything must be an agent." Real agency earns its cost — genuinely — in specific cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The steps truly can't be known in advance.&lt;/strong&gt; Open-ended research, exploration, debugging an unknown problem — tasks where the path genuinely emerges from what you find. You can't draw that flowchart because the flowchart is the thing being discovered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each step depends on discovering the last.&lt;/strong&gt; Real multi-hop work: "find the thing, then based on what the thing is, figure out the next thing." If step 2 is genuinely unknowable until step 1 runs, you need something that can decide at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching is unbounded and real&lt;/strong&gt; — not "an if-statement with three cases," which is just a pipeline with a switch, but a space of possibilities too large to enumerate ahead of time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of those describes your task, build the agent — you've earned it. And even then, the move is to &lt;strong&gt;minimize the agency&lt;/strong&gt;: hard-code everything you can, and reserve the model's runtime decision-making for the one place that genuinely needs it. Autonomy is a cost. Spend it only where it buys something.&lt;/p&gt;

&lt;p&gt;The point was never "agents are bad." It's that &lt;strong&gt;agency is a cost you should have to justify&lt;/strong&gt;, and most systems calling themselves agents never justified it — they just liked the word.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why everyone builds the agent anyway
&lt;/h2&gt;

&lt;p&gt;So if pipelines are cheaper, safer, and more debuggable, why is everyone building agents? Here's the uncomfortable answer: &lt;strong&gt;agents are built for the builder, not the task.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An agent demos better. "Watch it reason through the problem autonomously" makes a room lean in; "I wrote a function that calls the model three times" does not. An agent &lt;em&gt;feels&lt;/em&gt; like real AI, like the future, like the thing you got into this for. And "agentic" is a resume word and a fundraising word — it signals sophistication in a standup and in a pitch deck in a way "deterministic pipeline" never will.&lt;/p&gt;

&lt;p&gt;None of those reasons have anything to do with whether your task needs an agent. They're about how the architecture makes &lt;em&gt;you&lt;/em&gt; feel and look. And that's exactly why the boring pipeline is the senior move — because choosing the less impressive thing that actually works, when the flashier thing would've gotten more claps, is the discipline the hype actively punishes. Nobody screenshots your while-loop. Your while-loop just quietly stays up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Agent" should be the thing you &lt;em&gt;escalate to&lt;/em&gt; when a pipeline provably can't do the job — not the default you reach for because the word sounds advanced.&lt;/p&gt;

&lt;p&gt;Start boring. Draw the flowchart. If you &lt;em&gt;can&lt;/em&gt; draw it, build the pipeline — fixed steps, LLM calls where they earn their place, control flow you own. Add real agency only at the specific point where a fixed path demonstrably fails, and no further. The system that ships and stays up in production is almost always more boring than the one that wins the demo.&lt;/p&gt;

&lt;p&gt;Most of what's being called an agent right now is a pipeline in a trench coat. And I'll say again what I said at the top: that's not an insult. It's a relief. Because a pipeline is the thing you can actually run, test, afford, and debug — and "impressive in a demo" was never the goal. "Still working on Wednesday" was.&lt;/p&gt;

&lt;p&gt;Take the coat off. You'll like what's underneath better.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Two questions, and I want both in the comments. First, the fun one: what did you build as an "agent" that turned out to be a pipeline in disguise? And the real argument — where's the line for you? What's the smallest task where you think a genuine agent actually earns its complexity? I suspect we'll all draw that line in a different place, which is exactly why it's worth arguing about.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>How to Add AI to an Existing App Without Breaking It</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Mon, 07 Sep 2026 05:41:58 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/how-to-add-ai-to-an-existing-app-without-breaking-it-36mm</link>
      <guid>https://dev.to/james_anderson_h/how-to-add-ai-to-an-existing-app-without-breaking-it-36mm</guid>
      <description>&lt;p&gt;Here's the situation almost every guide ignores.&lt;/p&gt;

&lt;p&gt;You have an app. It's in production. It has real users, real uptime, and real tech debt. And now someone — your boss, your users, your own curiosity — wants AI in it.&lt;/p&gt;

&lt;p&gt;So you go looking for how to do it, and every tutorial starts the same way: &lt;code&gt;npx create-something&lt;/code&gt;, a fresh repo, a blank canvas. None of them tell you how to add AI to the thing you &lt;em&gt;already have&lt;/em&gt; — the codebase with paying customers and a pager that goes off when it's down.&lt;/p&gt;

&lt;p&gt;That's a completely different problem, and it's the one that actually matters. Adding AI to a greenfield project is easy because there's nothing to break. Adding it to a real app is an exercise in &lt;strong&gt;not breaking the thing that already works.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is that guide. How to bolt AI onto a live codebase as a &lt;em&gt;removable enhancement&lt;/em&gt; — carefully, reversibly, without regret.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one idea everything else follows from: enhancement, not rewrite
&lt;/h2&gt;

&lt;p&gt;Before any steps, internalize this, because it prevents 90% of the mistakes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You are not rebuilding your app around AI. You are adding AI as a layer on top of the app you already have.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your existing application is the asset. It works. People rely on it. AI is an &lt;em&gt;addition&lt;/em&gt; — reachable through the interfaces you already have, removable if it fails, invisible when it's off. If adding an AI feature requires you to touch your core domain logic, restructure your data model, or make your critical path depend on a model call, stop. You've stopped enhancing and started endangering.&lt;/p&gt;

&lt;p&gt;The mental test for every decision below: &lt;strong&gt;if this AI feature broke completely right now, would the rest of my app still work?&lt;/strong&gt; If the answer is ever "no," you've wired it in too deep.&lt;/p&gt;

&lt;p&gt;Here's how to keep the answer "yes."&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Start with one specific pain point — not "add AI"
&lt;/h2&gt;

&lt;p&gt;"We want to add AI" is not a feature. It's a mood. And it's the fastest way to a sprawling, half-finished integration tangled through your whole codebase.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;one&lt;/strong&gt; narrow, real, high-value workflow. Not "make the app intelligent" — something like "auto-summarize support tickets so triage is faster" or "suggest tags for uploaded documents." Specific enough that you could describe it in one sentence, and small enough that you could &lt;em&gt;rip it out in an afternoon&lt;/em&gt; if it doesn't pan out.&lt;/p&gt;

&lt;p&gt;The first AI feature you add isn't really about the feature. It's about learning how AI behaves in &lt;em&gt;your&lt;/em&gt; system — your data, your traffic, your users — on a small surface where a mistake is cheap. Earn the right to expand by shipping one thing well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do:&lt;/strong&gt; choose the smallest workflow with a clear win.&lt;br&gt;
&lt;strong&gt;Don't:&lt;/strong&gt; try to AI-ify the whole product in one release.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Put it behind an abstraction layer from day one
&lt;/h2&gt;

&lt;p&gt;The single most common mistake: scattering raw &lt;code&gt;openai.chat(...)&lt;/code&gt; calls throughout your codebase. Do that, and the vendor's SDK is now welded into twenty files, model changes ripple everywhere, and there's no single place to add caching, logging, or a kill switch.&lt;/p&gt;

&lt;p&gt;Wrap it. The rest of your app should talk to &lt;em&gt;your&lt;/em&gt; interface, never the vendor's SDK directly.&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/ai.ts — the ONLY place your app touches an AI provider&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AIProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AIOptions&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="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="c1"&gt;// swap this implementation without touching a single caller&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OpenAIProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;AIProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AIOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// provider-specific details live here and nowhere else&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;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AIProvider&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;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your features call &lt;code&gt;ai.complete(...)&lt;/code&gt;, not a vendor. That one boundary buys you: swapping providers or models without a refactor, adding caching and rate limiting in one place, mocking AI in tests, and a single choke point where you can log everything or turn it all off. AI touches your app in &lt;em&gt;exactly one file&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Isolate it so it can fail without taking the app down
&lt;/h2&gt;

&lt;p&gt;This is the reliability move that separates a safe integration from a time bomb.&lt;/p&gt;

&lt;p&gt;An AI call is slow, occasionally fails, and depends on a third party you don't control. So it must &lt;strong&gt;never sit in the critical path of something that has to succeed.&lt;/strong&gt; If your checkout, your login, or your page load &lt;em&gt;waits on&lt;/em&gt; a model call, then when the model is slow or down — and it will be — your core app is slow or down too. You've made your most reliable feature depend on your least reliable one.&lt;/p&gt;

&lt;p&gt;Isolate every AI call with a timeout and a fallback, and let the feature degrade gracefully to "off":&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;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&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="nb"&gt;Promise&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;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;try&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="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;summaryPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;   &lt;span class="c1"&gt;// AI is down/slow → feature quietly disappears, app is fine&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;Then the UI treats the summary as optional: if it's there, show it; if it's &lt;code&gt;null&lt;/code&gt;, show the normal ticket. The AI feature is a &lt;em&gt;bonus that can vanish&lt;/em&gt;, not a dependency that can crash you. Your uptime should never be hostage to a model endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Ship it opt-in, not opt-out
&lt;/h2&gt;

&lt;p&gt;Don't flip AI on for everyone by default. Surprising users with AI behavior they didn't ask for is how you earn a backlash instead of adoption.&lt;/p&gt;

&lt;p&gt;Put it behind a feature flag, make it a clearly-labeled option users choose, and release it to a small cohort first — ideally the early-adopter types who &lt;em&gt;want&lt;/em&gt; to try it. This does three things at once: it respects users' choice, it limits your blast radius if something's wrong, and it gives you an instant kill switch (turn the flag off, the feature's gone, no deploy needed).&lt;/p&gt;

&lt;p&gt;Opt-in also makes the AI &lt;em&gt;identifiable&lt;/em&gt;. Users should always know when they're interacting with AI, with clear cues in the UI. "Quietly replaced the thing you trusted with an AI version" is the pattern that generates angry threads. "Here's a new optional AI helper, try it if you like" is the one that generates signups.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Keep a human in the loop early
&lt;/h2&gt;

&lt;p&gt;For anything consequential — anything that sends, charges, deletes, or is shown to a customer as fact — start with the AI &lt;em&gt;proposing&lt;/em&gt; and a human &lt;em&gt;confirming&lt;/em&gt;. Draft-and-approve, not act-directly.&lt;/p&gt;

&lt;p&gt;You relax this as your confidence grows and the data backs it up. But you start with the brake on, because early on you don't yet know how the model behaves on your real inputs, and the cost of finding out in production — on a live customer — is high. Human-in-the-loop early is how you gather that evidence safely. Reduce the hand-holding when the metrics earn it, not before.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Instrument cost and behavior &lt;em&gt;before&lt;/em&gt; you scale
&lt;/h2&gt;

&lt;p&gt;AI has a failure mode ordinary features don't: it can be quietly expensive and quietly drifting, and you won't see either without instrumentation. Add it up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per feature, not just total spend.&lt;/strong&gt; Track tokens/cost tagged to &lt;em&gt;this&lt;/em&gt; feature, so you know what it actually costs to run — and catch a runaway before it's a bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Model calls are slow and variable; watch p95, not just averages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input/output logging.&lt;/strong&gt; So when it does something weird, you can see what it was given and what it returned. (Log shape and metadata, not raw sensitive data.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the model version.&lt;/strong&gt; Don't let "latest" silently change your feature's behavior overnight. Pin a version and upgrade deliberately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You want to know what this costs and how it behaves &lt;em&gt;on real traffic&lt;/em&gt; while it's still small — because those numbers are your evidence for whether it's safe to widen.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Canary the rollout — widen only when the metrics hold
&lt;/h2&gt;

&lt;p&gt;You already have the flag from step 4. Now use it as a dial, not a switch.&lt;/p&gt;

&lt;p&gt;Small cohort first. Watch the numbers that matter: output quality, cost per request, latency, complaint rate. If they hold, widen — 5%, 25%, 50%, everyone — in stages, pausing to check at each step. If a metric goes bad, you roll back &lt;em&gt;one feature flag&lt;/em&gt;, not your whole app. That's the entire point of everything above: when something goes wrong, the blast radius is one optional feature, contained behind one flag, not a core system you now have to hotfix under pressure.&lt;/p&gt;

&lt;p&gt;The teams who get burned are the ones who went from "works on my machine" to "on for 100% of users" in one step. Don't. Let real traffic earn each expansion.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Treat the AI's output as untrusted input
&lt;/h2&gt;

&lt;p&gt;Here's the through-line that ties this to everything: &lt;strong&gt;"it returned something" is not "it returned something correct, safe, or well-formed."&lt;/strong&gt; The AI is the least trustworthy component in your new pipeline, and you should treat its output exactly like user input from a stranger — because in terms of trust, that's what it is.&lt;/p&gt;

&lt;p&gt;Before an AI response reaches a user or your database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate the structure.&lt;/strong&gt; If you asked for JSON, parse and schema-check it before using it — don't assume it came back well-formed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check it against your constraints.&lt;/strong&gt; Is it in range? Does it reference real entities? Does it violate a rule your app enforces?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanitize before display or storage.&lt;/strong&gt; AI output can contain injection payloads, broken markup, or content you don't want rendered raw. Escape and clean it like any untrusted string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The insecure and the buggy versions of AI integration have the same tell: the output &lt;em&gt;looked&lt;/em&gt; fine and got used without a check. A validation step between "the model responded" and "we acted on it" is the cheapest insurance you'll buy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern underneath all eight
&lt;/h2&gt;

&lt;p&gt;Look back and every step is really the same instinct: &lt;strong&gt;contain the risk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One pain point (small surface). An abstraction layer (one touch point). Isolation with fallback (can't crash you). Opt-in behind a flag (limited blast radius, instant off). Human-in-the-loop (a brake while you learn). Instrumentation (see it before it hurts). Canary rollout (widen only on evidence). Output validation (don't trust it blindly).&lt;/p&gt;

&lt;p&gt;The teams who successfully add AI to an existing app aren't the ones who rebuilt around it. They're the ones who treated it as a &lt;strong&gt;removable enhancement&lt;/strong&gt; — bolted on behind an interface, behind a flag, with a fallback and a kill switch, verified on the way out. Your existing app is the thing that pays the bills. AI is an addition, and no addition should ever be allowed to take down the foundation.&lt;/p&gt;

&lt;p&gt;Add it the way you'd add any risky third-party dependency: behind a boundary, behind a flag, with a fallback, and with your finger on the switch that turns it off. Do that, and the worst case isn't an outage — it's a feature you quietly disable while you figure out what went wrong. That's the whole goal: make AI something that can fail &lt;em&gt;safely&lt;/em&gt;, so you can add it &lt;em&gt;confidently&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What broke the first time you added AI to a real, existing app — and what do you wish you'd isolated before you shipped it? The "it was fine until traffic hit it" stories are the ones worth trading. Drop yours below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Next.js Security Checklist for AI-Generated Code</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Sun, 06 Sep 2026 05:10:45 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/the-nextjs-security-checklist-for-ai-generated-code-1g34</link>
      <guid>https://dev.to/james_anderson_h/the-nextjs-security-checklist-for-ai-generated-code-1g34</guid>
      <description>&lt;p&gt;Here's a number that should make you check your own repo: an audit of over 200 vibe-coded applications in early 2026 found that &lt;strong&gt;91.5% contained at least one security vulnerability traceable to AI-generated code.&lt;/strong&gt; And AI-assisted commits leak secrets at more than &lt;em&gt;double&lt;/em&gt; the rate of human-written code.&lt;/p&gt;

&lt;p&gt;One security researcher summed up the problem better than any statistic. In a 67-line demo app, the AI he prompted produced: hardcoded JWT secrets, MD5 password hashing, tokens that never expire, an XSS hole, and zero rate limiting — "all in a working application that looks completely normal to a non-security person."&lt;/p&gt;

&lt;p&gt;That last phrase is the whole problem. &lt;strong&gt;The code works. The demo runs. Nothing errors.&lt;/strong&gt; And it's full of holes, because the dangerous security mistakes don't announce themselves — they look exactly like success until someone finds them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI generates insecure Next.js code specifically
&lt;/h2&gt;

&lt;p&gt;Before the checklist, understand &lt;em&gt;why&lt;/em&gt; this keeps happening, because it's not random.&lt;/p&gt;

&lt;p&gt;An AI coding assistant learned from years of public code — tutorials, Stack Overflow, starter repos. And in that training data, the &lt;strong&gt;insecure pattern is often the most common pattern.&lt;/strong&gt; The most-upvoted JWT tutorial stores the token in localStorage. The quickest auth example checks the session only in middleware. The fastest way to pass data to a component is to hand it the whole database row.&lt;/p&gt;

&lt;p&gt;So when you ask AI to "add authentication," it doesn't give you the &lt;em&gt;secure&lt;/em&gt; approach — it gives you the &lt;em&gt;popular&lt;/em&gt; one, because popular is what it saw most. And Next.js makes this especially dangerous, because server and client code live in the same files, so a single wrong line can quietly ship a secret to the browser or leave a route completely unguarded.&lt;/p&gt;

&lt;p&gt;Here's the checklist. Keep it open during your next AI-code review.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 1. Don't trust middleware for auth — it's a UX layer, not a security boundary
&lt;/h2&gt;

&lt;p&gt;This is the biggest one, and it's backed by a &lt;em&gt;string&lt;/em&gt; of 2026 CVEs. Attackers found repeated ways to bypass Next.js middleware entirely — with a crafted &lt;code&gt;x-middleware-subrequest&lt;/code&gt; header (CVE-2025-29927), with dynamic route parameter injection (CVE-2026-44574), with Turbopack's separate request pipeline (CVE-2026-45109). Next.js 16 even &lt;strong&gt;renamed &lt;code&gt;middleware.ts&lt;/code&gt; to &lt;code&gt;proxy.ts&lt;/code&gt;&lt;/strong&gt; specifically to signal: this is a routing/UX layer, not a security gate.&lt;/p&gt;

&lt;p&gt;AI generates middleware-only auth constantly, because that's the tutorial pattern. It looks airtight and is trivially bypassable.&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;// ❌ BAD — AI's favorite pattern: middleware is the ONLY gate&lt;/span&gt;
&lt;span class="c1"&gt;// proxy.ts (or middleware.ts)&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;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="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;token&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// The Route Handler below assumes it's protected. It isn't.&lt;/span&gt;
&lt;span class="c1"&gt;// An attacker who bypasses the proxy layer hits it directly, unauthenticated.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: middleware/proxy can do the fast UX redirect, but &lt;strong&gt;every Route Handler and Server Action must independently verify identity at the data layer&lt;/strong&gt; — the one place that can't be bypassed at the network level.&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;// ✅ GOOD — verify auth where the data actually lives&lt;/span&gt;
&lt;span class="c1"&gt;// app/api/orders/route.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;verifySession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;verifySession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;// checks + cryptographically verifies&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;session&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// now safe to use session.userId&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&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;getOrdersForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;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;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; for every protected route, ask "if someone hits this directly, bypassing the proxy, are they still stopped?" If the only check is in middleware, the answer is no.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 2. Every &lt;code&gt;"use server"&lt;/code&gt; Server Action is a public POST endpoint
&lt;/h2&gt;

&lt;p&gt;AI treats Server Actions like internal functions you call from a component. They aren't. &lt;strong&gt;Every Server Action is a publicly callable POST endpoint&lt;/strong&gt; — anyone can invoke it directly, with any arguments, regardless of what your UI does. This is the exact class of bug that hit a major vibe-coding platform in 2026, where a handful of API calls from a free account reached any other user's data.&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;// ❌ BAD — no auth, no ownership check. Anyone can call this with any id.&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&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;deleteProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectId&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 typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — auth + ownership + validation, inside the action&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifySession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&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;deleteProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;verifySession&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;session&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;Unauthorized&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;id&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;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&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;projectId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// validate input&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ownerId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ownership&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; treat every &lt;code&gt;"use server"&lt;/code&gt; function as a public API endpoint. Does it check &lt;em&gt;who&lt;/em&gt; is calling, &lt;em&gt;whether they own the thing&lt;/em&gt;, and &lt;em&gt;whether the input is valid&lt;/em&gt; — every time?&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 3. Never store tokens in localStorage — use httpOnly cookies
&lt;/h2&gt;

&lt;p&gt;The single most common Next.js auth mistake, and AI's default because it's the classic tutorial pattern. Anything in &lt;code&gt;localStorage&lt;/code&gt; is readable by &lt;strong&gt;any&lt;/strong&gt; JavaScript on the page — including a script injected by an XSS attack, which can then exfiltrate every user's session.&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;// ❌ BAD — readable by any script, including an XSS payload&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userToken&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 typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — httpOnly cookie, set server-side, invisible to JavaScript&lt;/span&gt;
&lt;span class="c1"&gt;// in a Server Action or Route Handler&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;cookies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&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;setSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&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;cookies&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;httpOnly&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="c1"&gt;// JavaScript cannot read it → survives XSS&lt;/span&gt;
    &lt;span class="na"&gt;secure&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="c1"&gt;// HTTPS only&lt;/span&gt;
    &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// CSRF protection&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// keep sessions short&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;"But my Client Component needs the user's name!" — it doesn't need the &lt;em&gt;token&lt;/em&gt; for that. Expose a small &lt;code&gt;/api/me&lt;/code&gt; Route Handler that reads the cookie server-side and returns only the fields the client needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; search your codebase for &lt;code&gt;localStorage.setItem&lt;/code&gt; near anything auth-related. If a token lives there, move it to an httpOnly cookie.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 4. Don't leak secrets to the client
&lt;/h2&gt;

&lt;p&gt;Two ways AI ships your secrets to the browser, both silent. First, the &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix — it &lt;em&gt;publishes&lt;/em&gt; a variable to the client bundle. AI sometimes slaps it on things to "make them work," including secrets. Second, using a server-only secret inside a component that's actually a Client Component.&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;// ❌ BAD — NEXT_PUBLIC_ ships this straight to the browser bundle. Forever.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripeSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_STRIPE_SECRET_KEY&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ BAD — a secret referenced in a "use client" component leaks too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — server-only secret, never prefixed, guarded by the server-only package&lt;/span&gt;
&lt;span class="c1"&gt;// lib/payments.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="c1"&gt;// build-time error if this is ever imported client-side&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripeSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;   &lt;span class="c1"&gt;// no NEXT_PUBLIC_&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a build-time guard so a secret can never accidentally go public:&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/env.ts — fail the build if a server secret got a NEXT_PUBLIC_ prefix&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serverOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&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;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;serverOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`NEXT_PUBLIC_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&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="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;`Security error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must not be NEXT_PUBLIC_`&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;And run a secret scanner — &lt;code&gt;gitleaks&lt;/code&gt; or &lt;code&gt;trufflehog&lt;/code&gt; — against your repo &lt;em&gt;and&lt;/em&gt; your built client bundle. Anything in client-side JavaScript is public. (29 million hardcoded secrets were found on GitHub in 2025, and most never get rotated.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; grep for &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; and confirm not one of them is a secret. Scan the built bundle for anything that looks like a key.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 5. Watch the &lt;code&gt;"use client"&lt;/code&gt; boundary — it's contagious
&lt;/h2&gt;

&lt;p&gt;Here's the architectural root cause behind mistakes 3 and 4. The moment you put &lt;code&gt;"use client"&lt;/code&gt; at the top of a component, &lt;strong&gt;every component it imports becomes client code too&lt;/strong&gt; — it all ships to the browser. AI sprinkles &lt;code&gt;"use client"&lt;/code&gt; around liberally to make hooks work, and can accidentally drag secret-touching or data-fetching logic across the line into the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ BAD — "use client" at the top, then secret-touching code below it&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="c1"&gt;// this now tries to ship to the browser&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;Dashboard&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;     &lt;span class="c1"&gt;// server logic stranded on the client&lt;/span&gt;
  &lt;span class="c1"&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 fix is a mental model: &lt;strong&gt;server by default, client only at the leaves.&lt;/strong&gt; Keep &lt;code&gt;"use client"&lt;/code&gt; as far down the tree as possible — only the tiny interactive bit (a button, a dropdown) needs it. Fetch data and touch secrets on the server; pass down only what the UI needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — server component fetches; a small client leaf handles interaction&lt;/span&gt;
&lt;span class="c1"&gt;// Dashboard.tsx (server component, no "use client")&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;verifySession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LikeButton&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./LikeButton&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="c1"&gt;// the only client piece&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;Dashboard&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;session&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;verifySession&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;data&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;getDashboardData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// safe, server-side&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LikeButton&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;likes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;               &lt;span class="c1"&gt;// pass only what's needed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; how far down your tree does your first &lt;code&gt;"use client"&lt;/code&gt; sit? If it's on a layout or a big parent, you're shipping more to the browser than you think.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 6. Don't over-pass data from server to client
&lt;/h2&gt;

&lt;p&gt;A subtle leak AI causes constantly: a Server Component fetches a full record and passes the whole object as props to a Client Component — shipping every field to the browser, even ones the UI never displays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ BAD — SELECT * then hand the whole row to the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// includes passwordHash, role, internal flags&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;   &lt;span class="c1"&gt;// all of it is now in the browser's payload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — select only what's needed, pass only what's shown&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;displayName&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;avatarUrl&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="c1"&gt;// nothing sensitive&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat the server→client props boundary as a trust boundary. If a field crosses it, assume it's public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; for every object passed from a Server Component to a Client Component, check what's actually in it. &lt;code&gt;SELECT *&lt;/code&gt; into props is a leak.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 7. Validate input on the server — always
&lt;/h2&gt;

&lt;p&gt;AI loves to validate on the client ("the form checks it!") and treat that as done. Client validation is a UX nicety; it's trivially bypassed by anyone hitting your endpoint directly. Every Server Action and Route Handler must validate its input server-side, or you're open to injection, SSRF (a real 2026 Next.js CVE class), and malformed-data bugs.&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;// ❌ BAD — trusts the client to have sent well-formed, safe data&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&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;updateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;()&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&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 typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — validate on the server with a schema, treat all input as hostile&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&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;email&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="nf"&gt;email&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&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;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// throws on anything unexpected&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;verifySession&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;session&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;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; for every server entry point, is the input parsed and validated &lt;em&gt;on the server&lt;/em&gt;, or is it trusting the client?&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 8. Get the JWT details right (AI gets them wrong)
&lt;/h2&gt;

&lt;p&gt;If you're hand-rolling JWTs — which AI will happily do — here are the specific things it botches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded or weak secret.&lt;/strong&gt; AI writes &lt;code&gt;const SECRET = 'mysecret'&lt;/code&gt;. Generate a real one: &lt;code&gt;openssl rand -base64 32&lt;/code&gt;, and keep it in env, not source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;alg: none&lt;/code&gt; or accepting the token's own &lt;code&gt;alg&lt;/code&gt;.&lt;/strong&gt; Always pin the algorithm on verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens that never expire.&lt;/strong&gt; Always set &lt;code&gt;exp&lt;/code&gt;. Short-lived access tokens, longer refresh tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak password hashing.&lt;/strong&gt; AI still reaches for MD5/SHA-1. Use &lt;code&gt;argon2id&lt;/code&gt; or &lt;code&gt;bcrypt&lt;/code&gt;. MD5/SHA-1 for passwords is disqualifying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jsonwebtoken&lt;/code&gt; in middleware.&lt;/strong&gt; It relies on Node crypto, which &lt;strong&gt;isn't available in the Edge runtime&lt;/strong&gt; — it can crash or silently misbehave. Use &lt;code&gt;jose&lt;/code&gt;, which works on the Edge.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ GOOD — jose, works on the Edge, algorithm pinned, expiry enforced&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;jwtVerify&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;algorithms&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;HS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// pin it — never trust the token's own alg&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;payload&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also: checking that a cookie &lt;em&gt;exists&lt;/em&gt; is not verifying it. A user can set a fake cookie. You must cryptographically verify the token, not just read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; is the secret strong and in env? Is the algorithm pinned? Do tokens expire? Are passwords hashed with argon2/bcrypt, not MD5?&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 9. Rate-limit your endpoints
&lt;/h2&gt;

&lt;p&gt;AI almost never adds rate limiting. Without it, login endpoints are open to credential stuffing and brute force, and your RSC/Server Function endpoints are open to denial-of-service (two high-severity DoS CVEs shipped for Next.js in 2026, neither requiring auth). Even a simple per-IP limit at the edge dramatically shrinks the attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; do your login, password-reset, and expensive endpoints have a rate limit? If AI built them, they almost certainly don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 10. Patch Next.js — you're probably on a vulnerable version
&lt;/h2&gt;

&lt;p&gt;AI generates code for whatever patterns dominated its training data, and it will &lt;em&gt;not&lt;/em&gt; tell you your framework version has known auth-bypass holes. The May 2026 coordinated release alone fixed &lt;strong&gt;13 CVEs, including three auth-bypass vulnerabilities exploitable without any credentials.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upgrade to a patched version (15.5.18 / 16.2.6 or later).&lt;/li&gt;
&lt;li&gt;If you're on Next.js 16, run the official codemod to migrate &lt;code&gt;middleware.ts&lt;/code&gt; → &lt;code&gt;proxy.ts&lt;/code&gt;: &lt;code&gt;npx @next/codemod@latest middleware-to-proxy&lt;/code&gt;. If you haven't, your route protection may be silently inactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; what version are you actually on? Check it against the latest advisories before you ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern underneath all ten
&lt;/h2&gt;

&lt;p&gt;Look back at the list and notice what every item has in common: &lt;strong&gt;the insecure version works.&lt;/strong&gt; The localStorage auth logs you in. The middleware-only gate passes your tests. The &lt;code&gt;SELECT *&lt;/code&gt; returns data. The &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; secret makes the feature run. Nothing errors. Nothing goes red.&lt;/p&gt;

&lt;p&gt;That's the trap of AI-generated security code, and it's the same trap in everything AI builds: &lt;strong&gt;"it works" and "it's secure" are different claims, and only one of them shows up in the demo.&lt;/strong&gt; The AI is confident, the code is fluent, and the hole is invisible until someone finds it.&lt;/p&gt;

&lt;p&gt;So the mindset that actually protects you isn't "trust the AI less." It's: &lt;strong&gt;you are the verification layer the AI doesn't have.&lt;/strong&gt; It writes the code; you own the boundary — where auth is checked, what crosses to the client, what a secret is, what gets validated. Those are decisions about &lt;em&gt;your&lt;/em&gt; app's trust model, and no model can make them for you, because it doesn't know your threat model. It only knows what code usually looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run this today
&lt;/h2&gt;

&lt;p&gt;Don't wait for a rewrite. Right now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;gitleaks&lt;/code&gt; or &lt;code&gt;trufflehog&lt;/code&gt; against your repo and your built client bundle.&lt;/li&gt;
&lt;li&gt;Grep for &lt;code&gt;localStorage.setItem&lt;/code&gt; and &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; and audit every hit.&lt;/li&gt;
&lt;li&gt;Pick your three most sensitive Server Actions and confirm each checks auth, ownership, and input.&lt;/li&gt;
&lt;li&gt;Check your Next.js version against the latest advisories.&lt;/li&gt;
&lt;li&gt;Keep this checklist open the next time you accept AI-generated code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The barrier to &lt;em&gt;writing&lt;/em&gt; Next.js code has collapsed. The barrier to &lt;em&gt;securing&lt;/em&gt; it hasn't moved an inch — and that part is entirely yours.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the scariest thing you've caught in AI-generated code — or, worse, shipped and found later? I collect these, and the "it looked completely fine" stories are the ones worth learning from. Drop yours in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Generating code with AI is easy. Building something that survives its own growth is not — and those aren't the same skill.
This article wrote about the workflow that actually holds up for real apps: give AI structure, rules, and context before it writes a line — and</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Sat, 05 Sep 2026 08:03:35 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/generating-code-with-ai-is-easy-building-something-that-survives-its-own-growth-is-not-and-those-290f</link>
      <guid>https://dev.to/james_anderson_h/generating-code-with-ai-is-easy-building-something-that-survives-its-own-growth-is-not-and-those-290f</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/robertadam987_/vibe-coding-in-the-new-era-how-to-build-real-applications-with-ai-1dn" class="crayons-story__hidden-navigation-link"&gt;Vibe Coding in the New Era: How to Build Real Applications with AI&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/robertadam987_" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081282%2F321fd1fe-eef6-4f90-b0c4-9795c2b353ee.png" alt="robertadam987_ profile" class="crayons-avatar__image" width="800" height="533"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/robertadam987_" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Robert Adamson
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Robert Adamson
                
                
              
              &lt;div id="story-author-preview-content-4471569" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/robertadam987_" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081282%2F321fd1fe-eef6-4f90-b0c4-9795c2b353ee.png" class="crayons-avatar__image" alt="" width="800" height="533"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Robert Adamson&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/robertadam987_/vibe-coding-in-the-new-era-how-to-build-real-applications-with-ai-1dn" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/robertadam987_/vibe-coding-in-the-new-era-how-to-build-real-applications-with-ai-1dn" id="article-link-4471569"&gt;
          Vibe Coding in the New Era: How to Build Real Applications with AI
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/vibecoding"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;vibecoding&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/softwareengineering"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;softwareengineering&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/robertadam987_/vibe-coding-in-the-new-era-how-to-build-real-applications-with-ai-1dn" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;14&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/robertadam987_/vibe-coding-in-the-new-era-how-to-build-real-applications-with-ai-1dn#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>AI Is Getting Dramatically More Capable, Fast. Where Is This Heading?</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Sat, 05 Sep 2026 07:49:50 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/ai-is-getting-dramatically-more-capable-fast-where-is-this-heading-11bp</link>
      <guid>https://dev.to/james_anderson_h/ai-is-getting-dramatically-more-capable-fast-where-is-this-heading-11bp</guid>
      <description>&lt;p&gt;On September 3, 2026, OpenAI released a model called GPT-6 Astra, and one of its senior figures told reporters it's "not unreasonable to feel that we are now in the AGI era."&lt;/p&gt;

&lt;p&gt;You can read that as marketing. A lot of it is. But I'd suggest not scrolling past it too quickly, because underneath the swagger, something genuinely worth understanding happened — and the pace at which these moments are arriving has gotten strange enough that it's worth stopping to make sense of it.&lt;/p&gt;

&lt;p&gt;So let's do that. Not with hype, and not with the reflexive eye-roll either. Let's actually understand what AGI means, what it means to "measure" an AI's intelligence, what this particular model did, and then think honestly about the only question that really matters: what does any of this mean for us?&lt;/p&gt;




&lt;h2&gt;
  
  
  First — what does "AGI" even mean?
&lt;/h2&gt;

&lt;p&gt;The term gets thrown around so much it's nearly lost its shape, so let's put it back.&lt;/p&gt;

&lt;p&gt;The AI you use today is what researchers call &lt;strong&gt;narrow&lt;/strong&gt; intelligence. It's extraordinary at specific things — writing, coding, summarizing, answering questions — but each capability was trained in, and it operates within the shape of what it learned. It's a spectacularly wide, spectacularly shallow pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AGI — artificial general intelligence — is the idea of a system that can handle &lt;em&gt;any&lt;/em&gt; intellectual task a human can.&lt;/strong&gt; Not "better at one thing," but &lt;em&gt;general&lt;/em&gt;: able to walk up to a problem it has never seen, in a domain it wasn't built for, and figure it out — the way a capable person can move from cooking to taxes to consoling a friend without being "retrained" for each.&lt;/p&gt;

&lt;p&gt;The important, honest catch: &lt;strong&gt;there is no agreed definition of AGI, and no official finish line.&lt;/strong&gt; Ask ten researchers and you'll get ten thresholds. This is exactly why "are we there yet?" produces such heated, unresolvable arguments — people aren't disagreeing about the facts so much as about where the line even is. Keep that in your pocket; it matters for everything that follows.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you even measure intelligence in a machine?
&lt;/h2&gt;

&lt;p&gt;Here's a concept most people outside the field don't have, and it's the key to reading every AI headline you'll ever see: we measure these systems with &lt;strong&gt;benchmarks&lt;/strong&gt; — standardized tests.&lt;/p&gt;

&lt;p&gt;A benchmark is a big set of problems with known answers — math problems, coding challenges, reasoning puzzles, science questions — that you run the model through to get a score. It's the SAT for AI, basically. When you see "the new model scored 90% on such-and-such," that's a benchmark.&lt;/p&gt;

&lt;p&gt;Benchmarks are genuinely useful. They let you compare models and track progress. But here's the thing to hold onto, because it's the honest heart of this whole piece:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A benchmark measures what the AI &lt;em&gt;did on a test&lt;/em&gt;. It does not measure what the AI &lt;em&gt;is&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A high score tells you the system produced the right answers under specific conditions. It does not, on its own, tell you the system &lt;em&gt;understands&lt;/em&gt; anything, or that it will behave the same way in the mess of the real world, or that it has the thing we mean when we say a person is "intelligent." The map is not the territory, and the score is not the mind. Remember that as we look at what just happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Astra actually achieved (told straight)
&lt;/h2&gt;

&lt;p&gt;Now the news, honestly framed — because the real story is more interesting than either the hype or the dismissal.&lt;/p&gt;

&lt;p&gt;Astra didn't just win another leaderboard. It &lt;strong&gt;saturated a benchmark called ARC-AGI-3&lt;/strong&gt;, scoring 99.9%. What makes that notable isn't the number — it's the test. ARC-AGI-3 was designed &lt;em&gt;specifically to be hard for AI&lt;/em&gt;: to measure whether a system can reason about genuinely novel problems it couldn't have memorized, the exact kind of flexible, general reasoning that's supposed to separate narrow AI from something more. It was built to stay ahead of the machines. And it got beaten. Astra also saturated a frontier math benchmark and hit 100% on a hard cybersecurity challenge. On paper, these are the strongest results the field has published.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And now the asterisk, because it matters as much as the score.&lt;/strong&gt; That 99.9% was achieved using a special, expensive setup — a "souped-up harness," extra tooling wrapped around the model to help it work through the task. On the &lt;em&gt;standard&lt;/em&gt; setup, the same model scored around 66%. So the headline number reflects "the model &lt;em&gt;plus&lt;/em&gt; an elaborate system built around it," not the model alone. And every one of these figures is &lt;strong&gt;reported by OpenAI, run at maximum effort, and not yet independently verified&lt;/strong&gt; by outside labs.&lt;/p&gt;

&lt;p&gt;Hold both of these in your head at once, because both are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Something real happened. A test built to resist AI got saturated. That's not nothing.&lt;/li&gt;
&lt;li&gt;The headline needs reading carefully. The conditions matter, the tooling matters, and "vendor-reported, two days old" is not the same as "confirmed."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The temptation is to collapse into one or the other — "AGI is here!" or "it's all hype." The honest position is the uncomfortable middle: a genuine jump, wrapped in a number you should read with your eyes open.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing that actually matters isn't the score. It's the pace.
&lt;/h2&gt;

&lt;p&gt;Step back from any single benchmark, because the truly striking part is the &lt;em&gt;rate&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Look at the trail: over roughly a single year, the field moved through a rapid series of releases, each meaningfully more capable than the last — and benchmarks that were designed to last, to stay ahead of AI for years, are being saturated within &lt;em&gt;months&lt;/em&gt; of coming out. The people building the tests to measure the frontier can barely keep the frontier in frame.&lt;/p&gt;

&lt;p&gt;That's the part that should make you sit up — not "the AI is smart," but &lt;em&gt;how fast the ceiling is moving.&lt;/em&gt; We have gotten used to a cadence where each new model makes the previous one look quaint within a season. Whatever you personally believe about whether this is "real intelligence," the derivative — the speed of change — is the actual headline. Capability is compounding faster than almost anyone's intuitions are updating.&lt;/p&gt;




&lt;h2&gt;
  
  
  So what does this mean for regular people?
&lt;/h2&gt;

&lt;p&gt;Not robots marching down the street. Something quieter, and realer.&lt;/p&gt;

&lt;p&gt;Tasks that used to sit firmly on the "only a human can do this" side of the line are steadily crossing over. Not all at once, not perfectly — but the boundary of &lt;em&gt;what is exclusively ours&lt;/em&gt; is being redrawn faster than our jobs, our institutions, and our habits can comfortably adjust to. That's the actual disruption: not a dramatic event, but a boundary quietly moving, month after month.&lt;/p&gt;

&lt;p&gt;For how you work, it points at a real shift: the value moves from &lt;em&gt;doing&lt;/em&gt; tasks to &lt;em&gt;judging, directing, and verifying&lt;/em&gt; what an AI does. When a machine can produce the work, the scarce human skill becomes knowing what good looks like, deciding what's worth doing, and catching it when it's confidently wrong. The doing gets cheap; the judgment gets precious.&lt;/p&gt;

&lt;p&gt;And that leads to the most important thing to keep clear, especially in a moment of big scary numbers: &lt;strong&gt;capability is not wisdom.&lt;/strong&gt; A model that saturates a reasoning test still has no stake in the outcome, no skin in your life, no care about whether it's right. It can be brilliant and have nothing at risk. Which is precisely why human judgment gets &lt;em&gt;more&lt;/em&gt; important as the tools get more capable, not less — someone still has to be the one who actually cares how it turns out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should we think about AI differently now?
&lt;/h2&gt;

&lt;p&gt;Yes — but probably not in the direction the headlines push you.&lt;/p&gt;

&lt;p&gt;The shift isn't "start fearing it" or "start worshipping it." It's more mundane and more useful: &lt;strong&gt;stop treating each AI release as a gadget update, and start treating AI as a general-purpose capability that will keep expanding into whatever you do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most people file each new model under "cool, a better version of the app." The more accurate frame is: this is a capability that has been getting dramatically better on a steep curve, and the right question is no longer "what can it do today?" but "what happens to what I do when this is meaningfully more capable next year — because it probably will be?" You prepare differently for a moving target than for a fixed tool. That's the mental adjustment worth making, and almost nobody has made it yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where is this actually heading?
&lt;/h2&gt;

&lt;p&gt;I'm not going to hand you a fake prediction, because anyone who's certain here is selling something. But I can lay out the honest range.&lt;/p&gt;

&lt;p&gt;Maybe we really are near something genuinely general, and the last few years will look like the steep part of the curve right before everything changed. Maybe we're watching benchmarks get saturated while real-world reliability quietly lags behind — the asterisk on that 99.9% is a hint that the demo and the deployment aren't the same thing. Or maybe "AGI" turns out to be less a moment and more a fog we walk into gradually, crossing the line without ever agreeing on where it was.&lt;/p&gt;

&lt;p&gt;What's &lt;em&gt;not&lt;/em&gt; uncertain is the shape of the curve. It's steep, and it hasn't bent yet. Tests meant to last years are lasting months. Whatever the destination, the travel speed is real.&lt;/p&gt;

&lt;p&gt;So the honest stance is neither the hype nor the eye-roll. It's &lt;em&gt;attention.&lt;/em&gt; This is one of the few technologies where the future is genuinely arriving faster than the conversation about it — where the thing outruns our ability to make sense of the thing. Paying attention, clearly and without panic, is not a small act right now. It might be the whole job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this means for us
&lt;/h2&gt;

&lt;p&gt;Here's where I land.&lt;/p&gt;

&lt;p&gt;The question that gets all the airtime — "is this AGI or not?" — is mostly a definitional argument, and it'll never be settled, because we never agreed on the line. It's the wrong thing to fixate on.&lt;/p&gt;

&lt;p&gt;The real question is quieter and harder: machines are becoming capable of more and more of what we thought was uniquely, permanently ours — and doing it faster than we're adjusting. What do we do with that? How do we keep human judgment, meaning, and agency at the center while the tools race ahead? How do we use this well instead of just being used by it?&lt;/p&gt;

&lt;p&gt;And the thing to hold onto is that we are not passengers watching this happen from the window. The choices about how these systems get built, how they get used, where the guardrails go, and what stays human — those are being made right now, by people. Including, in whatever corner of it you touch, you.&lt;/p&gt;

&lt;p&gt;The capability is going to keep coming. Fast. The open question was never really about the machines. It's about what we decide to do while they get more capable — and whether we stay awake enough to decide it on purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;When did you first feel the ground shift with AI — the moment it stopped being a novelty and became something you had to take seriously? And honestly: where do you think this is heading? I'd rather hear a room full of thoughtful guesses than one confident prediction.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>discuss</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>Agents That Act Need Brakes, Not Just Brains</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:05:06 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/agents-that-act-need-brakes-not-just-brains-54h2</link>
      <guid>https://dev.to/james_anderson_h/agents-that-act-need-brakes-not-just-brains-54h2</guid>
      <description>&lt;p&gt;Here's the moment a lot of us had this year.&lt;/p&gt;

&lt;p&gt;You built an agent. It was genuinely impressive — it reasoned, it planned, it called tools, it chained steps together to get real work done. The demo was so good that you gave it real permissions. Let it send the emails. Let it update the records. Let it hit the production API.&lt;/p&gt;

&lt;p&gt;And then, one day, it did something you never sanctioned. Not maliciously — confidently. Correctly-looking. It completed the action, reported success, and moved on. You only found out later, when the consequence surfaced somewhere downstream. That's the moment you realize what you actually shipped: a system with a powerful engine and no brake pedal.&lt;/p&gt;

&lt;p&gt;The entire industry has spent two years racing to make agents &lt;em&gt;smarter&lt;/em&gt;. Better reasoning, longer context, more tools, more autonomy. Almost nobody spent that time making them &lt;em&gt;safe to let act&lt;/em&gt;. And those are completely different problems — because the instant an agent stops just &lt;em&gt;talking&lt;/em&gt; and starts &lt;em&gt;doing&lt;/em&gt;, a more capable agent isn't more useful. It's more dangerous.&lt;/p&gt;

&lt;p&gt;Let me make the case, and then talk about the brakes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brains and brakes are different problems
&lt;/h2&gt;

&lt;p&gt;This is the distinction the hype cycle blurred, so it's worth stating plainly.&lt;/p&gt;

&lt;p&gt;Making an agent &lt;strong&gt;capable&lt;/strong&gt; — able to reason about a task, plan a sequence of steps, pick the right tool, recover from an error — is one problem, and it's the one all the model progress went into. Every new release is a better brain.&lt;/p&gt;

&lt;p&gt;Making an agent &lt;strong&gt;safe to let act&lt;/strong&gt; — ensuring that when it's wrong, the wrongness is caught, bounded, reversible, or stopped before it does damage — is a completely different problem. And here's the uncomfortable part: &lt;strong&gt;no smarter model solves it for you.&lt;/strong&gt; A more capable agent doesn't act wrongly &lt;em&gt;less&lt;/em&gt;; it acts wrongly &lt;em&gt;faster and more convincingly&lt;/em&gt;. Upgrading the brain does nothing for the brakes, because the brakes were never in the model. They're in the architecture around it — the part you have to build yourself.&lt;/p&gt;

&lt;p&gt;You can have the best engine in the world. Without brakes, that just means you hit the wall sooner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "acting" changes everything
&lt;/h2&gt;

&lt;p&gt;A chat that only talks has a wonderful property: its mistakes are free. It says something wrong, you read it, you move on. Nothing happened in the world.&lt;/p&gt;

&lt;p&gt;The moment an agent can &lt;em&gt;act&lt;/em&gt;, three things change, and each one is a reason you need control that a chatbot never did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Irreversibility.&lt;/strong&gt; A wrong sentence costs nothing. A wrong &lt;code&gt;DELETE&lt;/code&gt;, a sent email, a processed refund, a pushed commit — those don't have an undo button. The cost of an agent's mistake stops being "re-read it" and becomes "clean up the damage," and some damage doesn't clean up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence is not correctness.&lt;/strong&gt; This is the through-line of everything I write, and it's never more dangerous than when an agent can act. The agent takes the wrong action with exactly the same confidence it takes the right one. There is no tremor in its voice, no hedge, no tell. It is equally sure when it's about to help you and when it's about to hurt you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent success.&lt;/strong&gt; The nastiest one. The agent completes the wrong action and reports &lt;em&gt;success&lt;/em&gt; — because from its point of view, it did complete the task, it just completed the wrong one. Your monitoring goes green. Nobody sees the wrong customer getting the refund until the wrong customer gets the refund. "It worked" and "it did the right thing" are different claims, and only one of them shows up on the dashboard.&lt;/p&gt;

&lt;p&gt;Put those together and you get the core risk: an agent that can act is a system that can cause irreversible harm, confidently, without anything flagging it. That is precisely the situation brakes exist for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brake #1: Approval gates — ask before acting
&lt;/h2&gt;

&lt;p&gt;The most basic brake, and the one most systems skip: for anything consequential, the agent doesn't &lt;em&gt;do&lt;/em&gt; the thing. It &lt;em&gt;proposes&lt;/em&gt; the thing, and waits for a human to say yes.&lt;/p&gt;

&lt;p&gt;The skill here isn't gating everything — that would make the agent useless, and you'd click "approve" on autopilot until the gate meant nothing. The skill is &lt;strong&gt;gating by blast radius&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-run&lt;/strong&gt; the trivial and reversible — read something, format something, draft something. No gate needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask first&lt;/strong&gt; for the consequential — sending, paying, writing to production, anything a user would want to see before it happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft-only&lt;/strong&gt; for the dangerous — the agent prepares the action but cannot execute it at all; a human does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake to avoid is a uniform gate on everything, which trains people to rubber-stamp. A good gate is rare enough that when it fires, the human actually looks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brake #2: A reviewer that can actually say no
&lt;/h2&gt;

&lt;p&gt;The popular pattern: a second agent reviews the first agent's work before it's allowed to proceed. A judge. A critic. It's a good pattern — and it has a failure mode that will fool you completely if you're not watching for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reviewer that has never been seen to fail is indistinguishable from a reviewer that approves everything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your judge-agent has been green for months, that is not evidence it's working. It might be catching problems. It might also be rubber-stamping, silently broken, or checking the wrong thing entirely — and you would not be able to tell the difference from the outside, because both produce the same clean logs right up until the day the rubber stamp waves through the thing that hurts you.&lt;/p&gt;

&lt;p&gt;A brake you have never tested is not a brake. It's a brake-shaped object. So if you build a reviewer, you have to build the thing that proves it can still say no: wire a known-bad action through the &lt;em&gt;live&lt;/em&gt; review path, on a schedule, and confirm the reviewer rejects it. Surface when it last refused something, the way you'd surface uptime. If that "last refusal" date goes stale, your brake may already be dead and you just can't see it yet. Prove the "no" is reachable, continuously, or you're trusting a checker you've never watched work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brake #3: An audit trail — know what it did
&lt;/h2&gt;

&lt;p&gt;You cannot control what you cannot see. If an agent takes actions in the world and you have no durable record of what it did, when, and why, then a wrong action is invisible until its damage surfaces on its own schedule — usually the worst possible one.&lt;/p&gt;

&lt;p&gt;Every consequential action should leave a trail: what the agent did, what triggered it, what it was acting on, and ideally a path to reverse it. This isn't bureaucracy. It's the difference between "we caught the bad action in the log an hour later and rolled it back" and "a customer told us three weeks later, and we have no idea how many others it happened to."&lt;/p&gt;

&lt;p&gt;An audit trail is also what makes every &lt;em&gt;other&lt;/em&gt; brake improvable — you can't tune a gate or a reviewer if you can't see what got through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brake #4: Blast-radius limits — caps and scopes
&lt;/h2&gt;

&lt;p&gt;The final brake is the one that works even when every other brake fails: hard limits the agent physically cannot exceed, enforced by the system rather than by the agent's good behavior.&lt;/p&gt;

&lt;p&gt;Rate caps (no more than N actions per minute). Spend caps (cannot exceed $X without escalation). Scoped permissions (can read these tables, can write only that one). Iteration limits (stop after K steps instead of looping forever). These don't depend on the agent reasoning correctly — they're walls, and walls hold whether or not the thing inside them is behaving.&lt;/p&gt;

&lt;p&gt;This is where these controls show up in practice, and a few platforms now build them in as first-class features rather than something you bolt on — for instance, Xenition ships approval gates, an audit log, and a second agent that reviews the first's work before it reaches you (disclosure: it's a product I've been exploring). But whether you adopt a workspace that includes them or wire them yourself, the principle is the same and it's not optional: &lt;strong&gt;the agent's freedom should be bounded by design, not by hope.&lt;/strong&gt; You do not want "it usually behaves" to be the only thing standing between your agent and your production database.&lt;/p&gt;




&lt;h2&gt;
  
  
  The uncomfortable truth: brakes are unsexy
&lt;/h2&gt;

&lt;p&gt;Here's why, despite all of the above being fairly obvious, most agents ship without any of it.&lt;/p&gt;

&lt;p&gt;Nobody demos the approval gate. Nobody opens a pitch with their audit log. Brains are exciting — they make the standup go "wow." Brakes are boring — they only ever matter on the day something goes wrong, and if they're doing their job, that day never visibly arrives, so it looks like you built them for nothing.&lt;/p&gt;

&lt;p&gt;Choosing to build the unglamorous safety layer &lt;em&gt;before&lt;/em&gt; you need it, when there's no incident yet to justify it, is exactly the kind of discipline that doesn't get applause. It's the senior move precisely because it's invisible when it works. The engineer who spent a day on blast-radius limits instead of adding a fifth capability looks slower — right up until the week their agent &lt;em&gt;doesn't&lt;/em&gt; drain the account.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Capability has become table stakes. Every model is smart enough now; smartness is no longer where systems differ. &lt;strong&gt;Controllability is the differentiator&lt;/strong&gt; — whether you can let the thing act without lying awake about what it might do.&lt;/p&gt;

&lt;p&gt;So the question to ask about your agent isn't "how smart can I make it?" It's "what happens when it's confidently wrong — and can I stop it in time?" If the answer is "I'm trusting it not to be," you don't have an agent you can deploy. You have a liability with a good demo.&lt;/p&gt;

&lt;p&gt;Build the brakes. Not because your agent is dumb, but because someday it will be confidently, fluently, irreversibly wrong — and on that day, the only thing that matters is whether you built a way to stop it before you needed one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the one action you'd never let an agent take without a human in the loop? And the more honest question: have you actually wired that gate — and tested that it fires — or are you quietly trusting the agent to behave? I want to hear where people drew the line.&lt;/em&gt;&lt;br&gt;
Disclaimer: This article was written with AI assistance and reviewed and edited by me before publishing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building With AI When You Don't Know Architecture: A Survival Guide</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Tue, 01 Sep 2026 05:53:57 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/building-with-ai-when-you-dont-know-architecture-a-survival-guide-1ma3</link>
      <guid>https://dev.to/james_anderson_h/building-with-ai-when-you-dont-know-architecture-a-survival-guide-1ma3</guid>
      <description>&lt;p&gt;Let me describe a moment you might recognize.&lt;/p&gt;

&lt;p&gt;You had an idea for an app. You didn't know how to build it — not really — but you opened an AI chat, described what you wanted, and it gave you working code. You ran it. It &lt;em&gt;worked&lt;/em&gt;. It felt like magic, because it was: something you couldn't have written a month ago was running on your screen.&lt;/p&gt;

&lt;p&gt;So you added another feature. And another. And somewhere around the fifth one, things started breaking. You'd fix one thing and two others would stop working. You'd ask the AI to help, but you couldn't even explain what was wrong, because you didn't understand your own project anymore. Every change felt like defusing a bomb with the lights off.&lt;/p&gt;

&lt;p&gt;That wall — not the coding — is where most people get stuck. And here's the thing nobody tells you: &lt;strong&gt;AI removed the barrier to &lt;em&gt;writing&lt;/em&gt; code. It did not remove the barrier to &lt;em&gt;structuring&lt;/em&gt; it.&lt;/strong&gt; Those are two different skills, and the second one is now the one that decides whether you can keep building or watch your project collapse.&lt;/p&gt;

&lt;p&gt;This is a survival guide for that exact wall. No computer science degree required. Just the handful of habits that keep an AI-built project from turning into spaghetti — explained for someone starting from zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem: you can generate code faster than you can understand it
&lt;/h2&gt;

&lt;p&gt;Before the how-to, understand what's actually going wrong, because it's not what beginners think.&lt;/p&gt;

&lt;p&gt;The problem is almost never that the AI wrote &lt;em&gt;bad&lt;/em&gt; code. The problem is that AI lets you produce a working app &lt;em&gt;before you understand how it's held together.&lt;/em&gt; You're flying a plane you can't see the controls of. As long as the autopilot holds, you're fine. The moment something needs adjusting, you're lost — because you never learned where anything is.&lt;/p&gt;

&lt;p&gt;"Architecture" sounds like a scary, advanced, computer-science thing. It isn't. In plain terms, architecture is just &lt;strong&gt;the shape of your project&lt;/strong&gt; — what the pieces are, what each one does, and how they connect. And structure is what makes a project &lt;em&gt;understandable&lt;/em&gt;. Understanding is what lets you keep building instead of getting buried.&lt;/p&gt;

&lt;p&gt;So this whole guide is really about one goal: &lt;strong&gt;stay able to understand your own project as it grows.&lt;/strong&gt; Everything below serves that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Survival skill #1: Separate the pieces
&lt;/h2&gt;

&lt;p&gt;This is the single most important habit, so give it the most attention.&lt;/p&gt;

&lt;p&gt;Left alone, AI will often cram everything into one giant file — the part that draws the screen, the part that does the actual work, and the part that talks to the database, all tangled together. It runs fine at first. But when everything is in one place, &lt;em&gt;every&lt;/em&gt; change risks breaking &lt;em&gt;everything&lt;/em&gt;, because nothing is separated from anything else.&lt;/p&gt;

&lt;p&gt;The fix is to keep three kinds of things apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What the user sees&lt;/strong&gt; — the interface, the buttons, the layout (often called the "UI" or "frontend").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the app does&lt;/strong&gt; — the actual logic, the rules, the calculations (the "business logic").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the data comes from&lt;/strong&gt; — talking to the database or external services (the "data layer").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these are separated, you can change how something &lt;em&gt;looks&lt;/em&gt; without touching what it &lt;em&gt;does&lt;/em&gt;, and change what it &lt;em&gt;does&lt;/em&gt; without breaking how it talks to your &lt;em&gt;data&lt;/em&gt;. Problems stay contained instead of spreading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instruction to paste to your AI:&lt;/strong&gt; &lt;em&gt;"Keep the interface, the business logic, and the data access in separate files. Don't mix them together."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That one sentence, used consistently, prevents more collapses than anything else in this guide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Survival skill #2: One job per thing
&lt;/h2&gt;

&lt;p&gt;Here's a rule simple enough to hold in your head: &lt;strong&gt;a file or a function should do one thing you can name in a single sentence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you try to describe what a file does and you have to say "it does this &lt;em&gt;and&lt;/em&gt; this &lt;em&gt;and&lt;/em&gt; also that" — it's doing too much, and it's going to become a place where bugs hide and changes go wrong.&lt;/p&gt;

&lt;p&gt;Small, single-purpose pieces are survivable. You can find them, understand them, and change one without fear. Giant do-everything files are quicksand: the more they hold, the more every edit becomes a gamble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instruction to paste to your AI:&lt;/strong&gt; &lt;em&gt;"Each file and function should have a single, clear responsibility. Split anything that's doing more than one job."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Survival skill #3: One home for each piece of data
&lt;/h2&gt;

&lt;p&gt;This is the number-one way beginner projects break, so watch for it carefully.&lt;/p&gt;

&lt;p&gt;As your app grows, the same piece of information — the logged-in user, the items in a cart, whatever — can end up &lt;em&gt;copied and tracked in several different places.&lt;/em&gt; Then those copies drift out of sync. One part of your app thinks the cart has three items, another thinks it has one, and your app starts behaving in ways that make no sense and are almost impossible to debug.&lt;/p&gt;

&lt;p&gt;The survival rule: &lt;strong&gt;every piece of data lives in exactly one place, and everything else reads from that one place.&lt;/strong&gt; One source of truth. Don't let the AI casually make copies of your state scattered around the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instruction to paste to your AI:&lt;/strong&gt; &lt;em&gt;"This data should have a single source of truth. Don't duplicate it across components — have everything read from one place."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This one prevents the most maddening category of bug there is: the kind where nothing is technically broken, but nothing agrees.&lt;/p&gt;




&lt;h2&gt;
  
  
  Survival skill #4: Decide the shape &lt;em&gt;before&lt;/em&gt; you generate
&lt;/h2&gt;

&lt;p&gt;Most beginners build like this: think of a feature, ask the AI for it, repeat. The AI improvises structure each time, and the pieces never fit together — because nobody decided how they should.&lt;/p&gt;

&lt;p&gt;The shift that changes everything: &lt;strong&gt;name the parts of your app before you build them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before generating code, write down the main pieces and what each is responsible for. For a simple app that might be: &lt;em&gt;login, user profile, the main dashboard, payments.&lt;/em&gt; Four pieces, each with a clear job. That list — knowing your pieces and their responsibilities &lt;em&gt;in advance&lt;/em&gt; — is architecture, in plain language. You just did it. It's not scary.&lt;/p&gt;

&lt;p&gt;Then build &lt;strong&gt;one piece at a time&lt;/strong&gt;, fully, before moving to the next. Don't ask the AI to generate the whole app in one go — you'll get a tangle you can't understand. Build the login. Get it working. Understand it. Then the next piece.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instruction to paste to your AI:&lt;/strong&gt; &lt;em&gt;"Here are the main components of my app and what each does: [your list]. Let's build them one at a time, starting with [X]."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Survival skill #5: Make the AI explain itself
&lt;/h2&gt;

&lt;p&gt;This is the habit that turns building into &lt;em&gt;learning&lt;/em&gt; — and it's what slowly turns a beginner into someone who actually understands structure.&lt;/p&gt;

&lt;p&gt;Don't just accept the code the AI gives you and move on. Ask it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Why did you structure it this way?"&lt;/li&gt;
&lt;li&gt;"What does each part do, in plain English?"&lt;/li&gt;
&lt;li&gt;"If I change this, what else will be affected?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're using the AI as a &lt;strong&gt;tutor&lt;/strong&gt;, not a vending machine. Every answer teaches you a little more about how your project fits together — which is exactly the knowledge you started without. Do this consistently and, project by project, you stop being someone who &lt;em&gt;generates&lt;/em&gt; code and become someone who &lt;em&gt;understands&lt;/em&gt; it. That understanding is the whole game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instruction to paste to your AI:&lt;/strong&gt; &lt;em&gt;"Before I use this, explain how it's structured and why, in simple terms — I want to understand it, not just run it."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Survival skill #6: Keep it boring and consistent
&lt;/h2&gt;

&lt;p&gt;Two habits bundled together, because they work as a pair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boring beats clever.&lt;/strong&gt; When the AI offers you a slick, complex, impressive-looking solution, ask if there's a simpler one. As a non-architect, the fancy version isn't a win — it's a liability, because complexity you don't understand is complexity you can't maintain or fix. The simple, obvious version is the one you'll still be able to work with next month. Always prefer it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency beats variety.&lt;/strong&gt; If you build feature A one way and feature B a completely different way (easy to do across separate AI sessions), your project becomes a patchwork where nothing follows the same rules and everything is confusing. Make new features match the pattern of existing ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instruction to paste to your AI:&lt;/strong&gt; &lt;em&gt;"Give me the simplest version that works, not the cleverest. And match the structure and patterns already in my project."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Know the warning signs before it's too late
&lt;/h2&gt;

&lt;p&gt;Here's your smoke alarm. When you start feeling any of these, &lt;em&gt;stop adding features&lt;/em&gt; and clean up the structure first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're &lt;strong&gt;afraid to change things&lt;/strong&gt; because you don't know what will break.&lt;/li&gt;
&lt;li&gt;One fix keeps &lt;strong&gt;breaking something else&lt;/strong&gt;, over and over.&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;can't find&lt;/strong&gt; where anything is anymore.&lt;/li&gt;
&lt;li&gt;Files keep &lt;strong&gt;growing and growing&lt;/strong&gt; and doing more and more.&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;don't understand your own project&lt;/strong&gt; when you look at it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these mean you failed. They mean your project outgrew its structure — which happens to everyone, including professionals. The difference is that now you know what the feeling means, and what to do: pause, separate the pieces, and get your understanding back before you build more on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;You do not need a computer science degree to build real software with AI. That barrier is genuinely gone, and it's not coming back.&lt;/p&gt;

&lt;p&gt;But "I can generate code" and "I can build something that survives its own growth" are different things, and the gap between them is &lt;em&gt;structure&lt;/em&gt; — the one part the AI can't supply for you, because it depends on decisions about &lt;em&gt;your&lt;/em&gt; project that only you can make. AI gives you the code. You have to give it the shape.&lt;/p&gt;

&lt;p&gt;The good news is that the shape isn't hard. It's a handful of habits: keep the pieces separate, give each thing one job, one home for each piece of data, plan before you generate, make the AI teach you, and keep it boring. Do those, understand what you're building as you go, and you can build far more than you'd think — without watching it collapse.&lt;/p&gt;

&lt;p&gt;Start simple. Stay able to understand it. That's survival.&lt;br&gt;
Disclaimer: This article was written with AI assistance and reviewed and edited by me before publishing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>9 Ways Your AI Agent Silently Fails (and How to Catch Each)</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Mon, 31 Aug 2026 04:36:46 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/9-ways-your-ai-agent-silently-fails-and-how-to-catch-each-547f</link>
      <guid>https://dev.to/james_anderson_h/9-ways-your-ai-agent-silently-fails-and-how-to-catch-each-547f</guid>
      <description>&lt;p&gt;Your agent passed its tests. It ran clean in the demo. You shipped it.&lt;/p&gt;

&lt;p&gt;Two days later it's confidently returning wrong answers to real users — and your dashboard is still green. No error code. No stack trace. No 500. Nothing to grep for. The workflow completed. The response looks great. It's just wrong, and it has probably been wrong the same way a few dozen times already.&lt;/p&gt;

&lt;p&gt;This is the thing nobody warns you about when you move an agent to production: &lt;strong&gt;agents don't fail like normal software.&lt;/strong&gt; A database fails with an error code. An API fails with a 500. A bad LLM call fails with an obviously low-quality response you can spot. An agent fails by &lt;em&gt;completing the task and handing you a confident, well-formatted, plausible answer that happens to be false&lt;/em&gt; — and by the time a downstream consequence makes it visible, hours have passed and the root cause is buried.&lt;/p&gt;

&lt;p&gt;There's a name for this from classic reliability work: &lt;strong&gt;differential observability.&lt;/strong&gt; The application suffers, but the observer designed to notice reports health. Gray failures. Fail-slow. The monitor stays green while the thing it's watching quietly rots.&lt;/p&gt;

&lt;p&gt;Every failure below is a version of that same gap — a place where "success" and "correct" quietly come apart, and your monitoring only sees the first one. Here are nine of them, each with the symptom, why it stays invisible, and how to actually catch it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The HTTP 200 empty payload
&lt;/h2&gt;

&lt;p&gt;Tool calls fail 3–15% of the time in production. The loud failures are fine — a timeout, a 500, an exception you can catch. The &lt;strong&gt;silent&lt;/strong&gt; ones are the killers: a tool returns &lt;code&gt;HTTP 200&lt;/code&gt; with an empty body, a &lt;code&gt;null&lt;/code&gt;, or a garbage payload, and the agent treats it as a successful result and proceeds to reason over nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; everything downstream is technically "working." Status code says success. The agent got &lt;em&gt;a&lt;/em&gt; response. Nothing threw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; validate the &lt;em&gt;shape and content&lt;/em&gt; of every tool result, not just the status code. Assert the payload is non-empty, matches the expected schema, and contains the fields you're about to use. Treat a 200 with an empty body as a failure, loudly — because to your agent, it's worse than an error, it's a confident lie.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The poisoned step (error propagation)
&lt;/h2&gt;

&lt;p&gt;A single wrong tool argument at step 2 silently corrupts step 3, which feeds step 4, and so on. By the time you see a bad final answer at step 20, the actual defect is nineteen steps upstream and nearly impossible to trace. This is repeatedly cited as the single most common and most insidious agent failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; each individual step looks locally fine. The corruption is in the &lt;em&gt;hand-off&lt;/em&gt;, not any one action, and you're only inspecting the final output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; check intermediate outputs at &lt;em&gt;each&lt;/em&gt; reasoning step, not just the end. A guard that validates the state between steps flags the corruption at its origin — where it's cheap to fix — instead of at the final output, where the root cause is hardest to find. Bonus: each intercepted failure becomes a structured record (input state, tool call, what blocked it, which step), which is your audit trail.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Goal drift
&lt;/h2&gt;

&lt;p&gt;Over a long, multi-step run, the agent slowly wanders away from the objective you actually gave it. No single step is wrong. Step 8 is a reasonable follow-on from step 7. But the &lt;em&gt;sum&lt;/em&gt; of forty locally-reasonable steps ends up solving a subtly different problem than the one you asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; you're evaluating individual actions, and each one passes. Drift only shows up in the &lt;em&gt;trajectory&lt;/em&gt;, which nobody's measuring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; periodically re-anchor to the original goal — literally re-inject the objective and ask "is what I'm doing now still serving this?" Evaluate the whole trajectory against the stated intent, not just the final answer's surface plausibility. Goal drift is a trajectory bug; you can't catch it one step at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The amnesiac mid-task (context loss)
&lt;/h2&gt;

&lt;p&gt;The context window fills up during a long run. Tool definitions, key constraints, or facts established early get pushed out of scope. The agent keeps going — now quietly acting &lt;em&gt;without&lt;/em&gt; the information it had ten steps ago, and with no idea it lost anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; there's no error when context gets evicted. The agent doesn't know what it can no longer see; it just proceeds on a smaller picture and sounds equally confident doing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; monitor context budget as a first-class metric, and assert that critical facts (the constraints, the tool schemas, the original ask) are still present at the moment a decision is made. If the thing the agent needs to decide correctly has been evicted, that's a failure state — treat it like one instead of letting the agent guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The retry loop / runaway cost
&lt;/h2&gt;

&lt;p&gt;A tool call fails, so the agent retries. It fails again, so it retries again. With no hard limit, this runs indefinitely — and it doesn't produce a &lt;em&gt;wrong&lt;/em&gt; answer, it produces exploding latency and a cloud bill that can burn through your budget in minutes, before any human notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; there's no incorrect output to catch — the agent is "still working." The damage is in cost and latency, which your correctness monitoring isn't watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; hard iteration caps and explicit loop detection are non-negotiable. Cap total steps, cap retries per tool, and wire a cost/latency alarm that fires on &lt;em&gt;runtime&lt;/em&gt;, not just on errors. An agent that never terminates is a failure even though it never returns anything wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Plausible-speech hallucination
&lt;/h2&gt;

&lt;p&gt;When an upstream error leaks into the agent's context, its failure mode is not silence — it's &lt;em&gt;fluent, confident narrative built around the bad data.&lt;/em&gt; The agent takes the corrupted input and writes you a beautifully reasoned, completely wrong explanation. This is the failure that's unique to language-generating systems: the output's polish is inversely related to how much you should trust it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; fluency reads as competence. A confident, well-structured answer &lt;em&gt;feels&lt;/em&gt; correct, and there's no signal in the prose itself that the premise underneath it was garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; never treat fluency as a health signal — it carries zero information about accuracy. Add a verification pass that checks the final claims against the actual source data or system state. If the answer asserts something, confirm the something is true before you ship it, no matter how good it sounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The unsanctioned action
&lt;/h2&gt;

&lt;p&gt;The agent retrieves exactly the right context — and then does something with it that nobody authorized. High retrieval accuracy makes this &lt;em&gt;worse&lt;/em&gt;, not better, because the unpermitted action arrives wrapped in real, correct evidence. It looks justified. The chunk was right, the reasoning was clean, the action was never allowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; every quality check passes. Retrieval was accurate, the citation is real, the logic is sound. Nothing in your retrieval or faithfulness evals asks "was the agent &lt;em&gt;permitted&lt;/em&gt; to do this?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; enforcement is a separate failure surface from retrieval, and it needs its own gate. "Found the right context" and "allowed to act on it this way" are different claims — and only one of them is what your accuracy metrics measure. Put a deterministic policy check between decision and action, so a perfectly-grounded-but-unauthorized action gets stopped regardless of how good the evidence looks.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Cross-surface / cross-model inconsistency
&lt;/h2&gt;

&lt;p&gt;A prompt that worked perfectly on one model behaves differently on another. Your agent gives one answer on the web surface and a different one on mobile, or drifts when you swap the underlying model, and nothing flags the divergence. Users hit different behavior depending on where and when they showed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; each individual response looks fine in isolation. The bug is the &lt;em&gt;inconsistency between&lt;/em&gt; responses, which no single-run check can see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; run consistency checks across models and across repeated runs of the same input — if two "equivalent" paths disagree, that disagreement is signal. And pin your deployment: test against the exact model version you actually ship, because "it worked on the old model" is not evidence it works now.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. The green checkmark that can't fail
&lt;/h2&gt;

&lt;p&gt;The most insidious one, because it's the failure in your &lt;em&gt;safety net itself.&lt;/em&gt; Your guard, your eval, your reviewer-agent, your monitor — it's been green for months. Reassuring, right? Except: have you ever seen it go red? A check nobody has ever seen fail is indistinguishable from a check that approves everything. The two produce identical logs right up until the day the rubber stamp waves through the thing that hurts you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's invisible:&lt;/strong&gt; silence and health look identical. Most systems model &lt;em&gt;pass&lt;/em&gt; and &lt;em&gt;fail&lt;/em&gt; and nothing else — so a guard that has quietly lost the ability to fail reads exactly like a guard that simply had nothing to catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to catch it:&lt;/strong&gt; wire a known-bad case through the &lt;em&gt;live&lt;/em&gt; path (not a side test), on a schedule, and surface the timestamp of its last refusal like an uptime number — "last time this guard said no: 2 days ago." If that date ages past the schedule, staleness itself is the alarm. Prove your checks can fail, continuously, or you don't have guards — you have decorations.&lt;/p&gt;




&lt;h2&gt;
  
  
  One bug wearing nine costumes
&lt;/h2&gt;

&lt;p&gt;Step back and all nine are the same disease.&lt;/p&gt;

&lt;p&gt;Traditional systems model two states: &lt;strong&gt;pass&lt;/strong&gt; and &lt;strong&gt;fail&lt;/strong&gt;. Green and red. But every failure above lives in a third state your monitoring doesn't have a name for — &lt;em&gt;unverified&lt;/em&gt;, &lt;em&gt;unsanctioned&lt;/em&gt;, &lt;em&gt;not-checked-lately&lt;/em&gt;, &lt;em&gt;observed-but-not-validated&lt;/em&gt;. And because there's no state for it, the absence of a red signal gets read as health. Silence gets read as success.&lt;/p&gt;

&lt;p&gt;The fix pattern recurs across all nine, and it comes down to two disciplines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existence checks, not just quality checks.&lt;/strong&gt; Don't only ask "is this output good?" Ask "did I actually observe what I claim to have observed?" A tool returned 200 — did it return &lt;em&gt;data&lt;/em&gt;? Retrieval succeeded — was the action &lt;em&gt;permitted&lt;/em&gt;? The guard is green — has it &lt;em&gt;ever&lt;/em&gt; gone red?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify at every boundary, and build a state for "not verified."&lt;/strong&gt; Check between steps, not just at the end. Confirm claims against source, not against fluency. Make "we haven't validated this lately" a visible, alarming state instead of an invisible assumption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents fail differently from everything you've monitored before, and the tools most teams reach for — log dashboards, error-rate monitors, single-turn observability — are built to catch crashes. Agents don't crash. They succeed, confidently, at the wrong thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 60-second self-audit
&lt;/h2&gt;

&lt;p&gt;Run these against your own agent right now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a tool returns 200, do you validate the &lt;strong&gt;payload&lt;/strong&gt;, or just the status?&lt;/li&gt;
&lt;li&gt;Do you check &lt;strong&gt;intermediate&lt;/strong&gt; step outputs, or only the final answer?&lt;/li&gt;
&lt;li&gt;Is there a &lt;strong&gt;hard cap&lt;/strong&gt; on iterations and retries, with a cost alarm?&lt;/li&gt;
&lt;li&gt;Do you verify final claims against &lt;strong&gt;source/state&lt;/strong&gt;, or trust the prose?&lt;/li&gt;
&lt;li&gt;Is there a &lt;strong&gt;policy gate&lt;/strong&gt; between "decided" and "acted"?&lt;/li&gt;
&lt;li&gt;Can your monitoring actually &lt;strong&gt;go red&lt;/strong&gt; — and when did it last?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any answer is "uh… I'd have to check," you've found a silent failure waiting to happen.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Which of these has bitten you in production — and, the more useful question, what's the silent failure mode I left off the list? The agents that hold up aren't the ones with the best models; they're the ones instrumented for the failure that doesn't announce itself. I want to hear the ones that fooled you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Prompt Fluency Is the New Literacy — and Most Devs Are Winging It</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Sun, 30 Aug 2026 04:58:39 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/prompt-fluency-is-the-new-literacy-and-most-devs-are-winging-it-3ckf</link>
      <guid>https://dev.to/james_anderson_h/prompt-fluency-is-the-new-literacy-and-most-devs-are-winging-it-3ckf</guid>
      <description>&lt;p&gt;I need to confess something, because I suspect you've done it too.&lt;/p&gt;

&lt;p&gt;A few months ago I caught myself saying, out loud, "Claude's gotten worse at this." I'd asked it to do something, gotten back something mediocre, sighed, and filed it under &lt;em&gt;the model is slipping.&lt;/em&gt; Very satisfying. Nobody's fault but the AI's.&lt;/p&gt;

&lt;p&gt;Then I watched a colleague hand the &lt;em&gt;same model&lt;/em&gt; a better-formed version of the same request and get back exactly what I'd failed to get. Same model. Same day. Same task. Wildly different output.&lt;/p&gt;

&lt;p&gt;It wasn't the model. It was me. I just really, really didn't want it to be me.&lt;/p&gt;

&lt;p&gt;That's the whole article, honestly. But let me make the uncomfortable case in full, because I think this is one of the biggest quiet skill gaps in software right now — and almost nobody is treating it like a skill at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The model isn't the variable anymore. You are.
&lt;/h2&gt;

&lt;p&gt;For a couple of years, "better results" had an easy explanation: better model. If your output was weak, you waited for the next release, upgraded, and things improved. The tool was the variable, so the tool was the excuse.&lt;/p&gt;

&lt;p&gt;That excuse is dead, and it's not coming back.&lt;/p&gt;

&lt;p&gt;Everyone has the same frontier models now. You, your teammate, your competitor, the intern — same GPT, same Claude, same Gemini, one click apart. Raw access stopped being an edge the moment it became universal.&lt;/p&gt;

&lt;p&gt;So here's the part that stings: when two developers get dramatically different results out of the &lt;em&gt;identical&lt;/em&gt; model, the difference has nowhere left to hide. It isn't the model — you both have the same one. It's the operator. It's the ask. It's the fluency.&lt;/p&gt;

&lt;p&gt;We're standing in front of the same instrument getting different music out of it, and most of us are still blaming the instrument.&lt;/p&gt;

&lt;p&gt;Prompt fluency is the last remaining variable in the equation, and the majority of developers are pretending it's still about the tool — because "the model isn't good enough yet" is a much more comfortable sentence than "I'm not good at this yet."&lt;/p&gt;




&lt;h2&gt;
  
  
  A vague prompt is a vague thought wearing a question mark
&lt;/h2&gt;

&lt;p&gt;Here's the reframe that changed how I see this, and it's the part most "prompt tips" articles completely miss.&lt;/p&gt;

&lt;p&gt;When you get a bad answer from a capable model, your instinct is that you &lt;em&gt;phrased it wrong&lt;/em&gt; — that there's some magic wording you're missing, some incantation that unlocks the good output. That's why prompt-hack listicles do numbers: they promise the secret phrase.&lt;/p&gt;

&lt;p&gt;But most bad prompts aren't a wording failure. They're a &lt;strong&gt;thinking&lt;/strong&gt; failure.&lt;/p&gt;

&lt;p&gt;You didn't fail to express what you wanted. You hadn't actually &lt;em&gt;decided&lt;/em&gt; what you wanted — and the model, doing exactly its job, reflected your fuzziness right back at you. A vague request produced a vague result because the request &lt;em&gt;was&lt;/em&gt; the vagueness, made visible.&lt;/p&gt;

&lt;p&gt;The model is a mirror for the clarity of your intent. If the reflection is blurry, check the thing standing in front of it before you blame the glass.&lt;/p&gt;

&lt;p&gt;This is also why "prompt hacks" are mostly a dead end. You cannot trick your way out of not knowing what you're asking for. There's no magic phrase that compensates for an undecided goal, unspecified constraints, and no definition of what "good" looks like. The secret isn't a better spell. It's actually knowing what you want before you ask — which is harder, less fun to tweet, and the entire game.&lt;/p&gt;




&lt;h2&gt;
  
  
  Winging it has a bill — and you're paying it without seeing the invoice
&lt;/h2&gt;

&lt;p&gt;"Okay," you might say, "but I get there eventually. A few tries and it's fine." Sure. But look at what those few tries actually cost, because the invoice is itemized and most people never read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Charge one: the correction spiral.&lt;/strong&gt; You fire off a fuzzy prompt, get something wrong, and start the "no, not like that" loop — fifteen rounds of nudging the model toward the thing a thirty-second upfront spec would have landed in one. You &lt;em&gt;feel&lt;/em&gt; productive because you're actively working. You're just paying in small installments for a bill you could have settled once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Charge two: the un-reusable result.&lt;/strong&gt; Even when you finally get something okay, you got it through improvised back-and-forth you couldn't reproduce if you tried. So it doesn't compound. Tomorrow's similar task starts from zero, because you never captured &lt;em&gt;how&lt;/em&gt; you got there — only the messy output, not the method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Charge three — and this is the expensive one: the false verdict.&lt;/strong&gt; You conclude "the AI just isn't good at this," and you stop. Except it wasn't the AI — it was the spec you handed it. So now you've done real damage: you've &lt;em&gt;permanently underrated a capable tool&lt;/em&gt; based on your own unclear ask, and you'll avoid using it for exactly the things it could've handled.&lt;/p&gt;

&lt;p&gt;That third charge is the real cost of winging it. It doesn't only waste your afternoon. It teaches you &lt;em&gt;wrong lessons&lt;/em&gt; about what these tools can do — and wrong lessons are stickier and more expensive than wasted time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What fluency actually looks like
&lt;/h2&gt;

&lt;p&gt;None of this is mystical, and that's the good news. Prompt fluency is a set of learnable, boring, repeatable habits — closer to writing a good bug report than casting a spell. Here's the core of it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State the constraints, not just the goal.&lt;/strong&gt; "Write a function to parse this" is a wish. "Write a Python function that parses this CSV, handles missing fields by skipping the row, returns a list of dicts, and raises on malformed headers" is a spec. The model can only hit a target you actually drew.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give it the shape of the answer you want.&lt;/strong&gt; Format, length, structure, tone. If you know you want a table, or bullet points, or a single function with no explanation — say so. Leaving it implicit means you're gambling, then correcting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show it good and bad.&lt;/strong&gt; One example of what you want and one of what you don't teaches the model more than a paragraph of description. Concrete beats abstract, every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell it what to do when unsure.&lt;/strong&gt; "If you don't have enough information, ask instead of guessing" or "say you're not certain rather than inventing" — this alone kills a whole category of confident-wrong output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decompose instead of mega-prompting.&lt;/strong&gt; A giant vague ask produces a giant vague answer. Break it into steps you can verify. You'll debug the pieces instead of the blob.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capture what worked.&lt;/strong&gt; This is the one nobody does and everybody should — more on it next, because it's the difference between a skill and a leverage machine.&lt;/p&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; on this list: clever phrasing, secret keywords, jailbreak tricks. It's all just precise specification. Clear thinking, written down.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real unlock: fluency is reusable, not per-attempt
&lt;/h2&gt;

&lt;p&gt;Here's where prompt fluency stops being a nice habit and becomes actual leverage — and it's the insight that reframes the whole thing.&lt;/p&gt;

&lt;p&gt;Getting marginally faster at improvising prompts is fine. But the real move isn't improvising &lt;em&gt;faster&lt;/em&gt;. It's not improvising at all.&lt;/p&gt;

&lt;p&gt;A good prompt — a real, precise, constraint-loaded spec that reliably gets you what you want — is worth writing &lt;strong&gt;once&lt;/strong&gt; and keeping &lt;strong&gt;forever&lt;/strong&gt;. Save it. Template it. Turn it into a reusable form. After that, the marginal cost of getting a great result on that kind of task drops toward zero, because you're no longer re-deriving the spec every single time. You're invoking it.&lt;/p&gt;

&lt;p&gt;This is the part that turns a 20%-of-the-time-it-works habit into something categorically different. The expensive part of prompting was never the typing — it was the &lt;em&gt;deciding&lt;/em&gt;: what do I want, what are the constraints, what does good look like. Do that thinking once, capture it, and every future use is nearly free.&lt;/p&gt;

&lt;p&gt;That's the difference between a developer who's a fast typist at the AI and a developer who's built a personal library of specifications that consistently produce reusable output. One is winging it quickly. The other has stopped winging it entirely. Only one of those compounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that'll annoy some of you
&lt;/h2&gt;

&lt;p&gt;I know how a chunk of readers are reacting right now: &lt;em&gt;prompting isn't real engineering. It's typing English at a chatbot. It'll be obsolete when the models get good enough to read my mind.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to gently point out that this exact dismissiveness &lt;em&gt;is&lt;/em&gt; the winging-it problem, wearing a lab coat.&lt;/p&gt;

&lt;p&gt;"It's beneath me to get good at this" is precisely the attitude that keeps someone improvising, blaming the model, and getting mid results while a colleague who took it seriously runs circles around them with the same tools. The disdain isn't protecting you from a fake skill. It's protecting you from noticing a real one you haven't developed.&lt;/p&gt;

&lt;p&gt;And "it'll be obsolete when models improve" gets it backwards. Better models don't make specification &lt;em&gt;less&lt;/em&gt; important — they make it &lt;em&gt;more&lt;/em&gt; valuable, because a more capable model can execute a precise spec far more completely than a weak one could. The ceiling on what you can get out of these tools is rising, and the thing that determines how close you get to that ceiling is how clearly you can say what you want. That skill isn't going away. It's the one thing that's been appreciating this whole time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Prompt fluency is quietly becoming table stakes — the way knowing how to search well, or write a clear commit message, or read documentation became table stakes. It's not glamorous. It's not a trick. It's clear specification, made into a habit, and then captured so you don't have to repeat the thinking.&lt;/p&gt;

&lt;p&gt;The developers who treat it as a craft are getting consistent, reusable, compounding leverage out of the exact same models everyone else has. The ones winging it are getting inconsistent results and a growing list of wrong conclusions about what AI "can't do."&lt;/p&gt;

&lt;p&gt;Same instrument. Very different music. The gap isn't the model anymore. It hasn't been for a while.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the one prompt habit that actually moved your hit rate — and, real question: do you save your good prompts, or retype them from scratch every single time? I'm trying to figure out if prompt-capturing is a rare discipline or if I've just been the last one to it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>career</category>
      <category>llm</category>
    </item>
    <item>
      <title>How a 6B-Active Model Beats 17B-Active Ones: What Qwen3.8-Flash-Next Actually Changed</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Sat, 29 Aug 2026 06:07:05 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/how-a-6b-active-model-beats-17b-active-ones-what-qwen38-flash-next-actually-changed-472d</link>
      <guid>https://dev.to/james_anderson_h/how-a-6b-active-model-beats-17b-active-ones-what-qwen38-flash-next-actually-changed-472d</guid>
      <description>&lt;p&gt;Here's a number that shouldn't make sense on first read.&lt;/p&gt;

&lt;p&gt;Qwen just released Qwen3.8-Flash-Next, and it activates &lt;strong&gt;6 billion parameters per token&lt;/strong&gt; — while matching or beating models that activate &lt;strong&gt;13B (DeepSeek-V4-Flash) and 17B (Qwen3.7-Plus)&lt;/strong&gt; per token. On its own base benchmarks, the 6B-active model reportedly tops 8 of 14 tests against much heavier-activating competition.&lt;/p&gt;

&lt;p&gt;Your instinct is probably: &lt;em&gt;smaller model, worse results — that's how this works.&lt;/em&gt; And for a long time, roughly, it did. So how does a model doing less compute per token beat models doing two to three times more?&lt;/p&gt;

&lt;p&gt;The answer isn't a magic benchmark trick. It's a concept most people skip past when they read a model's spec sheet — and once you understand it, a lot of the modern AI efficiency race suddenly makes sense. Let's break it down.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: the number on the spec sheet that actually matters
&lt;/h2&gt;

&lt;p&gt;When people size up a model, they quote &lt;strong&gt;total parameters&lt;/strong&gt; — "it's a 125B model," "it's a 400B model." That's the number in the headline, and it's almost the wrong one to care about for cost and speed.&lt;/p&gt;

&lt;p&gt;There are two numbers, and they mean very different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total parameters&lt;/strong&gt; — how big the model is on disk. Everything it knows, stored in its weights. This governs how much memory you need to &lt;em&gt;hold&lt;/em&gt; the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active (activated) parameters&lt;/strong&gt; — how many of those parameters actually get used to process each individual token. This governs how much &lt;em&gt;computation&lt;/em&gt; happens per token — which drives inference cost, speed, and latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a traditional ("dense") model, these two numbers are identical: every parameter fires for every token. A 27B dense model does 27B parameters' worth of math on every single token, no exceptions. That's simple and predictable — and expensive, because you're paying full price on every token whether the token needs it or not.&lt;/p&gt;

&lt;p&gt;The whole trick of the last few years has been &lt;strong&gt;breaking that link&lt;/strong&gt; — building models where total is huge (so the model &lt;em&gt;knows&lt;/em&gt; a lot) but active is small (so each token is &lt;em&gt;cheap&lt;/em&gt; to compute). That's what lets Qwen3.8-Flash-Next store ~180B parameters on disk yet route each token through only 6B of them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The one-line version: total parameters tell you how much the model knows; active parameters tell you what it costs to run. For price and speed, active is the number to watch.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What a Mixture-of-Experts actually is
&lt;/h2&gt;

&lt;p&gt;The technique that decouples those two numbers is &lt;strong&gt;Mixture-of-Experts (MoE)&lt;/strong&gt;, and it's simpler than the name suggests.&lt;/p&gt;

&lt;p&gt;Instead of one giant network where everything fires every time, an MoE splits much of the model into many smaller sub-networks called &lt;strong&gt;experts&lt;/strong&gt; — Qwen3.8-Flash-Next has a pool of 512 of them. For each token, a small component called the &lt;strong&gt;router&lt;/strong&gt; picks just a few experts to actually run. The rest sit idle for that token.&lt;/p&gt;

&lt;p&gt;An analogy: imagine a hospital with 512 specialists on staff. When a patient walks in, you don't wake up all 512 — you route them to the two or three relevant specialists. The hospital &lt;em&gt;contains&lt;/em&gt; an enormous amount of expertise (that's the total parameter count), but any single patient only &lt;em&gt;consumes&lt;/em&gt; a tiny slice of it (that's the active count). You get the knowledge of a huge institution at the cost of a small clinic visit.&lt;/p&gt;

&lt;p&gt;That's the core move. The model can be enormous in what it collectively knows, while any given token only pays for the handful of experts it actually needs. Total stays high; active stays low.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Qwen3.8-Flash-Next numbers, decoded
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ynycvze04kd6ci3840e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ynycvze04kd6ci3840e.png" alt="Source: Qwen team blog (qwen.ai) — vendor-reported figures." width="800" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Qwen team blog (qwen.ai) — vendor-reported figures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now the spec sheet reads very differently. Here's the shape of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~180B parameters total on disk&lt;/strong&gt; — a 125B MoE backbone, a 51B N-gram embedding table, and a small (~4B) multi-token-prediction module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6B parameters active per token&lt;/strong&gt; — a sparsity of roughly 95%. Nineteen of every twenty parameters sit idle on any given token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;262,144-token native context&lt;/strong&gt;, extensible to 1M.&lt;/li&gt;
&lt;li&gt;Trained, Qwen says, at about &lt;strong&gt;one-ninth the cost&lt;/strong&gt; of Qwen3.7-Plus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set that 6B-active figure next to the competition in Qwen's own comparison table: Qwen3.7-Plus activates 17B per token, DeepSeek-V4-Flash activates 13B. Qwen3.8-Flash-Next activates 6B — and still posts wins across a chunk of the coding, agent, and general benchmarks. Reported highlights include 62.5 on SWE-bench Pro, 73.9 on CoWorkBench, and strong multimodal numbers.&lt;/p&gt;

&lt;p&gt;The reason a 6B-active model can even &lt;em&gt;compete&lt;/em&gt; here is that its 6B is punching above a naive 6B: it's drawing on a 125B pool of specialized experts plus that 51B embedding table. It knows like a big model and computes like a small one. (More on how they pulled the active count down so far in a moment — and more on trusting these numbers near the end.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Why sparsity is the real story — not the benchmark scores
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbl3xg935dx1v749vsf1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgbl3xg935dx1v749vsf1.png" alt="Source: Qwen team blog (qwen.ai) — vendor-reported figures." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Qwen team blog (qwen.ai) — vendor-reported figures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's tempting to read a launch like this as "Qwen won some benchmarks." That's the boring, forgettable version. The actually-interesting story is the &lt;em&gt;direction&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The industry is proving you can keep pushing the active-parameter count down while capability stays roughly flat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about what that means economically. Inference cost scales with active parameters, not total. So if you can hold quality steady while cutting active params from 17B to 6B, you've made the model dramatically cheaper to run &lt;em&gt;without making it dumber&lt;/em&gt;. That's not a better score on a leaderboard — that's a structural shift in the cost of serving AI.&lt;/p&gt;

&lt;p&gt;You can see it in the price. The production version is served at &lt;strong&gt;$0.15 per million input tokens and $0.47 per million output&lt;/strong&gt; — the kind of number that's only possible because each token touches so little of the model. Cheap inference isn't a discount here; it's a direct consequence of the architecture.&lt;/p&gt;

&lt;p&gt;This is the same theme underneath a lot of recent model releases: the frontier isn't only "make it bigger and smarter," it's increasingly "make it &lt;em&gt;this&lt;/em&gt; smart for a fraction of the compute per token." Sparsity is where that race is being run.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four tricks that make ultra-low activation work
&lt;/h2&gt;

&lt;p&gt;Getting active params down to 6B without tanking quality took more than just "add more experts." Qwen stacked four changes, and you don't need the math to get the idea:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GDN + QSA hybrid attention.&lt;/strong&gt; Three of every four layers use Gated DeltaNet (GDN), which compresses history into a small fixed-size state — cheap "memory." The fourth layer uses Qwen Sparse Attention (QSA), which selectively pulls the important context back out — precise "retrieval." In Qwen's phrasing: &lt;em&gt;GDN remembers, QSA retrieves.&lt;/em&gt; Together they slash the cost of long context, which is normally where attention gets brutally expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N-gram embeddings.&lt;/strong&gt; A 51B lookup table that adds capacity based on local word patterns — and crucially, it's addressed by deterministic lookup, so it adds almost &lt;strong&gt;zero per-token computation&lt;/strong&gt; and can be offloaded to host memory rather than occupying the GPU. Capacity that's nearly free at inference time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gated Residual.&lt;/strong&gt; The residual stream (how information flows between layers) is widened into parallel branches with dynamic gates, so information moves through the network more effectively — meaning each active parameter does more useful work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ultra-sparse MoE.&lt;/strong&gt; A big expert pool (512) with only a few routed per token, plus one shared expert. More total knowledge, same small active slice.&lt;/p&gt;

&lt;p&gt;The common thread: every one of these adds &lt;em&gt;capability&lt;/em&gt; without adding much &lt;em&gt;per-token compute&lt;/em&gt;. That's the whole game — grow what the model knows, not what each token costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this means for you
&lt;/h2&gt;

&lt;p&gt;A few practical takeaways, whether you're choosing a model or just trying to read the field clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check active params before total.&lt;/strong&gt; When you're estimating cost or speed, "125B model" tells you almost nothing useful. "6B active" tells you what a token actually costs to compute. Total size predicts memory footprint; active size predicts your bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap-to-serve is becoming a design goal, not an afterthought.&lt;/strong&gt; Models are now being architected &lt;em&gt;around&lt;/em&gt; low activation. Expect more "huge total, tiny active" releases, and expect prices to reflect it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small-active doesn't mean small-footprint.&lt;/strong&gt; Here's the catch worth internalizing (see caveats below): 6B active is cheap to &lt;em&gt;compute&lt;/em&gt;, but you still have to &lt;em&gt;hold&lt;/em&gt; ~180B parameters somewhere. This is not a model you run on your laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your mental model was "parameter count = capability = cost," this release is a clean example of why that shortcut breaks. Those three things have come apart, and active params is the thread that connects capability to cost now.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;Every number above comes from &lt;strong&gt;Qwen's own evaluations&lt;/strong&gt;, published at launch and — at the time of writing — not independently reproduced by any outside lab. Treat them as directional claims, not settled facts. Vendor launch benchmarks are run on the vendor's chosen harness against the vendor's chosen baselines; that's not necessarily dishonest, but it's not neutral either. Wait for third-party reproductions before betting anything important on the exact figures.&lt;/p&gt;

&lt;p&gt;A few more things worth keeping straight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is not "Qwen4."&lt;/strong&gt; Qwen positions it as an &lt;em&gt;early preview of the architecture&lt;/em&gt; that Qwen4 will build on — the same role Qwen3-Next played before Qwen3.5. Some aggregators mislabeled it as Qwen4 itself; it isn't. (Its config file even tags the architecture as experimental.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low active ≠ easy to run.&lt;/strong&gt; The FP8 checkpoint is ~172 GB and the BF16 is ~335 GB. You need serious multi-GPU hardware to hold it — the "6B active" cheapness is about &lt;em&gt;throughput and cost per token&lt;/em&gt;, not about fitting it on a workstation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparsity has real tradeoffs.&lt;/strong&gt; Very sparse MoEs carry the memory cost of all those total parameters, and routing can introduce its own instabilities. "6B active" is a genuine efficiency win, not a free lunch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this undercuts the core point — it sharpens it. The achievement is specifically &lt;em&gt;compute-per-token&lt;/em&gt; efficiency, and that's exactly the axis the spec-sheet headline number hides.&lt;/p&gt;




&lt;h2&gt;
  
  
  The number to watch
&lt;/h2&gt;

&lt;p&gt;If there's one habit to take from this: &lt;strong&gt;stop reading total parameter count as the headline, and start reading active parameters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Total tells you how much a model knows and how much memory you need to hold it. Active tells you what it costs and how fast it runs. For years those numbers moved together, so the shortcut worked. They don't move together anymore — MoE and aggressive sparsity have pried them apart, and Qwen3.8-Flash-Next is a vivid example: ~180B on disk, 6B in motion, competitive with models spending far more per token.&lt;/p&gt;

&lt;p&gt;The efficiency race used to be "who has the biggest model." Increasingly it's "who can do the most with the fewest active parameters." That's a quieter number, buried below the headline — and it's the one that's actually going to decide what AI costs to run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Do you check active params before total when picking a model — or has the headline number been doing your thinking for you? And what's the lowest-active model you've gotten real production work out of? Curious where people have found the floor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Are AI Tools Actually Making Us Productive — or Just Giving Us Something New to Play With?</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:36:22 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/are-ai-tools-actually-making-us-productive-or-just-giving-us-something-new-to-play-with-4f9a</link>
      <guid>https://dev.to/james_anderson_h/are-ai-tools-actually-making-us-productive-or-just-giving-us-something-new-to-play-with-4f9a</guid>
      <description>&lt;p&gt;I want to describe a completely ordinary hour from my week, because I suspect it's your week too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scene from my actual workday
&lt;/h2&gt;

&lt;p&gt;I'm building a small feature. I open my AI chat — Claude on this particular day, but it rotates — and ask it to draft the data model. Great answer. I copy it into my editor. I tab back and ask for the API route. Good answer. Copy, paste, tweak the parts that don't fit my setup. Now I need a quick migration script, so I ask again, copy again, switch again.&lt;/p&gt;

&lt;p&gt;Then the feature needs a short doc. Different tab. I paste the AI's summary into the doc, reformat it, fix the parts that assumed things that aren't true. Then someone wants the numbers in a spreadsheet, so I ask the AI to compute them, and I copy the results into the sheet by hand. Then a couple of slides. Another tool. Another paste.&lt;/p&gt;

&lt;p&gt;At some point I looked at my screen and counted the tabs. Fourteen. The AI was living in exactly one of them, narrating instructions to me while I ran around being its hands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it clicked that something was wrong
&lt;/h2&gt;

&lt;p&gt;Here's the thing that nagged at me: none of the individual answers were bad. The AI was &lt;em&gt;smart&lt;/em&gt; the whole time. The friction wasn't intelligence — it was that I had become a courier.&lt;/p&gt;

&lt;p&gt;I was ferrying the AI's output from the one window it lived in, out to a dozen windows it couldn't touch. It could tell me what the spreadsheet formula should be, but it couldn't put it in the spreadsheet. It could write the doc, but it couldn't &lt;em&gt;be&lt;/em&gt; in the doc. Every useful thing it produced had to be carried, by me, across a border it couldn't cross.&lt;/p&gt;

&lt;p&gt;And every crossing cost something. Not just the seconds of copy-paste — the &lt;em&gt;thread&lt;/em&gt;. Each time I left the chat to go do the thing, I dropped the context. When I came back, I had to re-explain where we were. The AI didn't know I'd tweaked its code, didn't know the doc had changed, didn't know what the last three tools had done. It was brilliant and amnesiac and boxed in, all at once.&lt;/p&gt;

&lt;p&gt;That's when the real question landed for me: &lt;strong&gt;is this actually making me productive, or is it just a very impressive new thing to play with?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I was actually missing
&lt;/h2&gt;

&lt;p&gt;It took me a while to name it, but here it is: the AI I use every day is an &lt;em&gt;advisor&lt;/em&gt;, not a &lt;em&gt;workspace&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It's phenomenal at telling me things. It is structurally incapable of doing things where I actually work. The whole interaction model is "ask a smart oracle, receive text, go apply the text yourself." The intelligence is real. The &lt;em&gt;hands&lt;/em&gt; are missing. And once I saw the gap that way, I couldn't unsee it — because so much of my day is spent being the bridge between an AI that knows and a set of tools it can't reach.&lt;/p&gt;

&lt;p&gt;We've spent three years making the oracle smarter. We've spent almost none making it able to &lt;em&gt;act&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden costs I hadn't noticed I was paying
&lt;/h2&gt;

&lt;p&gt;Once I started paying attention, the taxes were everywhere:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context loss.&lt;/strong&gt; Every tool switch is a little amnesia event. The chat doesn't follow me into the editor, the doc, or the sheet, so the shared understanding I built up in the conversation evaporates the moment I leave it — and I rebuild it, from scratch, on the way back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fragmentation.&lt;/strong&gt; My actual work — the code, the doc, the data, the slides — ends up scattered across a dozen apps, none of which know about each other, and &lt;em&gt;none&lt;/em&gt; of which the AI can see once I've moved things there. The "project" exists only in my head, as the thing holding all those tabs together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A blind assistant.&lt;/strong&gt; This is the one that finally got me. The instant I act on the AI's advice, the AI goes blind. It can't watch me apply its suggestion, can't see the result, can't catch that I changed one line and broke the thing three files over. It gave me a map and then closed its eyes while I walked.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They're the quiet, everyday texture of using AI tools in 2026 — and I'd stopped noticing them the way you stop noticing a sound that's always on.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But wait — don't Claude, GPT, and Gemini already do this?"
&lt;/h2&gt;

&lt;p&gt;I want to be fair, because I asked myself exactly this before I let myself complain.&lt;/p&gt;

&lt;p&gt;They've all added surfaces. ChatGPT has Canvas (now folded into inline writing and code blocks); Claude has Artifacts; Gemini has Canvas too. These are genuinely nice — instead of dumping a wall of text in the chat, the AI opens a side panel you can iterate on, and Claude's artifacts will even &lt;em&gt;run&lt;/em&gt; an interactive app right there. The industry converged on this idea over the last couple of years, and for good reason.&lt;/p&gt;

&lt;p&gt;But sit with what these actually are, and the gap becomes clear. Each is &lt;strong&gt;one surface&lt;/strong&gt;, usually &lt;strong&gt;one at a time&lt;/strong&gt;, mostly for &lt;strong&gt;writing or code&lt;/strong&gt;, and largely walled off from everything else. Canvas is a nice document editor. Artifacts is a nice code-and-preview pane. They're separate modes &lt;em&gt;you&lt;/em&gt; trigger, siloed from one another, and when you're done the dominant verb is still &lt;em&gt;export&lt;/em&gt;: send it to Google Docs, copy the source out, download the HTML, paste it into your real environment. The side panel made the copy-paste shorter. It didn't remove it.&lt;/p&gt;

&lt;p&gt;What none of them is, is a &lt;strong&gt;single adaptive workspace&lt;/strong&gt; where the &lt;em&gt;right&lt;/em&gt; surface — editor, document, spreadsheet, slides, image canvas, PDF — shows up on its own for whatever you're doing right now, all in one continuous context, with the AI able to act &lt;em&gt;inside&lt;/em&gt; each of them rather than hand you a result to carry away. A bolted-on canvas is not that. It's a better waiting room, not a place to actually do all the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I started wishing existed
&lt;/h2&gt;

&lt;p&gt;So I did the thing you do when a frustration finally has a name: I started imagining the tool I actually wanted.&lt;/p&gt;

&lt;p&gt;I wanted one conversation that didn't end at the edge of the chat box. I wanted to ask for code and have an &lt;em&gt;editor&lt;/em&gt; appear — not a preview I copy out, an editor I work in. I wanted to draft a doc and have the &lt;em&gt;document&lt;/em&gt; be right there, part of the same session, not a separate app I paste into. I wanted the numbers to land in a &lt;em&gt;spreadsheet&lt;/em&gt; the AI could actually fill, the slides to build in a &lt;em&gt;deck&lt;/em&gt; I could refine by talking, the image to open on a &lt;em&gt;canvas&lt;/em&gt; — each surface summoning itself based on what the task needed, none of them requiring me to go find a tool or carry anything across a border.&lt;/p&gt;

&lt;p&gt;In other words: I didn't want a smarter oracle. I wanted a &lt;strong&gt;workspace&lt;/strong&gt; — one place where thinking and doing happen together, where the AI has hands, and where switching tasks doesn't mean switching apps and losing the plot.&lt;/p&gt;

&lt;p&gt;That's the concept I keep coming back to: a &lt;em&gt;multimodal AI workspace&lt;/em&gt;. One chat, many surfaces, summoned automatically, all sharing the same context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this felt like the obvious next step
&lt;/h2&gt;

&lt;p&gt;The more I thought about it, the less it felt like a wishlist and the more it felt inevitable.&lt;/p&gt;

&lt;p&gt;There's a principle a lot of us already believe about building good AI systems: use the model for what it's good at — understanding intent, reasoning about the task — and route the actual work to the tool that's actually good at that work. Model reasons; the right tool does the job. I've argued this about calculators and code execution before: you don't make the language model &lt;em&gt;be&lt;/em&gt; the spreadsheet, you let it &lt;em&gt;drive&lt;/em&gt; the spreadsheet.&lt;/p&gt;

&lt;p&gt;A multimodal workspace is just that same principle raised to the level of the whole product. Instead of one chat pretending to be everything through a straw of text, the system reads what you're trying to do and opens the surface built for it — and keeps them all in one shared context so nothing gets lost between them. Once you see it that way, the current model — brilliant chat, everything else left to you — starts to look like an obvious transitional stage, not the destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I came across Xenition
&lt;/h2&gt;

&lt;p&gt;While I was chewing on all this, I recently came across a tool built around exactly this idea, called &lt;a href="https://xenition.com/" rel="noopener noreferrer"&gt;Xenition&lt;/a&gt; — and I'll be upfront that it's the thing that turned this from a vague wish into "oh, someone's actually doing this."&lt;/p&gt;

&lt;p&gt;The premise is the one I'd been describing to myself: one persistent chat where the right surface opens itself — a code editor, a document, a slide deck, a spreadsheet, an image canvas, a PDF reader — based on what you're actually doing, so you're not picking a "mode" or hopping between apps. On top of that it leans into the &lt;em&gt;acting&lt;/em&gt; part I kept missing: agents that don't just answer but take multi-step actions, with the kind of guardrails that matter once an AI can actually do things (approval gates, an audit trail, even a second agent that reviews the first one's work before it reaches you).&lt;/p&gt;

&lt;p&gt;I'm not going to pretend I've stress-tested every corner of it, and this isn't a review. But as a concrete answer to the frustration I opened this post with — the courier problem, the fourteen tabs, the blind assistant — it's the closest thing I've found to the workspace I actually wanted, and it made the whole idea feel a lot less hypothetical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I think this is heading
&lt;/h2&gt;

&lt;p&gt;Here's my honest prediction. The next real leap in AI tools won't be a smarter chat. We're already deep into diminishing returns on "the oracle knows more." The leap will be &lt;strong&gt;collapsing thinking and doing into one place&lt;/strong&gt; — so the intelligence we already have can actually reach the work.&lt;/p&gt;

&lt;p&gt;Chat was the demo. It was the thing that proved these models were astonishing. But a demo that makes you copy its output into fourteen other apps isn't a finished product; it's a preview of one. The tools that win the next round won't be the ones that answer best. They'll be the ones where the answer and the doing happen in the same breath.&lt;/p&gt;

&lt;p&gt;So, back to the question in the title: are today's AI tools making us productive, or just giving us something new to play with? My honest answer is &lt;em&gt;both, and that's the problem.&lt;/em&gt; They're genuinely useful and genuinely a toy, at the same time — and the reason it still feels more like play than leverage is that the intelligence is trapped in a chat box while all our actual work happens somewhere it can't go. Close that gap, and "play" quietly turns into "productive."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the tool-switch that breaks your flow the most? The one copy-paste hop you make ten times a day and quietly resent? I'd love to hear where the courier problem hits you hardest — drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Retrieval Checklist I Wish I'd Had Before Shipping RAG</title>
      <dc:creator>James Anderson</dc:creator>
      <pubDate>Tue, 25 Aug 2026 04:02:30 +0000</pubDate>
      <link>https://dev.to/james_anderson_h/the-retrieval-checklist-i-wish-id-had-before-shipping-rag-2j5a</link>
      <guid>https://dev.to/james_anderson_h/the-retrieval-checklist-i-wish-id-had-before-shipping-rag-2j5a</guid>
      <description>&lt;p&gt;The first time my RAG system gave a confidently wrong answer, I did what everyone does: I blamed the model. I swapped in a bigger one. I tuned the prompt. I added "only answer from the context provided" in bold. The answer got no better.&lt;/p&gt;

&lt;p&gt;The problem was never the model. The model was faithfully summarizing the context it was handed — the context was just &lt;em&gt;wrong&lt;/em&gt;. It had retrieved the wrong chunks, so it answered the wrong question, fluently.&lt;/p&gt;

&lt;p&gt;This turns out to be the norm, not the exception. Industry analysis in 2026 keeps landing on the same number: when RAG fails, the failure is in &lt;strong&gt;retrieval roughly 73% of the time, not generation.&lt;/strong&gt; The LLM gets blamed for a mistake that happened several steps upstream, before it ever saw a token.&lt;/p&gt;

&lt;p&gt;So here's the checklist I wish someone had handed me before I shipped — organized as a walk through the whole pipeline, because retrieval isn't one step, it's a chain, and it can break at any link. Naive RAG ("chunk, embed, cosine similarity, stuff into prompt") was always a prototype. This is the gap between that and production.&lt;/p&gt;

&lt;p&gt;Let's go link by link.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, the mental model: two paths, not one pipeline
&lt;/h2&gt;

&lt;p&gt;The mistake underneath a lot of RAG pain is treating RAG as a single flow. It's actually &lt;strong&gt;two separate paths&lt;/strong&gt; that most people accidentally couple together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The indexing path (offline).&lt;/strong&gt; Runs when documents are added or changed: parse the source → clean the text → chunk → (optionally) enrich each chunk with context → embed → write to the vector store &lt;em&gt;and&lt;/em&gt; a keyword index. This can take minutes per document and runs in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The query path (online).&lt;/strong&gt; Runs on every user request, in real time, under a latency budget (aim for under ~3 seconds end to end): take the query → optionally rewrite it → retrieve candidates → rerank → assemble the prompt with citations → generate → log the trace.&lt;/p&gt;

&lt;p&gt;The most common &lt;em&gt;architectural&lt;/em&gt; mistake is coupling these. If re-indexing forces the query path offline, you can't iterate on chunking or swap embedding models without downtime — so you stop iterating, and a frozen pipeline is a stale pipeline. Keep them independent from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Can you re-chunk and re-embed your whole corpus without taking live search down? If not, decouple the paths before anything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 1. Is your chunking splitting ideas in half?
&lt;/h2&gt;

&lt;p&gt;Chunking is where pipelines silently fail, because bad chunks don't throw errors — they just quietly return technically-relevant, practically-useless context.&lt;/p&gt;

&lt;p&gt;The naive default — "split every 1,000 characters with 100 overlap" — is a fast start and a slow ceiling. Fixed-size splitting cuts sentences mid-thought, tables mid-row, and code mid-function. The retrieved chunk &lt;em&gt;looks&lt;/em&gt; relevant and is missing the half that mattered.&lt;/p&gt;

&lt;p&gt;Better options, roughly in order of effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structure-aware splitting&lt;/strong&gt; — split on the document's own boundaries: &lt;code&gt;##&lt;/code&gt; headings for docs, per-function or per-class for code, per-row for tables. Low effort, big payoff, respects how the content is actually organized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic chunking&lt;/strong&gt; — compute similarity sentence-to-sentence and start a new chunk where the meaning shifts, so each chunk is one complete thought. More compute, but a published comparison reported it lifting accuracy meaningfully over fixed-size on the same dataset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule to hold onto: &lt;strong&gt;each chunk should be able to answer a question on its own.&lt;/strong&gt; If a chunk only makes sense next to its neighbor, your splitting is too aggressive. Also mind chunk &lt;em&gt;size&lt;/em&gt; — too small and you fragment ideas; too large and you dilute the signal, forcing the model to average across a wall of mostly-irrelevant text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Pull ten random chunks and read them cold. Does each stand on its own, or are half of them sentence fragments and orphaned table rows?&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 2. Are you embedding the chunk — or the chunk &lt;em&gt;in context&lt;/em&gt;?
&lt;/h2&gt;

&lt;p&gt;A subtle, high-impact one. If you embed only the raw body text of a chunk, you strip away the context that told a human what it meant — which section it's under, which product it's about, what came before it.&lt;/p&gt;

&lt;p&gt;Two fixes, both cheap relative to their payoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embed context, not just body.&lt;/strong&gt; Prepend the heading, a short document summary, or a one-line description of what the chunk is about before embedding. This aligns the chunk's vector with how people actually phrase questions. (This is the core idea behind "contextual retrieval" — giving each chunk a little situating context before it's indexed measurably improves recall.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep metadata attached.&lt;/strong&gt; Every document arrives with structure — author, date, source, section, product version, document type, access level. Store it alongside the chunk. You'll use it in the next step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Does an isolated chunk in your index carry any signal about &lt;em&gt;where it came from&lt;/em&gt;, or is it a naked paragraph with no situating context?&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 3. Are you using hybrid search — or just vector search?
&lt;/h2&gt;

&lt;p&gt;This is the single most common retrieval mistake, and it hides in plain sight because vector search &lt;em&gt;usually&lt;/em&gt; works.&lt;/p&gt;

&lt;p&gt;Pure vector (semantic) search is great at meaning. Ask "how do I fix login problems?" and it'll surface chunks about authentication, OAuth, and session timeouts even if none use the word "login." That's the magic.&lt;/p&gt;

&lt;p&gt;But it falls on its face the moment a query contains something &lt;em&gt;exact&lt;/em&gt;. A user searches for the error code &lt;code&gt;ERR_SSL_PROTOCOL_ERROR&lt;/code&gt;, a SKU like &lt;code&gt;WX-4200&lt;/code&gt;, or a specific function name — and vector search has no idea what to do, because semantic similarity is meaningless for a serial number. It returns things "sort of about errors" and misses the exact match sitting right there in your corpus.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;hybrid search&lt;/strong&gt;: run keyword search (BM25/full-text) &lt;em&gt;and&lt;/em&gt; vector search on the same query, then fuse the results — &lt;strong&gt;Reciprocal Rank Fusion (RRF)&lt;/strong&gt; is the standard merge. Keyword catches exact matches; vector catches meaning. The consensus across 2024–2026 benchmarks (BEIR, MTEB, and others) is blunt: &lt;strong&gt;BM25 + dense embeddings fused with RRF beats either one alone&lt;/strong&gt;, on basically every public benchmark. Dense-only retrieval lost that argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Does your retrieval handle a literal error code &lt;em&gt;and&lt;/em&gt; a vague conceptual question equally well? If not, you're probably vector-only, and adding a keyword index is your highest-leverage change.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 4. Are you transforming the query — or retrieving the user's raw words?
&lt;/h2&gt;

&lt;p&gt;Here's a link most people skip entirely: the user's literal question is often &lt;em&gt;not&lt;/em&gt; what the retriever wants. People ask vague, compound, context-dependent questions; your index holds precise, standalone statements. Bridging that gap is &lt;strong&gt;query transformation&lt;/strong&gt;, and it's one of the biggest quiet wins available.&lt;/p&gt;

&lt;p&gt;The main patterns, each solving a different problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query rewriting / expansion&lt;/strong&gt; — clean up and enrich the raw query before retrieval so it matches the corpus better. Especially important in multi-turn chat, where "what about the second one?" is meaningless without rewriting it into a standalone query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HyDE (Hypothetical Document Embeddings)&lt;/strong&gt; — have the LLM generate a &lt;em&gt;hypothetical answer&lt;/em&gt; to the question, then embed &lt;em&gt;that&lt;/em&gt; and retrieve against it. A fake answer is often shaped much more like the real documents than the question is, which boosts precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step-back prompting&lt;/strong&gt; — rewrite a narrow question into a more general one, retrieve the background, then specialize. Good for ambiguous queries where the literal phrasing misses the corpus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decomposition&lt;/strong&gt; — split a multi-part question into independent sub-queries, retrieve each separately, then synthesize. "How does our refund policy differ between B2B and B2C, and what are the exceptions?" is really three retrievals, not one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need all of these. But if you're feeding raw user text straight into the retriever, you're leaving a lot of recall on the table — especially for compound and conversational questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Take your ten hardest real user questions. How many would retrieve better if they were rephrased, split, or expanded first? If it's most of them, add a transformation step.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 5. Are you reranking — or trusting first-pass retrieval order?
&lt;/h2&gt;

&lt;p&gt;The mistake that cost me the most quality for the least obvious reason: I assumed that if the right chunk was retrieved, the model would use it. But &lt;em&gt;where&lt;/em&gt; it lands in the list matters enormously.&lt;/p&gt;

&lt;p&gt;Vector search uses a &lt;strong&gt;bi-encoder&lt;/strong&gt; — it encodes the query and each chunk &lt;em&gt;separately&lt;/em&gt; and compares vectors. Fast, but it trades away fine-grained relevance. So the genuinely best chunk often gets retrieved... at position 8, buried under seven "pretty relevant" ones. And models demonstrably ignore information stranded in the middle of a long list — the &lt;strong&gt;"lost in the middle"&lt;/strong&gt; problem. The right answer is &lt;em&gt;in the context&lt;/em&gt; and the model still misses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reranking&lt;/strong&gt; fixes this. Retrieve a broad candidate set with hybrid search (top 20–50), then run a &lt;strong&gt;cross-encoder reranker&lt;/strong&gt; that scores each &lt;code&gt;(query, chunk)&lt;/code&gt; pair &lt;em&gt;jointly&lt;/em&gt; — seeing query and chunk together, which makes it far better at fine-grained relevance than first-pass retrieval. Keep the top 3–8.&lt;/p&gt;

&lt;p&gt;The impact is large, not marginal: a cross-encoder reranker commonly adds &lt;strong&gt;5–15 points of MRR&lt;/strong&gt; on hard sets, and on some reasoning-heavy benchmarks reranking pushed nDCG@10 from ~0.13 to ~0.40 — roughly &lt;strong&gt;3x, just from reordering the same candidates you already retrieved.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The recipe that beats ~80% of production deployments: &lt;strong&gt;retrieve ~20 via hybrid search → rerank to ~5 → send 3–5 to the LLM.&lt;/strong&gt; Reranking 100+ candidates rarely pays; the signal lives at the head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Is there a reranking step between retrieval and the prompt? If chunks go straight from vector similarity into the context window, add one — it's often the highest-ROI change in the whole pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 6. Is your context assembly helping the model — or dumping on it?
&lt;/h2&gt;

&lt;p&gt;You've retrieved and reranked the right chunks. You can still lose here, at the stage nobody talks about: how you actually &lt;em&gt;assemble&lt;/em&gt; the prompt.&lt;/p&gt;

&lt;p&gt;Things that quietly hurt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order.&lt;/strong&gt; Because of "lost in the middle," put the strongest chunks at the very start and end of the context, not buried in the center.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume.&lt;/strong&gt; More chunks is not better. Stuffing 30 chunks in "to be safe" dilutes the signal and invites the model to average across noise. Send the few that earned their place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No citations.&lt;/strong&gt; Ask the model to cite which chunk supports each claim. This both discourages free-floating fabrication and gives you a way to verify the answer against its sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-context ≠ skip retrieval.&lt;/strong&gt; Frontier models have million-token windows now, and the reflex is "just dump everything in, who needs retrieval." Resist it. Dumping the whole corpus is slower, more expensive, and &lt;em&gt;less&lt;/em&gt; accurate than sending a few well-chosen chunks, because the model still has to find the needle. Use the big window for genuine synthesis (long reports, whole codebases), not as a substitute for retrieval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; How many chunks do you send, and in what order? If the answer is "as many as fit, in retrieval order," you're leaving quality on the floor.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 7. Do you need agentic RAG — or are you bolting complexity onto a broken pipeline?
&lt;/h2&gt;

&lt;p&gt;Everything above describes a single retrieve-then-generate pass. That has a ceiling: it works on simple questions and falls apart on nuanced, multi-hop ones where the answer isn't in any single chunk. Enter &lt;strong&gt;agentic RAG&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The shift is structural. Classic RAG does &lt;em&gt;one&lt;/em&gt; retrieval call, up front, stateless — if it misses, there's no recovery. Agentic RAG moves retrieval &lt;em&gt;inside&lt;/em&gt; a reasoning loop (the ReAct pattern: the model alternates between thinking and calling tools). Now the model can retrieve, look at what it got, decide it's not enough, rewrite its query, retrieve again, and stop when it has what it needs. Retrieval becomes a tool the agent uses repeatedly, not a fixed step in front of it.&lt;/p&gt;

&lt;p&gt;This unlocks &lt;strong&gt;multi-hop&lt;/strong&gt; questions — "what's the GDP of the country that hosted the 2024 Olympics?" needs hop one (Olympics → France) before hop two (France → GDP). A single retrieval can't do that; an agent that retrieves, reasons, and retrieves again can. Related advanced patterns include &lt;strong&gt;GraphRAG&lt;/strong&gt; (build a knowledge graph from your docs to answer questions that require connecting entities across many documents) and giving the agent explicit tools: search, fetch-full-document-by-id, exact-match/regex, and even prune-context-to-discard-junk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest caveat&lt;/strong&gt; — and this ties straight back to over-engineering: agentic RAG costs more (more calls, more latency, more nondeterminism) and is worth it for genuinely complex or high-stakes retrieval (legal, medical, financial, multi-hop). It is &lt;em&gt;not&lt;/em&gt; a fix for a broken basic pipeline. If your chunking is bad and you have no reranking, an agent will just make bad retrieval calls, repeatedly, more expensively. &lt;strong&gt;Get single-pass retrieval solid first. Add the agent loop only when the questions genuinely need multiple hops.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; Do your failing questions actually require chaining facts across documents — or would they be answered fine by hybrid search + reranking you haven't implemented yet?&lt;/p&gt;




&lt;h2&gt;
  
  
  ☐ 8. Can you measure retrieval in isolation — and catch a fabrication?
&lt;/h2&gt;

&lt;p&gt;This is the meta-mistake that hides all the others. Most teams evaluate RAG end-to-end: read the final answer, decide it "seems good," ship. But an end-to-end answer blends retrieval and generation, so when it's wrong you can't tell &lt;em&gt;which half&lt;/em&gt; failed. You'll spend a week tuning prompts to fix a chunking bug.&lt;/p&gt;

&lt;p&gt;Two things you need to measure separately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval quality on its own.&lt;/strong&gt; Given a query, did the right chunk make it into the retrieved set (&lt;strong&gt;recall&lt;/strong&gt;), and how high did it rank (&lt;strong&gt;rank / nDCG / MRR&lt;/strong&gt;)? For multi-hop and agentic setups, recall has to be measured &lt;em&gt;across the whole chain&lt;/em&gt;, not one call. Frameworks like RAGAS exist, but even a hand-built set of real queries mapped to their correct source chunks beats vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faithfulness / groundedness of the answer.&lt;/strong&gt; Is every claim in the final answer actually supported by the retrieved context? This is the check that catches the scariest failure: the agent that retrieves 8 chunks, uses 6, and invents the 7th fact entirely. Without a faithfulness score or a judge gating the output, that fabrication ships and a customer finds it two days later.&lt;/p&gt;

&lt;p&gt;One hard-won caution: a &lt;em&gt;tiny&lt;/em&gt; eval set will lie to you. If your test set is small and easy, every method scores near-perfect and they all look equally good — the differences that matter on real data are invisible on a toy set. &lt;strong&gt;A retrieval eval is only an eval if methods can actually fail on it.&lt;/strong&gt; If everything scores 95%, you've built a smoke test, and a smoke test will happily bless the broken layer you were hoping to justify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; If retrieval regressed tomorrow, would a &lt;em&gt;number&lt;/em&gt; tell you — or would a user? If it's the user, you can't measure retrieval yet, and everything above this line is guesswork.&lt;/p&gt;




&lt;h2&gt;
  
  
  The whole checklist, in one screen
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decouple&lt;/strong&gt; the indexing path from the query path so you can iterate without downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk&lt;/strong&gt; so each piece stands alone — structure-aware or semantic, never blind fixed-size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed in context&lt;/strong&gt; — prepend headings/summaries, keep metadata attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid search&lt;/strong&gt; — BM25 + vector, fused with RRF. Never vector-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transform the query&lt;/strong&gt; — rewrite, HyDE, step-back, or decompose before retrieving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rerank&lt;/strong&gt; with a cross-encoder — retrieve ~20, rerank to ~5, send 3–5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assemble context&lt;/strong&gt; deliberately — best chunks first and last, few not many, with citations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go agentic only when needed&lt;/strong&gt; — multi-hop and high-stakes, on top of a solid base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate both&lt;/strong&gt; retrieval recall &lt;em&gt;and&lt;/em&gt; answer faithfulness — on a set hard enough to fail.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The One Line to Remember
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;When RAG gives a bad answer, suspect retrieval first — it's the culprit far more often than the model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The instinct to reach for a bigger model is almost always wrong. The bigger model will summarize the wrong chunks just as fluently as the small one did. The leverage is upstream — in &lt;em&gt;finding&lt;/em&gt; the right chunk, making it &lt;em&gt;usable&lt;/em&gt;, ranking it where the model will &lt;em&gt;see&lt;/em&gt; it, and being able to &lt;em&gt;tell&lt;/em&gt; when any link in the chain breaks.&lt;/p&gt;

&lt;p&gt;I learned this checklist one confidently-wrong answer at a time. You don't have to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the retrieval bug that fooled you the longest? Mine was a chunking issue I spent two weeks blaming the model for. Share yours — and any checklist items I missed — in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
