<?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: Ken Imoto</title>
    <description>The latest articles on DEV Community by Ken Imoto (@kenimo49).</description>
    <link>https://dev.to/kenimo49</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%2F3800250%2F275022f6-cba9-47e3-b69e-e8faf7675a0c.jpg</url>
      <title>DEV Community: Ken Imoto</title>
      <link>https://dev.to/kenimo49</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenimo49"/>
    <language>en</language>
    <item>
      <title>Claude Code vs Cursor vs Codex: 31 Days of Real Receipts, and the Cheapest Isn't Who You Think</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/claude-code-vs-cursor-vs-codex-31-days-of-real-receipts-and-the-cheapest-isnt-who-you-think-1o6b</link>
      <guid>https://dev.to/kenimo49/claude-code-vs-cursor-vs-codex-31-days-of-real-receipts-and-the-cheapest-isnt-who-you-think-1o6b</guid>
      <description>&lt;p&gt;Every "which coding agent is best" post I read ranks accuracy, plugin support, and vibes. Almost none of them show a monthly bill. This is the post I wanted to read before I picked one.&lt;/p&gt;

&lt;p&gt;For the 31 days of June 2026 I ran all three official coding agents on the same laptop: &lt;strong&gt;Claude Code&lt;/strong&gt; (Anthropic), &lt;strong&gt;Codex&lt;/strong&gt; (OpenAI, inside ChatGPT), and &lt;strong&gt;Cursor&lt;/strong&gt;. Same repos, same tasks, same me. I kept receipts. I logged sessions. I ran a local Qwen 3.5 35B on an RTX 4070 in parallel to see where owning silicon paid back.&lt;/p&gt;

&lt;p&gt;Short answer: one of them is cheapest for me right now, but not by the margin the marketing pages suggest, and if my usage shape changes even a little the ranking flips. That is the real headline. &lt;strong&gt;The bill depends on what you do more than on which logo you pick.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "one month" was, in detail
&lt;/h2&gt;

&lt;p&gt;The month I logged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;22 working days at the keyboard.&lt;/li&gt;
&lt;li&gt;Roughly 6 hours a day inside an agent, mixed reading + writing.&lt;/li&gt;
&lt;li&gt;Two projects: one TypeScript SaaS with ~180 files, one Python data pipeline with ~60.&lt;/li&gt;
&lt;li&gt;Three "hard" refactors (multi-file, week-scale). The rest was normal feature and bugfix work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usage shape matters more than the number on the pricing page. A part-time hobbyist and a full-time engineer buying the same $200 tier are buying two different products at the same price. The line I care about is not $/month, it is &lt;strong&gt;$/hour-of-agent-time&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three cost shapes
&lt;/h2&gt;

&lt;p&gt;Every coding agent bills you as one of three shapes, and June 2026 has not changed that.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subscription with usage multiplier.&lt;/strong&gt; Flat fee, soft cap on requests, throttle when you're over. Claude Code Pro / Max, ChatGPT Plus / Pro, Cursor Pro / Ultra all fit here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metered API.&lt;/strong&gt; Per token. No monthly ceiling unless you set one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local GPU.&lt;/strong&gt; You bought the card. Electricity + amortization. Zero variable cost per token but a fixed capacity ceiling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The break-even between these three is what most posts skip. My June numbers, in one table:&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code: $200/month Max, and I hit the throttle twice
&lt;/h2&gt;

&lt;p&gt;I ran Claude Code on the Max 20x plan at &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;$200/month, per Anthropic's current pricing page&lt;/a&gt;. The Pro tier at $20/month exists but is calibrated for a few focused sessions a day, not an agent-driven workflow.&lt;/p&gt;

&lt;p&gt;What the month bought me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code was my primary tool for the two hard refactors. Long-context is where it earns the bill.&lt;/li&gt;
&lt;li&gt;I hit the 5-hour rolling limit twice on Max, both times during a deep multi-file refactor where I was running Sonnet 4.6 in the background across 3 subagents. That is Anthropic's cost tell: subagents multiply token spend, and the Max tier feels the cap when you fan out.&lt;/li&gt;
&lt;li&gt;Everything else fit comfortably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I had run the same month on the metered API at &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;Sonnet 4.6 pricing of $3/M input and $15/M output&lt;/a&gt; (the introductory $2/$10 promo runs through August 31, 2026, so I'm quoting the standard rate), the honest estimate for my token volume landed somewhere in the $260-$380 range. The subscription won by ~$60-$180, at the cost of a rate limit I can predict but not remove.&lt;/p&gt;

&lt;p&gt;Specific lesson: &lt;strong&gt;for a heavy user, Max 20x beats the metered API only until you fan out subagents.&lt;/strong&gt; Anthropic's own engineering blog notes that multi-agent runs use roughly &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;15x the tokens of a single-agent chat&lt;/a&gt;. That's real. If I ran three subagents on Sonnet as my normal shape, the metered path would probably beat Max.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codex: bundled inside ChatGPT Pro at $100, and the metering just changed
&lt;/h2&gt;

&lt;p&gt;Codex is the odd one. It does not have a standalone subscription. It rides inside your ChatGPT plan.&lt;/p&gt;

&lt;p&gt;I ran ChatGPT Pro at &lt;a href="https://chatgpt.com/pricing/" rel="noopener noreferrer"&gt;$100/month&lt;/a&gt;, the tier OpenAI &lt;a href="https://techcrunch.com/2026/04/09/chatgpt-pro-plan-100-month-codex/" rel="noopener noreferrer"&gt;added in April 2026 to sit between the $20 Plus and the $200 Pro-20x&lt;/a&gt;. Two things the marketing page does not emphasize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On April 2, 2026, &lt;a href="https://developers.openai.com/codex/pricing" rel="noopener noreferrer"&gt;Codex pricing moved from per-message to API-token-equivalent metering&lt;/a&gt;. If you were used to the old plan, your "same amount of work" started drawing down credits at a different rate. Nobody's monthly bill stayed the same, they just moved.&lt;/li&gt;
&lt;li&gt;The average Codex developer sits at roughly $100-$200/month across all instances they're running. That is the same number I hit. Bundling with ChatGPT means I paid one bill, but the token math is not hidden. It is just billed as one line item.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Where Codex earned the seat for me: quick front-end scaffolding, one-shot script generation, "explain this stack trace" flows where I want an answer inside a browser tab I already have open. Where it did not: long-lived agent sessions in a real repo. That is where Claude Code held the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursor: $60/month Pro+, and I stopped forcing frontier models
&lt;/h2&gt;

&lt;p&gt;Cursor's &lt;a href="https://www.cursor.com/pricing" rel="noopener noreferrer"&gt;June 2025 credit model change&lt;/a&gt; means the plan you buy is really a credit pool. Hobby is $0, Pro is $20, &lt;a href="https://cursor.com/pricing" rel="noopener noreferrer"&gt;Pro+ is $60, Ultra is $200&lt;/a&gt;. I paid for Pro+ this month.&lt;/p&gt;

&lt;p&gt;The move Cursor pushed on me was Auto mode. On any paid plan Auto is unlimited: it picks a model for you and does not touch your credits. If you leave Auto on for tab completion and short edits and only reach for Claude Sonnet or GPT-4o for the actual reasoning work, the credit pool lasts.&lt;/p&gt;

&lt;p&gt;I did the opposite for the first two weeks and paid for it. I forced Claude Sonnet everywhere, my credits drained by day 18, and I bought a top-up. The last two weeks I flipped the pattern: Auto for autocomplete, frontier models only when I explicitly asked. Same Pro+ pool covered the rest of the month with room to spare.&lt;/p&gt;

&lt;p&gt;The Cursor-specific insight nobody puts on the pricing page: &lt;strong&gt;Pro at $20 is fine if you use Auto for most things. Pro+ at $60 is where you land if you cannot help yourself and keep clicking Sonnet.&lt;/strong&gt; Ultra at $200 is for someone running agent mode all day; that was not me this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The local GPU: RTX 4070 + Qwen 3.5 35B, the breakeven that is not what you think
&lt;/h2&gt;

&lt;p&gt;I keep an RTX 4070 running Qwen 3.5 35B locally, and I ran it as a fourth "agent" in parallel to see which jobs made sense to send there.&lt;/p&gt;

&lt;p&gt;The naive breakeven math is easy and misleading. A $600 card amortized over 24 months is $25/month. Electricity at moderate use is another $10-$15. On paper: $35-$40/month for unlimited tokens.&lt;/p&gt;

&lt;p&gt;Here is what the naive math misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 4070 cannot run the frontier models. It runs the small ones, competently but at ~40% of the quality of Sonnet 4.6 on the tasks I care about. For me, that eliminated it from primary agent duty.&lt;/li&gt;
&lt;li&gt;Where it did earn its slot: &lt;strong&gt;bulk classification, refactor precheck, and offline batch jobs.&lt;/strong&gt; I ran a 3,000-file "which files touch payment logic" scan on Qwen overnight; the same job on Claude API would have been $8-$12. Twenty of those a month is where the card breaks even.&lt;/li&gt;
&lt;li&gt;Zero of my long-context refactor sessions belonged on local. The context window on Qwen 3.5 35B is not comparable to Sonnet 4.6.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local is not a subscription replacement. It is a batch-workload absorber. Buying a 4070 to save money on your primary agent is buying the wrong tool for the wrong job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 31-day scoreboard
&lt;/h2&gt;

&lt;p&gt;Here is what landed on the credit card and what I got for it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5efgnmayw2zcw2q9h80n.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%2F5efgnmayw2zcw2q9h80n.png" alt="31-day scoreboard: Claude Code $200, Codex $100, Cursor $60, local Qwen on RTX 4070 ~$35, where each earned its seat" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly cost&lt;/th&gt;
&lt;th&gt;Where it earned it&lt;/th&gt;
&lt;th&gt;Where it did not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Max 20x&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;Long refactors, subagent runs&lt;/td&gt;
&lt;td&gt;Rate-limited twice on fan-out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;ChatGPT Pro $100&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;Quick scaffolding, browser-tab flow&lt;/td&gt;
&lt;td&gt;Multi-file agent sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;Pro+&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;Autocomplete + Auto mode&lt;/td&gt;
&lt;td&gt;Credit drain if you force Sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local (Qwen 3.5 35B)&lt;/td&gt;
&lt;td&gt;RTX 4070 amortized&lt;/td&gt;
&lt;td&gt;~$35&lt;/td&gt;
&lt;td&gt;Bulk scans, batch classification&lt;/td&gt;
&lt;td&gt;Long context, frontier quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total this month: &lt;strong&gt;$395&lt;/strong&gt;. Yes, all four. The overlap is not wasted; each tool covered a workload the others charged 3-5x more to do.&lt;/p&gt;

&lt;p&gt;Now the honest bit. If I had to pick one and drop the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If my month were 90% multi-file agent refactors and I could tolerate a rate limit, I would keep Claude Code Max 20x and drop the other three. Roughly $200/month.&lt;/li&gt;
&lt;li&gt;If my month were 90% "one file at a time, browser tab open," I would keep ChatGPT Pro at $100 and use its Codex allocation. Roughly $100/month.&lt;/li&gt;
&lt;li&gt;If my month were 90% autocomplete-first coding with a couple of hard problems a week, I would keep Cursor Pro at $20 with Auto default and pay-as-I-go for Sonnet. Roughly $20-$40/month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason I keep all three is that none of my months look like "90% one shape." Yours probably do not either. But if you have to pick, pick against your dominant workload, not against the benchmark you saw on Twitter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I will drop for July
&lt;/h2&gt;

&lt;p&gt;Two changes I'm making for the July run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cap the Claude Code subagent fanout at two.&lt;/strong&gt; I hit the throttle twice this month, both times because I told it to run three subagents in parallel. Anthropic's own doc &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;reports 15x token spend on multi-agent runs&lt;/a&gt;; I don't need three, two solves most of the problem, and I stay under the rate limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop reaching for Sonnet in Cursor.&lt;/strong&gt; Pro at $20 with Auto default was clearly the right tier for me. The $40/month I saved on Cursor pays for something more useful than a habit.&lt;/p&gt;

&lt;p&gt;If I write this post again at the end of July I'll compare the two months side by side. The Twitter version of "which agent is cheapest?" is the wrong question. The version worth answering: &lt;strong&gt;what is my usage shape, and which of these four bills matches it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That answer changed for me twice this month. It will probably change again. But I'll have receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The book I extracted the receipt-first playbook from
&lt;/h2&gt;

&lt;p&gt;If you liked the receipt-first framing, I turned the general "estimate cost before shipping the prototype" mindset into a full playbook in &lt;a href="https://kenimoto.dev/books/claude-code-mastery?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cc-cursor-codex-31d" rel="noopener noreferrer"&gt;Claude Code Mastery&lt;/a&gt;. It covers the subagent fanout math, when Skills earn their token cost, and how to keep a heavy month from turning into a surprise. Same posture as this post: real numbers, then decisions.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Claude Code / Anthropic API pricing: &lt;a href="https://claude.com/pricing" rel="noopener noreferrer"&gt;claude.com/pricing&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;ChatGPT / Codex pricing: &lt;a href="https://chatgpt.com/pricing/" rel="noopener noreferrer"&gt;chatgpt.com/pricing&lt;/a&gt; and &lt;a href="https://developers.openai.com/codex/pricing" rel="noopener noreferrer"&gt;developers.openai.com/codex/pricing&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cursor pricing: &lt;a href="https://cursor.com/pricing" rel="noopener noreferrer"&gt;cursor.com/pricing&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Anthropic multi-agent token math: &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;Anthropic Engineering — Multi-Agent Research System&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Simon Willison publishes ongoing coding-agent usage-shape breakdowns worth reading alongside: &lt;a href="https://simonwillison.net/" rel="noopener noreferrer"&gt;simonwillison.net&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Compaction Plugin I Was Releasing Warned Me Mid-Release</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/the-compaction-plugin-i-was-releasing-warned-me-mid-release-2f25</link>
      <guid>https://dev.to/kenimo49/the-compaction-plugin-i-was-releasing-warned-me-mid-release-2f25</guid>
      <description>&lt;p&gt;At 3 a.m. I was finishing the release of &lt;a href="https://github.com/kenimo49/compact-ops" rel="noopener noreferrer"&gt;compact-ops&lt;/a&gt;, a Claude Code plugin that protects sessions from context compaction. Context usage hit 67%, and a notification appeared in my own session: threshold crossed, run &lt;code&gt;/compact&lt;/code&gt; at a clean stopping point, here is your current plan and your latest decision. I wrote that warning. I was also the person it saved.&lt;/p&gt;

&lt;p&gt;Twenty minutes later the same session made the opposite point. I ran &lt;code&gt;/compact&lt;/code&gt;, and all three of the plugin's hooks failed. During the release I had restructured the plugin's directory layout, so the running session was still holding a stale path. Compaction itself finished normally, because every hook is fail-open: if it breaks, it steps aside instead of blocking the built-in behavior. One session, one night, and I got to watch the feature work and watch it fail safely. You can't buy dogfooding like that.&lt;/p&gt;

&lt;p&gt;This post covers what &lt;code&gt;/compact&lt;/code&gt; actually throws away, what I borrowed from the plugin this one is derived from, and the six things I hardened before making the repo public.&lt;/p&gt;

&lt;h2&gt;
  
  
  What /compact actually throws away
&lt;/h2&gt;

&lt;p&gt;When a Claude Code session fills its context window, the whole conversation gets compressed into a single built-in summary and the original messages are discarded. Manual &lt;code&gt;/compact&lt;/code&gt; and auto-compact both work this way.&lt;/p&gt;

&lt;p&gt;The summary is decent at code. What it drops is operational state: "the push was already approved", "we tried that approach and it failed", "that number came from this file". When the post-compaction agent forgets those, it re-asks for permissions you already granted and re-attempts fixes you already buried. That's the failure mode I wanted insurance against.&lt;/p&gt;

&lt;p&gt;Here is the timeline with and without the plugin:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Moment&lt;/th&gt;
&lt;th&gt;Standard Claude Code&lt;/th&gt;
&lt;th&gt;With compact-ops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Usage passes 60% (configurable)&lt;/td&gt;
&lt;td&gt;No warning; auto-compact arrives unannounced&lt;/td&gt;
&lt;td&gt;One-shot reminder plus a 3-line recitation of plan / phase / latest decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At compact time&lt;/td&gt;
&lt;td&gt;Built-in summary only&lt;/td&gt;
&lt;td&gt;Same compression, plus a transcript backup and a separate LLM writing a 10-heading state file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right after compact&lt;/td&gt;
&lt;td&gt;Agent continues from the summary alone&lt;/td&gt;
&lt;td&gt;State file and a "re-read the originals first" note injected into the fresh context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After the session ends&lt;/td&gt;
&lt;td&gt;Summary exists only inside that session&lt;/td&gt;
&lt;td&gt;State persists for 30 days and is re-injected on &lt;code&gt;claude --resume&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If a hook fails&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Fail-open; standard compaction proceeds untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The compaction algorithm itself is untouched. Everything runs through official hooks, from the outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing on compact-plus
&lt;/h2&gt;

&lt;p&gt;The core idea is not mine. &lt;a href="https://github.com/u-ichi/compact-plus" rel="noopener noreferrer"&gt;u-ichi's compact-plus&lt;/a&gt; (MIT) got there first: a PreCompact hook that calls a separate LLM to write structured state before the summary eats everything. It's the kind of tool you want installed the moment you read its README.&lt;/p&gt;

&lt;p&gt;My setup had three mismatches with its assumptions, so I built a derivative instead of a fork and changed exactly three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The usage warning works with nothing but the plugin.&lt;/strong&gt; compact-plus reads a marker written by a statusline script that lives in the author's separate dotfiles repo, so a plugin-only install never fires the warning. compact-ops computes usage inside a UserPromptSubmit hook, straight from the latest &lt;code&gt;message.usage&lt;/code&gt; in the transcript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State survives reboots.&lt;/strong&gt; compact-plus keeps state in &lt;code&gt;$TMPDIR&lt;/code&gt;, which is gone after a restart. If you resume yesterday's work every morning, that's no insurance at all. State now lives under &lt;code&gt;~/.claude/compact-ops/&lt;/code&gt;, organized per project, kept for 30 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery also fires on &lt;code&gt;--resume&lt;/code&gt;.&lt;/strong&gt; Not just across a compact. A SessionStart hook injects the same state when you reopen the session the next day, reboot included.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also made the LLM backend Claude-only (Sonnet primary, Haiku fallback). compact-plus falls back to Codex, which assumes a ChatGPT Pro subscription sitting next to your Claude one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A state file with 10 fixed headings
&lt;/h2&gt;

&lt;p&gt;Before each compaction, a separate LLM writes a markdown file that always has the same 10 headings: Active Plan, Current Phase, TaskList Summary, Session Decisions, Constraints and Blockers, Worker Topology, Skills Invoked, Editing Files, Failed Attempts, Recovery Notes. The interesting ones are Session Decisions and Failed Attempts -- precisely the operational facts that built-in summaries tend to thin out.&lt;/p&gt;

&lt;p&gt;The warning side reuses the same file. When usage crosses the threshold, the injected reminder isn't just "compact soon" -- it recites the top of your current state so the agent keeps the big picture during the messy turns right before a compact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[COMPACT WARNING] Context usage reached 67% (134,102 / 200,000 tokens).
State recitation:
- Active Plan: Ship compact-ops v0.2.0 (hardening + marketplace fix)
- Current Phase: post-release verification
- Recent Session Decision: circular symlink removed; source is "./" now
- At a natural work boundary, tell the user they can run /compact as-is.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That recitation is what appeared in my session at 3 a.m. Reading your own plan back, written by a hook you wrote, is a strange kind of code review.&lt;/p&gt;

&lt;p&gt;So the post-compaction agent restarts with two anchors: the standard summary and the structured state file. But the state file is never treated as authoritative. The injected recovery guidance explicitly says to re-read the original project files before trusting anything. An LLM summary trusted blindly by another LLM is just a faster game of telephone.&lt;/p&gt;

&lt;p&gt;It's also a plain markdown file. When a handoff feels off, you can open it and see exactly what the previous session thought it was passing along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six things I hardened before going public
&lt;/h2&gt;

&lt;p&gt;v0.1.0 was "works on my machine". Before flipping the repo public I ran my own review plus a second pass with an independent CLI reviewer, and shipped v0.2.0 with six fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Permissions.&lt;/strong&gt; State files and transcript backups contain your raw conversation, including any secrets that tool output echoed. Every hook now sets &lt;code&gt;umask 077&lt;/code&gt;; directories are 700, files are 600.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;session_id validation.&lt;/strong&gt; Hook input JSON was flowing straight into file paths. It now passes an allowlist (alphanumerics plus &lt;code&gt;._-&lt;/code&gt;, no &lt;code&gt;..&lt;/code&gt;) before touching the filesystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM output validation.&lt;/strong&gt; The state generator used to be trusted after a first-line check. Now all 10 headings must be present, or the previous state is kept and the write is skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-pass jq.&lt;/strong&gt; The transcript squasher spawned jq three or four times per line, which on a long session could eat the hook's entire timeout budget. It's one streaming process now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gzip backups.&lt;/strong&gt; Transcript JSONL compresses to roughly a tenth of its size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug logging.&lt;/strong&gt; The real lesson. Fail-open design doesn't get in your way in production, and it also dies in complete silence. The three hook failures at the top of this post were only visible because the harness printed an error line; the plugin itself had no way to tell me why. &lt;code&gt;COMPACT_OPS_DEBUG=1&lt;/code&gt; now logs every swallowed failure. If you're writing fail-open hooks, write the logging first. I did it in the wrong order and got lucky.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I deliberately didn't build
&lt;/h2&gt;

&lt;p&gt;The Claude Code ecosystem already has heavier answers to context loss: full memory layers with MCP tools, BM25 retrieval over past sessions, three-file dev-docs systems you maintain by hand. Good tools, different trade-offs.&lt;/p&gt;

&lt;p&gt;compact-ops stays intentionally narrow: no MCP server, no database, no new workflow to learn, no daemon. It's shell scripts behind official hooks, and state is plain markdown you can &lt;code&gt;cat&lt;/code&gt;. If it ever misbehaves, you uninstall the plugin and you're back to stock Claude Code -- there's nothing to migrate out of. I wanted the insurance to cost nothing when it isn't paying out.&lt;/p&gt;

&lt;p&gt;The other non-feature: it never replaces the built-in summary. Tools that intercept or rewrite compaction itself break whenever Claude Code changes internals. Hooks are the supported surface, so that's the whole footprint.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kenimo49/compact-ops.git
claude plugin marketplace add /path/to/compact-ops &lt;span class="nt"&gt;--scope&lt;/span&gt; user
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;compact-ops@compact-ops-local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements: Claude Code v2.x, &lt;code&gt;jq&lt;/code&gt;, and the &lt;code&gt;claude&lt;/code&gt; CLI as the state-writing backend. Linux and macOS. After installing, just run &lt;code&gt;/compact&lt;/code&gt; as usual -- each compact costs one extra LLM call (Sonnet by default, downgradable to Haiku or off).&lt;/p&gt;

&lt;p&gt;The plugin's first beneficiary was the session that built it: saved by its own 67% warning, then stress-tested by its own hook failure, in the same night. Agent tooling is like that -- the reasons for a design only get written down where something actually broke.&lt;/p&gt;

&lt;p&gt;What does your agent forget after &lt;code&gt;/compact&lt;/code&gt;? If you have a story where a post-compaction session confidently undid your afternoon, I want to hear it.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>7 Services, 0 Upload Solutions: Why MCP File Transfer Fails</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:05:10 +0000</pubDate>
      <link>https://dev.to/kenimo49/7-services-0-upload-solutions-why-mcp-file-transfer-fails-2f2o</link>
      <guid>https://dev.to/kenimo49/7-services-0-upload-solutions-why-mcp-file-transfer-fails-2f2o</guid>
      <description>&lt;p&gt;I spent a day attaching a single receipt to a freee expense through an MCP client. It never worked. So I went wider: seven services my team actually uses in production, one upload test each.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;zero of seven&lt;/strong&gt; support file upload in the standard MCP way. Four refuse it outright. Three "work" only because the MCP server is reading files off its own local disk — which falls over the moment you containerize the server, and which is not really MCP at all.&lt;/p&gt;

&lt;p&gt;This is not a server bug list. The hole is in the protocol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl58dgkgace0omjun6p52.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%2Fl58dgkgace0omjun6p52.png" alt="7 services tested, 0 full solutions for MCP file upload, June 2026" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol has 3 content types. File is not one of them.
&lt;/h2&gt;

&lt;p&gt;MCP tool results return one of three content types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TextContent&lt;/code&gt; (JSON strings, structured data)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ImageContent&lt;/code&gt; (base64-encoded PNG / JPEG only)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EmbeddedResource&lt;/code&gt; (URI reference to a host-owned resource)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no &lt;code&gt;FileContent&lt;/code&gt;. There never was. An Anthropic engineer answered a &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1197" rel="noopener noreferrer"&gt;public Discussion #1197&lt;/a&gt; thread on this directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I don't think you're overlooking anything, your use-case is currently finicky in the current state of the protocol."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translated from politeness: this is a real gap, you found it, we know.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1306" rel="noopener noreferrer"&gt;SEP-1306&lt;/a&gt; proposed adding a binary elicitation mode in August 2025, where the server hands the client an upload URL and the client POSTs &lt;code&gt;multipart/form-data&lt;/code&gt; to it. A later proposal, &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356" rel="noopener noreferrer"&gt;SEP-2356&lt;/a&gt;, adds &lt;code&gt;Tool.inputFiles&lt;/code&gt; and &lt;code&gt;requestedFiles&lt;/code&gt; so tools can declaratively say "I expect files here" and clients can render a native file picker.&lt;/p&gt;

&lt;p&gt;Both are proposals. Neither is in the &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/" rel="noopener noreferrer"&gt;2026-07-28 release candidate&lt;/a&gt;. The release candidate ships Apps, Tasks, and elicitation form/URL modes — all of which are "do file work &lt;em&gt;outside&lt;/em&gt; the core protocol" workarounds. The core itself stays text and base64-image only.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvaz91xjnmapirfx52dbx.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%2Fvaz91xjnmapirfx52dbx.png" alt="MCP tool result types: TextContent, ImageContent, EmbeddedResource ship — FileContent does not exist in the spec" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What each service actually does when you try
&lt;/h2&gt;

&lt;h3&gt;
  
  
  freee (accounting) — hard no
&lt;/h3&gt;

&lt;p&gt;freee's REST API takes &lt;code&gt;multipart/form-data&lt;/code&gt; receipt attachments. MCP's JSON-RPC layer cannot send that. The MCP server sits between your client and freee, and the protocol it speaks upstream simply has no slot for the bytes. Expense workflows that need receipt attachment do not complete end-to-end. They stop at "describe the receipt in text."&lt;/p&gt;

&lt;h3&gt;
  
  
  Jira / Confluence — hard no, and worse in containers
&lt;/h3&gt;

&lt;p&gt;The Atlassian official remote MCP server's &lt;a href="https://community.atlassian.com/" rel="noopener noreferrer"&gt;community response&lt;/a&gt; says it plainly: "file uploads or image attachments via the MCP Remote Agent are not supported."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sooperset/mcp-atlassian/issues/618" rel="noopener noreferrer"&gt;mcp-atlassian Issue #618&lt;/a&gt; is darker. The attachment tool expects a local filesystem path on the MCP server itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attachment_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"filename"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grafana.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"File not found: /home/user/jira-mcp/grafana.png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your client has the screenshot. The MCP server is in a Docker container that cannot see your filesystem. Issue tracked, no fix path because the fix is upstream in the protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notion — on the roadmap, no date
&lt;/h3&gt;

&lt;p&gt;Notion's docs state: "Image and file uploads are not currently supported in Notion MCP, but this is on our roadmap." Roadmap items without dates do what they always do.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub — no, and developers have noticed
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/github/github-mcp-server/issues/738" rel="noopener noreferrer"&gt;github-mcp-server Issue #738&lt;/a&gt; asks for image upload so Claude Code can attach UI screenshots to PRs. The thread is closed-pending-spec. Translation: the GitHub team is also waiting for SEP-1306.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gmail, Google Drive, Slack — "works" in a way that doesn't count
&lt;/h3&gt;

&lt;p&gt;Third-party Gmail and Drive MCP servers attach files by reading the path off the server's own disk. CData's Slack server has an &lt;code&gt;UploadFile&lt;/code&gt; tool that does the same. None of these are transferring bytes through MCP. They are transferring a &lt;em&gt;path string&lt;/em&gt;, then the server opens the file locally.&lt;/p&gt;

&lt;p&gt;Two problems with that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client and server need to share a filesystem. Containerize the server and the model is now describing files it cannot reach.&lt;/li&gt;
&lt;li&gt;It's a side channel, not the protocol. If the spec eventually does add &lt;code&gt;FileContent&lt;/code&gt; or binary elicitation, every "works today" path will be the wrong shape.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why the core avoids binary on purpose
&lt;/h2&gt;

&lt;p&gt;Worth steelmanning: the spec authors did not forget. The avoidance is deliberate, for three reasons that are all defensible in isolation and add up to a real product gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON-RPC is text-shaped.&lt;/strong&gt; MCP picked JSON-RPC because it's introspectable and trivially debuggable. Binary in JSON means base64, which means a 33% size tax on every byte and a flood of tokens into the model's context window. A 1 MB receipt becomes 1.33 MB of text and probably ~340k tokens of nothing the model can do anything semantic with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File paths are a known injection surface.&lt;/strong&gt; Every "let the model send a filename" feature in the last 30 years has had a command injection or path traversal bug filed against it within a year. The MCP team is being conservative on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The context window is the bottleneck.&lt;/strong&gt; If file upload were a primitive, every tool would want it, and conversations would routinely embed 5–10 MB of base64. The economics break before the security does.&lt;/p&gt;

&lt;p&gt;These are real. They also mean the protocol is making the file problem someone else's. Right now that someone is every MCP server author, and the solutions don't compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workaround that actually ships
&lt;/h2&gt;

&lt;p&gt;Until SEP-1306 or SEP-2356 lands, the only thing that genuinely works end-to-end is the &lt;strong&gt;reference-not-bytes&lt;/strong&gt; pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The MCP client (or the user) uploads the file to object storage — S3, GCS, a presigned-URL endpoint your server owns.&lt;/li&gt;
&lt;li&gt;The MCP tool call passes the resulting &lt;strong&gt;URL or storage key as a &lt;code&gt;TextContent&lt;/code&gt; string&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The MCP server fetches the bytes from storage, hands them to the upstream API (freee, GitHub, Jira), and returns the new attachment ID as text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bytes never touch JSON-RPC. The model never sees a 340k-token base64 blob. The server is a thin proxy, and it works identically whether it runs on your laptop or in a container.&lt;/p&gt;

&lt;p&gt;This is also, not coincidentally, the shape SEP-1306 will eventually formalize. Building this way now means the migration when (if) the spec lands is roughly: swap your hand-rolled upload endpoint for the spec'd one, keep everything else. It's also where I should have started, instead of spending the first day trying to convince the protocol to carry bytes it was never going to carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five-second checklist before you commit to MCP for a file workflow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does your upstream API need &lt;code&gt;multipart/form-data&lt;/code&gt;? If yes, MCP is not your transport — it's your &lt;em&gt;control plane&lt;/em&gt;. Wire object storage underneath.&lt;/li&gt;
&lt;li&gt;Is your MCP server containerized? If yes, &lt;em&gt;do not&lt;/em&gt; trust any "works locally" file path tool. It will silently fail in prod.&lt;/li&gt;
&lt;li&gt;Is the file &amp;gt;1 MB? Don't even consider base64-embedding it in &lt;code&gt;ImageContent&lt;/code&gt;. The token bill alone is a no.&lt;/li&gt;
&lt;li&gt;Do you need an audit trail of which bytes were uploaded? Log the storage key, not the tool call — JSON-RPC didn't carry the bytes anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to take away
&lt;/h2&gt;

&lt;p&gt;MCP is a great control plane. It is not a file transport. The 2026-07-28 release candidate confirms this for another release cycle. If file work is on the critical path of your agent — expenses, ticket attachments, PR screenshots, Notion uploads — you need object storage and a thin server-side fetcher, today. Treat the protocol's eventual &lt;code&gt;FileContent&lt;/code&gt; as a possible future cleanup, not a roadmap dependency.&lt;/p&gt;

&lt;p&gt;The seven-service test reads as a vendor problem until you sit with the spec. Then it reads as a spec problem with seven downstream casualties. Worth knowing which one you're solving.&lt;/p&gt;

&lt;p&gt;If you want the systematic version — every MCP server I tested, the exact failure modes, OWASP MCP Top 10, and the production reference-not-bytes pattern with code — I wrote a Japanese book on it: &lt;a href="https://kenimoto.dev/books/mcp-security-practice?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp-7-uploads" rel="noopener noreferrer"&gt;MCP Security in Practice (Impress)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1306" rel="noopener noreferrer"&gt;SEP-1306: Binary Mode Elicitation for File Uploads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356" rel="noopener noreferrer"&gt;SEP-2356: File input support for tools and elicitation (PR)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/" rel="noopener noreferrer"&gt;2026-07-28 MCP Specification Release Candidate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sooperset/mcp-atlassian/issues/618" rel="noopener noreferrer"&gt;mcp-atlassian Issue #618 — Docker filesystem issue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/github/github-mcp-server/issues/738" rel="noopener noreferrer"&gt;github-mcp-server Issue #738 — PR image upload&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1197" rel="noopener noreferrer"&gt;MCP Discussion #1197 — current protocol limitation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Anthropic Rewrote frontend-design Skill: 3 AI Design Clichés Named (With Hex Codes)</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Fri, 03 Jul 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/anthropic-rewrote-frontend-design-skill-3-ai-design-cliches-named-with-hex-codes-22m4</link>
      <guid>https://dev.to/kenimo49/anthropic-rewrote-frontend-design-skill-3-ai-design-cliches-named-with-hex-codes-22m4</guid>
      <description>&lt;p&gt;Anthropic quietly rewrote their &lt;code&gt;frontend-design&lt;/code&gt; Skill on June 18 in commit &lt;a href="https://github.com/anthropics/claude-code/commit/423563cf" rel="noopener noreferrer"&gt;&lt;code&gt;423563cf&lt;/code&gt;&lt;/a&gt;. The new version contradicts the old one on its central thesis, and names three specific AI-generated design clichés in the public plugin documentation. &lt;strong&gt;With hex codes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; diff is &lt;strong&gt;+39 / -26&lt;/strong&gt; lines (commit-wide it's +41/-28, the rest is a &lt;code&gt;marketplace.json&lt;/code&gt; bump and the plugin version going to 1.1.0). On paper, a maintenance bump. In reality, a philosophy reversal.&lt;/p&gt;

&lt;p&gt;I noticed this while reviewing my own image-generation skills last week against the upstream &lt;a href="https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;anthropics/claude-code&lt;/code&gt; version of the file&lt;/a&gt;. It slid in under the radar, which is a shame because it's one of the more interesting design-engineering shifts Anthropic has shipped recently. Here is what changed and why it matters if you ship UI that touches a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old version: be bold everywhere
&lt;/h2&gt;

&lt;p&gt;The old Skill's central instruction was extreme. From the file Anthropic had been shipping until June 18:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And closed with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The structure of the old document was a list of axes to push: Typography, Color, Motion, Spatial Composition, Backgrounds. Each axis got its own "be distinctive here" paragraph. The implicit instruction was: maximize boldness across every axis simultaneously.&lt;/p&gt;

&lt;p&gt;It reads like a pep talk. Imagine running it on every UI generation in your product. Now imagine the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  New version: be bold in exactly one place
&lt;/h2&gt;

&lt;p&gt;The new Skill opens with a completely different frame:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Approach this as the design lead at a small studio known for giving every client a visual identity that could not be mistaken for anyone else's. This client has already rejected proposals that felt templated, and is paying for a distinctive point of view: make deliberate, opinionated choices about palette, typography, and layout that are specific to this brief, and &lt;strong&gt;take one real aesthetic risk you can justify.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note the word &lt;code&gt;one&lt;/code&gt;. The new file uses it again later, more directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Spend your boldness in one place.&lt;/strong&gt; Let the signature element be the one memorable thing, keep everything around it quiet and disciplined.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then closes the restraint section with an aphorism widely attributed to Coco Chanel:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Consider Chanel's advice: before leaving the house, take a look in the mirror and remove one accessory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reversal is precise. &lt;strong&gt;Old:&lt;/strong&gt; pick an extreme on every axis. &lt;strong&gt;New:&lt;/strong&gt; pick one signature, keep the rest quiet, then remove one more thing before you ship.&lt;/p&gt;

&lt;p&gt;If you read both versions back to back, the old one reads (to me) like it was written for the demo. The new one reads like it was written by someone who has sat with a year's worth of "be bold everywhere" outputs and noticed they all converged. I have no insider information on intent — this is just how the two documents land if you read them in sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  They named the three defaults. With hex codes.
&lt;/h2&gt;

&lt;p&gt;This is the part I did not expect.&lt;/p&gt;

&lt;p&gt;The new file contains this paragraph:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI-generated design right now clusters around three looks:&lt;br&gt;
(1) a warm cream background (near &lt;code&gt;#F4F1EA&lt;/code&gt;) with a high-contrast serif display and a terracotta accent;&lt;br&gt;
(2) a near-black background with a single bright acid-green or vermilion accent;&lt;br&gt;
(3) a broadsheet-style layout with hairline rules, zero border-radius, and dense newspaper-like columns.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anthropic is publicly stating, in their own plugin docs, the hex value of the cream background their model defaults to. They followed it with this carefully worded line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All three are legitimate for some briefs, but they are defaults rather than choices, and they appear regardless of subject.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is doing real work. It is not saying "these are bad designs." My read is closer to: "these are what we ship when no one is steering us." Either way, the team is auditing their own output and writing the audit into the public docs.&lt;/p&gt;

&lt;p&gt;This is essentially the visual-design equivalent of the AI Slop word lists that text-side teams have been maintaining for a year. To make the parallel concrete, here is what the three defaults look like rendered as actual product hero sections. Same fictional product (an audit tool called Lumen, naturally), three default treatments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrpspiequbvaovzp3loy.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%2Fcrpspiequbvaovzp3loy.png" alt="Cliche 1: warm cream and serif display with terracotta accent" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cliché 1: &lt;code&gt;#F4F1EA&lt;/code&gt; cream, a Playfair-style italic serif, terracotta accent. Reads "editorial sophistication" at a glance, reads "every AI-generated landing page I've seen this quarter" two seconds later.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qx8evpllfa3lwgm1m7i.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%2F7qx8evpllfa3lwgm1m7i.png" alt="Cliche 2: near-black with single acid-green accent" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cliché 2: near-black background, single bright accent, monospace details, "trusted by" strip. First impression: edgy modern tech. Closer look: indistinguishable from the last three YC Demo Day landing pages you visited.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53r7mret6l95rjq3vvji.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%2F53r7mret6l95rjq3vvji.png" alt="Cliche 3: broadsheet layout with hairline rules" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cliché 3: broadsheet style, hairline rules, zero border-radius, dense columns. Signals "serious and intellectual," then immediately falls back into the AI startup "About" page genre.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;None of these are bad. They are all competent, defensible, ship-ready. They are also default behavior, which means they read as templated regardless of what the underlying product actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The process got replaced with a loop
&lt;/h2&gt;

&lt;p&gt;The other major structural change is in how the Skill instructs the model to &lt;em&gt;work&lt;/em&gt;. The old version had a list of axes. The new version has a process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process: brainstorm, explore, plan, critique, build, critique again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expanded instructions describe a five-step loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the brief. If it is vague, pin a subject, audience, and the single job the page must do.&lt;/li&gt;
&lt;li&gt;Build a compact token system: 4-6 hex values, 2+ type roles, a layout described in prose + ASCII wireframe, and a &lt;em&gt;signature&lt;/em&gt; element.&lt;/li&gt;
&lt;li&gt;Critique the plan against the brief. Anywhere it reads like "the generic default you would produce for any similar page," rewrite it and say what changed.&lt;/li&gt;
&lt;li&gt;Build it.&lt;/li&gt;
&lt;li&gt;Take a screenshot and critique your own output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is, structurally, code review applied to design. Plan → diff against the spec → implement → self-review. The Skill is essentially asking the model to do its own design critique as a first-class step, with explicit instruction to flag any place where it would have produced the same thing for any other brief.&lt;/p&gt;

&lt;p&gt;Whether the model can actually do this self-critique reliably is a separate question. But the &lt;em&gt;intent&lt;/em&gt; — "audit your own defaults as part of the work" — is a notable shift from "execute the brief."&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy got promoted to design material
&lt;/h2&gt;

&lt;p&gt;The other new section is "More on writing in design." It did not exist in the old version. The opening line is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Words appear in a design for one reason: to make it easier to understand, and therefore easier to use. They are design material, not decoration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rules are practical. Some I want to lift directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A button says exactly what happens. "Save changes," not "Submit."&lt;/li&gt;
&lt;li&gt;The same verb threads through the whole flow. The button labeled "Publish" produces a toast that says "Published."&lt;/li&gt;
&lt;li&gt;Errors don't apologize. They state what happened and how to fix it.&lt;/li&gt;
&lt;li&gt;An empty screen is an invitation to act, not a mood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have ever fought with a product team about whether UX copy is "the designer's job" or "the engineer's job," Anthropic just put it on the design Skill's responsibility list.&lt;/p&gt;

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

&lt;p&gt;Here is the same Lumen hero, rebuilt with the new Skill's philosophy. Navy monochrome, one signature element: the word "One." set enormous, taking the entire vertical. Everything else quiet and disciplined.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9p21rf23x68sjtn75b8.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%2Fa9p21rf23x68sjtn75b8.png" alt="Restraint version: navy monochrome with one giant typographic signature" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The "after" treatment. Navy text on near-white, the single word "One." set giant and italic as the only visual moment, navigation and footer deliberately small. The image's job: prove that "spend boldness in one place" is a real layout choice, not a slogan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The signature here is a typographic moment, but the recipe generalizes: pick one axis (color, type, layout, motion, decoration) to push hard, then &lt;em&gt;withdraw&lt;/em&gt; on every other axis. The trap the old Skill set was telling the model to push on every axis at once, which paradoxically forces convergence on the safest combination of attacks. The new Skill explicitly diagnoses this in the line "Spend your boldness in one place."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you ship UI through a model
&lt;/h2&gt;

&lt;p&gt;A few practical takeaways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your generation prompts and Skills against the new version.&lt;/strong&gt; If your prompt has language like "be bold," "be distinctive," "push the design," you are likely producing one of the three named clichés. The fix is to specify &lt;em&gt;one&lt;/em&gt; axis to push and explicitly constrain the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hex-code audit is borrowable.&lt;/strong&gt; "If the output background is near &lt;code&gt;#F4F1EA&lt;/code&gt; and the accent is terracotta, flag it" is a check you can actually write. The same shape works for the other two clichés (near-black with an acid-green accent, broadsheet hairlines on white) — pick your own threshold values for those, Anthropic only spec'd the cream one to the hex. It's the visual equivalent of grep-ing for "delve" in LLM output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bring copy into the design pipeline.&lt;/strong&gt; If your design tooling doesn't include button labels, empty-state text, and error messages as first-class artifacts, you're shipping a stale split of responsibilities. Anthropic just promoted copy to "design material" in their public docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a critique step into your generation loop.&lt;/strong&gt; The "plan, build, critique, build again" pattern is portable. You can wrap any single-shot generation in a self-review pass that explicitly asks: "what would you have produced for any similar brief, and how does this differ?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The summary
&lt;/h2&gt;

&lt;p&gt;If I had to compress the new Skill into one sentence: &lt;em&gt;AI-generated design fails by being bold everywhere; the fix is to be bold in exactly one place and remove one accessory before shipping.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Anthropic is auditing the defaults their own model produces and writing the audit into the public plugin docs. That's a useful thing to read, and an unusually honest move to make where everyone can see it.&lt;/p&gt;

&lt;p&gt;The full diff is at &lt;a href="https://github.com/anthropics/claude-code/commit/423563cf" rel="noopener noreferrer"&gt;commit 423563cf&lt;/a&gt; if you want to read it cold. It's worth ten minutes.&lt;/p&gt;

&lt;p&gt;If you want the systematic version of building Skills like this — how &lt;code&gt;SKILL.md&lt;/code&gt; is structured, how to compose Skills with subagents, when to use a Skill vs. a slash command — I wrote a Kindle book on it: &lt;a href="https://kenimoto.dev/books/claude-code-mastery?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=afd-3-cliches" rel="noopener noreferrer"&gt;Claude Code Mastery&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;anthropics/claude-code&lt;/code&gt; — frontend-design &lt;code&gt;SKILL.md&lt;/code&gt; (current, post-June 18 rewrite)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/blob/423563cf/plugins/frontend-design/skills/frontend-design/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;SKILL.md&lt;/code&gt; pinned to commit &lt;code&gt;423563cf&lt;/code&gt; — the exact version this article quotes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/commit/423563cf" rel="noopener noreferrer"&gt;Commit &lt;code&gt;423563cf&lt;/code&gt; — the +39/-26 rewrite this article is reading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>anthropic</category>
      <category>ai</category>
      <category>design</category>
      <category>claude</category>
    </item>
    <item>
      <title>MCP Servers Ship Without OAuth. I Added It and 3 of 5 Clients Broke.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Thu, 02 Jul 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/mcp-servers-ship-without-oauth-i-added-it-and-3-of-5-clients-broke-j5l</link>
      <guid>https://dev.to/kenimo49/mcp-servers-ship-without-oauth-i-added-it-and-3-of-5-clients-broke-j5l</guid>
      <description>&lt;p&gt;The honest version of my MCP server's auth story is short: there wasn't any. The server ran over HTTP, anyone who knew the URL could call its tools, and the tools touched a real account. I knew this was bad. I had even written about other people doing it. So I did the responsible thing and added OAuth 2.1 the way the MCP spec describes it.&lt;/p&gt;

&lt;p&gt;Then I connected the same server from five clients to confirm nothing broke. Two of them authorized cleanly. Three of them broke, each in a completely different place, and not one of the failures was a bug in my server. The spec was satisfied. The clients had simply not caught up to it. That gap, the few inches between "the spec says X" and "the client does X," is exactly where your authorization lives, which means it is exactly the place you cannot afford a gap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqvomwe4iuc8dr9jjz1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqvomwe4iuc8dr9jjz1a.png" alt="A five-row matrix of MCP clients against an OAuth handshake: Claude Desktop and MCP Inspector pass, while VS Code, Cursor, and Claude Code break on loopback redirect port, dynamic client registration, and a rejected manual client ID respectively" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "adding OAuth to an MCP server" actually means in 2026
&lt;/h2&gt;

&lt;p&gt;Adding OAuth to an MCP server in 2026 means implementing the &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;2025-06-18 authorization spec&lt;/a&gt;, where your server is an OAuth 2.1 &lt;em&gt;Resource Server&lt;/em&gt;, not its own authorization server. This is the part that surprised me, because it inverts the older mental model.&lt;/p&gt;

&lt;p&gt;In the previous revision (2025-03-26) the MCP server was treated more or less as its own authorization server. The &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/changelog" rel="noopener noreferrer"&gt;2025-06-18 changelog&lt;/a&gt; reclassified it: "Classify MCP servers as OAuth Resource Servers, adding protected resource metadata to discover the corresponding Authorization server." So my server's job got smaller and pickier at the same time. It no longer issues tokens. It validates them, advertises where the real authorization server lives, and rejects anything addressed to someone else.&lt;/p&gt;

&lt;p&gt;Concretely, a compliant server has to do four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Return &lt;code&gt;401 Unauthorized&lt;/code&gt; with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header pointing at its resource metadata URL.&lt;/li&gt;
&lt;li&gt;Serve Protected Resource Metadata at &lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt; (&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;RFC 9728&lt;/a&gt;), naming the authorization server.&lt;/li&gt;
&lt;li&gt;Let the client discover the authorization server's own metadata (&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;RFC 8414&lt;/a&gt;) and register through Dynamic Client Registration (&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;RFC 7591&lt;/a&gt;), which the spec marks as SHOULD, not MUST.&lt;/li&gt;
&lt;li&gt;Validate the token's audience so a token minted for some other service can't be replayed against mine (&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;RFC 8707 resource indicators&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I implemented all four. I ran the &lt;a href="https://github.com/modelcontextprotocol/inspector" rel="noopener noreferrer"&gt;MCP Inspector&lt;/a&gt; against it and watched the full dance: 401, metadata fetch, browser redirect, token, authorized call. Green across the board. That was the moment I assumed I was done, which is the exact moment in every war story where the narrator is about to learn something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: five clients, one server, same handshake
&lt;/h2&gt;

&lt;p&gt;My test was deliberately boring. One server, five clients, the same OAuth handshake each time. The clients were the ones I actually use or expect my users to use: Claude Desktop, VS Code with the Copilot MCP integration, Cursor, Claude Code, and a remote bridge (&lt;code&gt;mcp-remote&lt;/code&gt;) for anything that only speaks stdio.&lt;/p&gt;

&lt;p&gt;I went in expecting an even spread of minor papercuts. What I got was a clean split. Two clients walked through the front door. Three got stuck in three different doorways, and the interesting part is that the three failures map almost perfectly onto the three things the spec leaves optional or underspecified. The clients didn't fail randomly. They failed exactly where the spec gives them room to disagree.&lt;/p&gt;

&lt;p&gt;Here is the scoreboard before the autopsies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client            Result    Failed at
----------------------------------------------------------
Claude Desktop    PASS      (clean)
MCP Inspector     PASS      (clean)
VS Code (Copilot) BROKE     loopback redirect URI port
Cursor            BROKE     dynamic client registration
Claude Code       BROKE     manual client_id rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(MCP Inspector is a tool, not a shipping client, but it earned its row by being the only thing that worked on the first try, which tells you something about who the spec was written against.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #1: VS Code and the loopback port that wasn't there
&lt;/h2&gt;

&lt;p&gt;VS Code broke because its OAuth callback wanted a specific loopback port that was already taken, and the authorization server refused the redirect. The error on screen was "Invalid Redirect URI," which is an unhelpful thing to read when your redirect URI looks correct.&lt;/p&gt;

&lt;p&gt;VS Code added native MCP authentication in &lt;a href="https://code.visualstudio.com/api/extension-guides/ai/mcp" rel="noopener noreferrer"&gt;version 1.101&lt;/a&gt;: it parses &lt;code&gt;WWW-Authenticate&lt;/code&gt;, follows the metadata, and tries DCR first. All correct. The trouble is the redirect target. OAuth for a desktop client uses a loopback redirect (&lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;/...&lt;/code&gt;), and &lt;a href="https://datatracker.ietf.org/doc/html/rfc8252#section-7.3" rel="noopener noreferrer"&gt;RFC 8252 §7.3&lt;/a&gt; is explicit that the authorization server MUST allow &lt;em&gt;any&lt;/em&gt; port for loopback, because the client picks an ephemeral one at runtime. Plenty of authorization servers ignore that and do strict, exact redirect-URI matching including the port. When the port VS Code grabbed didn't match what was registered, the handshake died.&lt;/p&gt;

&lt;p&gt;This is not a hypothetical I reverse-engineered from my own logs. It is filed: &lt;a href="https://github.com/microsoft/vscode/issues/278512" rel="noopener noreferrer"&gt;microsoft/vscode #278512&lt;/a&gt;, "MCP: OAuth Server Redirect URI Mismatch Bug," describes the default loopback port being unavailable and producing exactly "Invalid Redirect URI." The same shape shows up in &lt;a href="https://github.com/github/copilot-cli/issues/1951" rel="noopener noreferrer"&gt;github/copilot-cli #1951&lt;/a&gt; and in &lt;a href="https://github.com/anthropics/claude-code/issues/51319" rel="noopener noreferrer"&gt;claude-code #51319&lt;/a&gt;, where a hardcoded callback port breaks the moment you run two sessions at once. The fix lives on the authorization server: relax redirect matching for loopback the way the RFC tells you to. But you only learn you needed that fix by watching a client fail, because the spec-compliant server and the spec-compliant client can still disagree about whether "port" is part of the URI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #2: Cursor and the authorization server that wouldn't register a stranger
&lt;/h2&gt;

&lt;p&gt;Cursor broke because it insisted on Dynamic Client Registration, and my authorization server, like most authorization servers, doesn't offer it. DCR is the step where a brand-new client introduces itself to the authorization server at runtime and gets credentials on the spot, no human pre-registration. The MCP spec lists DCR as SHOULD. Clients read SHOULD as "rely on it." Authorization servers read SHOULD as "skip it."&lt;/p&gt;

&lt;p&gt;That mismatch is the single most common way OAuth-enabled MCP servers break, and it is not subtle in the issue trackers. &lt;a href="https://github.com/github/github-mcp-server/issues/1404" rel="noopener noreferrer"&gt;github/github-mcp-server #1404&lt;/a&gt;: "Dynamic Client Registration not supported." &lt;a href="https://github.com/anthropics/claude-code/issues/38102" rel="noopener noreferrer"&gt;claude-code #38102&lt;/a&gt;: the client "attempts DCR even when a clientId is supplied." &lt;a href="https://github.com/openai/codex/issues/15818" rel="noopener noreferrer"&gt;openai/codex #15818&lt;/a&gt;: OAuth login "fails when authorization server does not support dynamic client registration." &lt;a href="https://github.com/microsoft/vscode/issues/279955" rel="noopener noreferrer"&gt;microsoft/vscode #279955&lt;/a&gt;, filed flatly as "Dynamic Client Registration (DCR) issues in MCP Servers." Every major client has a version of this.&lt;/p&gt;

&lt;p&gt;The root cause is an asymmetry the spec bets on: it assumes authorization servers will support DCR, and &lt;a href="https://workos.com/blog/dynamic-client-registration-dcr-mcp-oauth" rel="noopener noreferrer"&gt;most of them do not&lt;/a&gt;. Okta, Entra ID, and the rest of the enterprise identity world mostly want you to pre-register a client by hand. So a client that &lt;em&gt;only&lt;/em&gt; knows how to DCR is a client that cannot talk to the authorization server every enterprise already runs. I had wired my server to a real identity provider, not a toy one, and the toy was the only thing that would have worked unmodified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break #3: Claude Code and the client_id it refused to use
&lt;/h2&gt;

&lt;p&gt;Claude Code broke in the mirror image of break #2: I tried to hand it a pre-registered &lt;code&gt;client_id&lt;/code&gt; to dodge the DCR problem, and it tried to do DCR anyway. The whole reason to support a manual client_id is to work with authorization servers that don't do DCR. A client that ignores the client_id you gave it and reaches for DCR regardless has, in effect, no escape hatch.&lt;/p&gt;

&lt;p&gt;This one is also documented rather than imagined. &lt;a href="https://github.com/anthropics/claude-code/issues/38102" rel="noopener noreferrer"&gt;claude-code #38102&lt;/a&gt; is titled, almost word for word, "MCP OAuth: 'does not support dynamic client registration' despite clientId configured." On the VS Code side the gap is tracked as a feature that doesn't exist yet: &lt;a href="https://github.com/microsoft/vscode/issues/252892" rel="noopener noreferrer"&gt;#252892&lt;/a&gt;, "capability to register a clientId for MCP OAuth," and &lt;a href="https://github.com/microsoft/vscode/issues/257415" rel="noopener noreferrer"&gt;#257415&lt;/a&gt;, "No option to disable Dynamic Client Registration and use custom (static) client information." Anthropic's own connector docs say the quiet part directly for Claude.ai: it &lt;a href="https://support.claude.com/en/articles/11503834-building-custom-connectors-via-remote-mcp-servers" rel="noopener noreferrer"&gt;"requires Dynamic Client Registration support and does not yet support a way for users to specify a client ID or secret."&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the two failures are a pincer. Break #2 is a client that demands DCR from an authorization server that can't do it. Break #3 is a client that won't accept the manual credential you'd use to route around DCR. Same root, opposite ends, and between them they cover most of the identity providers a real company actually uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got from three breaks to zero
&lt;/h2&gt;

&lt;p&gt;I got from three breaks to zero by fixing the authorization server, not the clients, plus one bridge for the client I couldn't fix. The clients aren't mine to patch, so the move was to make the server tolerant of the ways clients disagree.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For the loopback port mismatch:&lt;/strong&gt; relax redirect-URI matching on the authorization server so any loopback port validates, per RFC 8252. This is a server-side allowance, and it's the one change that quietly fixes a whole family of "Invalid Redirect URI" reports at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the DCR-required client against a no-DCR authorization server:&lt;/strong&gt; put an OAuth proxy in front that &lt;em&gt;does&lt;/em&gt; speak DCR, registers the client dynamically, and maps it onto a pre-registered upstream client. This is precisely why OAuth proxies exist in the MCP world, and why the community keeps building them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the client that ignored my manual client_id:&lt;/strong&gt; route it through &lt;a href="https://github.com/geelen/mcp-remote" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-remote&lt;/code&gt;&lt;/a&gt;, the npx bridge that runs the OAuth flow on the client's behalf and, in its static-config form, lets you inject a pre-registered client_id the picky client never had to know about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are elegant. They are shims around the seam between a spec and its implementations. But they share a useful property: every fix lives on infrastructure I control, because the one thing you can't ship is "please upgrade your client."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you before you add OAuth to an MCP server
&lt;/h2&gt;

&lt;p&gt;Before you add OAuth to an MCP server, assume the spec is the easy part and client compatibility is the actual project. I budgeted my time backwards. The server-side OAuth 2.1 implementation took an afternoon. Reconciling it with how five clients actually behave took the rest of the week, and that ratio is the real lesson.&lt;/p&gt;

&lt;p&gt;Three things I'd bolt to the wall:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test against real clients, not just the Inspector.&lt;/strong&gt; The Inspector is a reference implementation and it passes by construction. It is the most optimistic possible reader of your server. Your users are running Cursor and VS Code and Claude Code, and those are the readers who disagree. A green Inspector run means your server is correct, not that anyone can connect to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat DCR as a coin flip, and own both sides.&lt;/strong&gt; Half the clients assume Dynamic Client Registration works; half the authorization servers don't offer it. You cannot pick a side and be safe, because your users bring their own clients and your company brings its own identity provider. The durable answer is a proxy that speaks DCR outward and pre-registration inward, so the disagreement resolves on your infrastructure instead of in an error dialog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know which spec revision your clients are reading.&lt;/strong&gt; The auth spec is moving fast. The &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/changelog" rel="noopener noreferrer"&gt;2025-11-25 revision&lt;/a&gt; already deprecates DCR in favor of &lt;a href="https://aaronparecki.com/2025/11/25/1/mcp-authorization-spec-update" rel="noopener noreferrer"&gt;Client ID Metadata Documents&lt;/a&gt; and makes PKCE mandatory, which means a year from now the breakage will have a new shape and the same cause: clients catching up to the spec at different speeds. The thing that broke three of my five clients wasn't a flaw in OAuth or in MCP. It was the lag, and lag is permanent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want the full map of where MCP's attack surface actually lives, including why so many servers ship with no auth at all and what the OWASP MCP Top 10 says to do about it, I wrote &lt;a href="https://kenimoto.dev/books/mcp-security-practice?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp-oauth-3of5-broke" rel="noopener noreferrer"&gt;MCP Security in Practice&lt;/a&gt;. This post is the chapter that only exists because I tried to fix one of those gaps and found three more.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Adding OAuth to an MCP server is not a single task; it is a server task you finish in an afternoon followed by a compatibility task you finish never, because the clients keep moving. My server was spec-correct the whole time. Three of five clients still couldn't connect, because each one disagreed with the spec in a different place: a loopback port, a missing DCR, a rejected client_id. The fixes all lived on my side, which is the good news and the bad news at once. The good news is you can fix it. The bad news is "it works in the Inspector" was never the finish line. The finish line is five clients, and the spec only gets you to the door.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>oauth</category>
      <category>ai</category>
    </item>
    <item>
      <title>RTX 4070 + Qwen 35B: 2.8x Speedup From One llama.cpp Flag (--cpu-moe)</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Wed, 01 Jul 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/rtx-4070-qwen-35b-28x-speedup-from-one-llamacpp-flag-cpu-moe-569o</link>
      <guid>https://dev.to/kenimo49/rtx-4070-qwen-35b-28x-speedup-from-one-llamacpp-flag-cpu-moe-569o</guid>
      <description>&lt;p&gt;The Ollama defaults gave me &lt;strong&gt;12.2 tok/s&lt;/strong&gt; on Qwen3.5-35B-A3B against an RTX 4070 (12 GB). I switched to &lt;code&gt;llama.cpp&lt;/code&gt; with two flags and got &lt;strong&gt;34.6 tok/s&lt;/strong&gt;. 2.8x.&lt;/p&gt;

&lt;p&gt;The two flags were &lt;code&gt;-ngl 99&lt;/code&gt; (offload all layers to GPU) and &lt;code&gt;--cpu-moe&lt;/code&gt; (except the MoE experts, which go on the CPU). One of them is the obvious one. The other is the entire post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-m&lt;/span&gt; qwen35.gguf &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;--cpu-moe&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to walk through why that specific combination works on a 12 GB card that "should not" fit a 35B model, the full offload sweep so you can pick the right setting for your VRAM tier, and the three things that will cause the same command to under-perform on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GPU: RTX 4070, 12 GB&lt;/li&gt;
&lt;li&gt;RAM: 31 GB&lt;/li&gt;
&lt;li&gt;OS: WSL2 Ubuntu 24.04&lt;/li&gt;
&lt;li&gt;CUDA: 12.9&lt;/li&gt;
&lt;li&gt;Model: Qwen3.5-35B-A3B, Q4_K_M quantization (20.49 GiB, 34.66B params, 128 experts, 8 active per token)&lt;/li&gt;
&lt;li&gt;llama.cpp: built with &lt;code&gt;cmake -B build -DGGML_CUDA=ON&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are on the newer &lt;a href="https://qwen.ai/blog?id=qwen3.6-35b-a3b" rel="noopener noreferrer"&gt;Qwen3.6-35B-A3B&lt;/a&gt; (released early 2026) or the smaller &lt;a href="https://huggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct" rel="noopener noreferrer"&gt;Qwen3-Coder-30B-A3B&lt;/a&gt;, the same flag math applies. Both are 128-expert MoEs with 8 active per forward pass, so the offload ratio is identical and the speedup pattern transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "no chunks in VRAM" wins on a 12 GB card
&lt;/h2&gt;

&lt;p&gt;The reflexive instinct on a 12 GB card is to pack as much of the model as possible into VRAM and let the rest spill to RAM. That instinct is wrong for MoE.&lt;/p&gt;

&lt;p&gt;A dense 35B model wants every layer warm because every weight participates in every token. A MoE model is the opposite: of 128 experts in each layer, only 8 fire per token. The other 120 are dead weight that round trips for nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-ngl 99&lt;/code&gt; says "put every layer on the GPU." On a 12 GB card with a 20 GiB model, this should be impossible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--cpu-moe&lt;/code&gt; (&lt;a href="https://github.com/ggml-org/llama.cpp/discussions/22183" rel="noopener noreferrer"&gt;added to llama.cpp in mid-2025&lt;/a&gt; as the all-CPU shortcut for the more granular &lt;code&gt;--n-cpu-moe N&lt;/code&gt;) is the escape hatch: put every layer on the GPU &lt;strong&gt;except the MoE experts&lt;/strong&gt;, which stay on the CPU. Now what is on the GPU is the bandwidth-hungry part (attention, KV cache, layer norms, the router) and what is on the CPU is the sparse part (the experts themselves, which barely fire).&lt;/p&gt;

&lt;p&gt;The result on this machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VRAM used: 11.7 GB&lt;/strong&gt; (95% of 12 GB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation: 34.6 tok/s&lt;/strong&gt; (vs Ollama default 12.2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ollama's autoloader is not dumb — it figures out that ~58% of the model has to go on CPU and ~42% on GPU. But it splits the model the dense way: by layer. So you end up with half the attention paths on CPU (where bandwidth chokes) and half the experts on GPU (where they sit idle most of the time). It is the worst of both worlds, and it costs you 2.8x.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8l00n5fyp1cgcpdkfgj.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%2Fj8l00n5fyp1cgcpdkfgj.png" alt="n_cpu_moe sweep — putting all 48 layers of MoE experts on the CPU yields 34.6 tok/s on RTX 4070 12 GB" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The full offload sweep
&lt;/h2&gt;

&lt;p&gt;The flag people will reach for next is &lt;code&gt;--n-cpu-moe N&lt;/code&gt;, which lets you offload only N layers of experts to CPU and put the rest on GPU. The instinct is "well, GPU is faster, so put as many experts back on GPU as fit." This is also wrong, and the sweep shows by how much.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-ngl 99&lt;/code&gt; is fixed. Only N changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;--n-cpu-moe&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Expert layers on GPU&lt;/th&gt;
&lt;th&gt;tok/s (tg128)&lt;/th&gt;
&lt;th&gt;vs Ollama default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;48 (all experts on CPU)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;27.19&lt;/td&gt;
&lt;td&gt;2.2x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16.88&lt;/td&gt;
&lt;td&gt;1.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;15.29&lt;/td&gt;
&lt;td&gt;1.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;14.06&lt;/td&gt;
&lt;td&gt;1.2x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;12.85&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;11.71&lt;/td&gt;
&lt;td&gt;0.96x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monotonic decay. The moment you start pulling experts back onto the GPU, you steal VRAM from the parts that actually need bandwidth, and the whole pipeline slows down. By the time half the experts (24 layers) are back on GPU, you are slower than Ollama's automatic split.&lt;/p&gt;

&lt;p&gt;The reading is direct: the optimum for a 12 GB card on a 128-expert MoE is &lt;strong&gt;all experts on CPU&lt;/strong&gt;. Not "as many as fit." All of them.&lt;/p&gt;

&lt;p&gt;The bench command to reproduce, if you want to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./build/bin/llama-bench &lt;span class="nt"&gt;-m&lt;/span&gt; qwen35.gguf &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;-ncmoe&lt;/span&gt; 48 &lt;span class="nt"&gt;-n&lt;/span&gt; 128 &lt;span class="nt"&gt;-r&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-ncmoe 48&lt;/code&gt; is the bench-tool equivalent of &lt;code&gt;--n-cpu-moe 48&lt;/code&gt;, which in turn is the explicit form of &lt;code&gt;--cpu-moe&lt;/code&gt; for a 48-layer model. Same setting, three names. The flag landed in llama.cpp during a period of &lt;a href="https://huggingface.co/blog/Doctor-Shotgun/llamacpp-moe-offload-guide" rel="noopener noreferrer"&gt;active iteration on MoE offload semantics&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What VRAM tier maps to what
&lt;/h2&gt;

&lt;p&gt;You can almost read your sweet spot off the table above. Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8–10 GB cards&lt;/strong&gt; (RTX 4060 / 3070): full &lt;code&gt;--cpu-moe&lt;/code&gt;. You will not have headroom to put any experts on GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12 GB cards&lt;/strong&gt; (RTX 4070 / 3060 12 GB): full &lt;code&gt;--cpu-moe&lt;/code&gt;. The sweep above is yours. 34.6 tok/s is the realistic ceiling for Q4 35B-A3B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16 GB cards&lt;/strong&gt; (RTX 4060 Ti 16 GB / 4070 Ti SUPER): you can start putting a few expert layers back on GPU (N=44 → 4 layers on GPU) and gain a little, but only a little — the next regime gets dominated by &lt;a href="https://medium.com/@david.sanftenberg/gpu-poor-how-to-configure-offloading-for-the-qwen-3-235b-a22b-moe-model-using-llama-cpp-13dc15287bed" rel="noopener noreferrer"&gt;PCIe-bus expert transfers&lt;/a&gt; that eat the win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24 GB cards&lt;/strong&gt; (RTX 4090 / 3090): you can fit the model fully and skip this entire post. Lucky you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The crossover where "all on GPU" beats "experts on CPU" is somewhere around 24 GB for Q4 35B-A3B. Below that, the math says spread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that will tank your number
&lt;/h2&gt;

&lt;p&gt;The 34.6 tok/s above is not what you get by pasting the command. It is what you get by pasting the command &lt;strong&gt;after&lt;/strong&gt; clearing three traps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap 1: VRAM is not actually free.&lt;/strong&gt; WSL2 happily shares VRAM with Windows-side processes. If Edge has 200 MB of "hardware acceleration" stuck in VRAM, your attention layer fights for it and loses. Check with &lt;code&gt;nvidia-smi&lt;/code&gt; before benching. The number you want next to "Used" is under 500 MB before you start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap 2: Qwen thinking mode.&lt;/strong&gt; &lt;a href="https://huggingface.co/Qwen/Qwen3-30B-A3B" rel="noopener noreferrer"&gt;Qwen3.5 has a "thinking" mode&lt;/a&gt; that emits reasoning tokens before the answer. If you benchmark with a generic prompt, it will think for 400 tokens about "what is 2+2" and your tok/s number is meaningless. Either disable thinking via the system prompt or measure with prompts that bypass it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap 3: Quantization and build flags.&lt;/strong&gt; The 34.6 figure is Q4_K_M with a CUDA-enabled build. Q5_K_M will fall to roughly 28–30 tok/s on the same card because the experts get heavier per token. A CPU-only build of llama.cpp will obviously sit at single digits. If your number is off by 40%, check &lt;code&gt;nvidia-smi&lt;/code&gt; during inference — &lt;code&gt;llama-server&lt;/code&gt; should show 95%+ GPU utilization on the prompt and 30–60% on generation. If it shows 5%, you are running CPU-only without realizing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line worth memorizing
&lt;/h2&gt;

&lt;p&gt;For dense models, "put as much as possible on GPU" is correct. For sparse MoE on consumer GPUs, &lt;strong&gt;"put everything on GPU except the experts"&lt;/strong&gt; is correct, and the gap between those two heuristics is 2.8x on this card.&lt;/p&gt;

&lt;p&gt;The one-line version: the bottleneck on a 12 GB card running a 35B MoE is not parameters. It is bandwidth. The right partition puts the bandwidth-hungry part on the bandwidth-rich device, even when that means leaving 60% of the parameter count on the CPU.&lt;/p&gt;

&lt;p&gt;If you take one thing away from this post: run &lt;code&gt;llama-bench&lt;/code&gt; with &lt;code&gt;-ngl 99 -ncmoe 48 -n 128 -r 3&lt;/code&gt; on your card and write the number down. If it is more than 2x your Ollama default, the rest of your local-LLM tuning is variance. If it is less, your VRAM is not actually free.&lt;/p&gt;

&lt;p&gt;If you want the full data-driven engineering pattern behind this kind of measurement-first tuning — same logic applied to broader system harnesses, build pipelines, and agent loops — &lt;a href="https://kenimoto.dev/books/harness-engineering-guide?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=rtx-4070-cpu-moe" rel="noopener noreferrer"&gt;Harness Engineering Guide&lt;/a&gt; is the long form.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>performance</category>
      <category>ai</category>
      <category>hardware</category>
    </item>
    <item>
      <title>Barge-In Is the Voice-Agent Feature Nobody Benchmarks. I Added It and Lost 120ms.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/barge-in-is-the-voice-agent-feature-nobody-benchmarks-i-added-it-and-lost-120ms-4o7m</link>
      <guid>https://dev.to/kenimo49/barge-in-is-the-voice-agent-feature-nobody-benchmarks-i-added-it-and-lost-120ms-4o7m</guid>
      <description>&lt;p&gt;I have read more voice-agent benchmarks than I would like to admit. They all measure the same thing: how many milliseconds from "user stops talking" to "agent starts talking." Stack comparisons, P95 charts, the whole genre. Every one of them treats the conversation as a relay race where only one runner is moving at a time.&lt;/p&gt;

&lt;p&gt;Then I shipped barge-in: the ability for a user to talk over the agent and have it shut up gracefully. And I discovered the thing none of those benchmarks measure. Letting the user interrupt is not free. On my own pipeline, turning it on added 120ms to the exact latency number every chart obsesses over. Nobody benchmarks the cost of barge-in, because barge-in is the feature that makes your headline number worse, and no one wants to publish that.&lt;/p&gt;

&lt;p&gt;This is the field report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frg9mpac9j0a1ndf57suw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frg9mpac9j0a1ndf57suw.png" alt="Two latency timers compared: turn-taking latency at 800ms-plus, labeled " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What barge-in actually is
&lt;/h2&gt;

&lt;p&gt;Barge-in is the agent yielding the floor when the user starts speaking mid-response. It is the difference between a conversation and a press conference. Without it, the agent finishes its sentence no matter what you do, and you end up talking over a machine that cannot hear you over itself.&lt;/p&gt;

&lt;p&gt;Here is the part that surprised me. Every benchmark I had read measures a half-duplex world: the user talks, then the agent talks, cleanly alternating. Barge-in breaks that model entirely. To support it, the agent has to keep listening while it is speaking. That is full-duplex, and full-duplex is where the hidden cost lives.&lt;/p&gt;

&lt;p&gt;The numbers that get published are turn-taking latency: human conversation hands off in roughly 200-300ms, while most agents land somewhere between 800ms and 1500ms (&lt;a href="https://gradium.ai/content/turn-taking-voice-agents-vad" rel="noopener noreferrer"&gt;Gradium&lt;/a&gt;, &lt;a href="https://softcery.com/lab/ai-voice-agents-real-time-vs-turn-based-tts-stt-architecture" rel="noopener noreferrer"&gt;Softcery&lt;/a&gt;). Those are the numbers in the charts. The number nobody charts is how long it takes the agent to &lt;em&gt;stop&lt;/em&gt; once you cut in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I shipped, and the thing it broke
&lt;/h2&gt;

&lt;p&gt;My setup is a fairly ordinary cascade: WebRTC transport, streaming STT, an LLM, streaming TTS, with an orchestrator wiring the frames together. End-to-end I was sitting in a respectable place. Then the complaints came in, and they were the same complaints everyone gets: the agent steamrolls you, it answers before you finish, it keeps talking when you try to correct it.&lt;/p&gt;

&lt;p&gt;So I added barge-in. The mechanism is not exotic. While TTS is playing, you keep a VAD running on the inbound mic stream. The instant it fires, you duck the agent audio and decide whether to yield the turn. The common production move is to drop TTS gain by about 24dB the moment VAD fires, without killing the stream, so you can recover if it was a false alarm (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It worked. The agent stopped steamrolling. And my latency went up, because of the part I had not thought hard enough about: false barge-ins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naive barge-in is a disaster
&lt;/h2&gt;

&lt;p&gt;The first version fired on everything. A cough. A door. The user saying "mm-hm" to agree, which is not an interruption at all, it is a backchannel. Worst of all, it fired on the agent's own voice leaking back through the mic. The agent heard itself, decided someone was interrupting, and went silent. A machine startled by its own echo, like a dog barking at the mirror and then losing the staring contest.&lt;/p&gt;

&lt;p&gt;The fix for the echo problem is acoustic echo cancellation: feed the speaker output back as a reference signal, subtract it from the mic input, and you are left with just the human. That is table stakes for full-duplex and I will not relitigate it here.&lt;/p&gt;

&lt;p&gt;The fix for the cough-and-backchannel problem is where the latency went. You cannot trust a single VAD frame. Energy-based VAD does not know the difference between "I disagree, stop" and someone clearing their throat in a coffee shop. Background noise pushing energy above threshold is exactly the failure mode the field keeps naming (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;). So you add a guard. You require the interrupting speech to persist for a minimum duration before you commit to yielding.&lt;/p&gt;

&lt;p&gt;That guard is the 120ms. And it buys something real. A minimum-duration guard can cut the false-barge-in rate by 60-80%, but it adds roughly 200ms to the barge-in path (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;). I tuned mine tighter than that and landed at +120ms before my false-positive rate dropped under the 5% I was aiming for. The published target for barge-in is brutal in both directions: 95%+ accuracy, under 5% false positives, under 5% missed real interruptions (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;). You do not get there for free, and the currency you pay in is the same milliseconds your benchmark is bragging about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffy8bb6l8hoxvl7b50ohb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffy8bb6l8hoxvl7b50ohb.png" alt="The barge-in path as a flow: agent speaking, VAD fires on the mic, a minimum-duration guard that adds 120ms, then duck and yield at minus-24dB. Below, a tradeoff: no guard yields instantly but fires on coughs and echo, while the guard drops false barge-ins 60-80% at the cost of an interruption landing 120ms slower" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The two timers nobody puts on the same chart
&lt;/h2&gt;

&lt;p&gt;Here is what I think the benchmarks get structurally wrong. There is not one latency in a voice agent. There are two, and they pull in opposite directions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timer&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Direction barge-in pushes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Turn-taking latency&lt;/td&gt;
&lt;td&gt;User stops -&amp;gt; agent starts&lt;/td&gt;
&lt;td&gt;This is what every chart reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Barge-in latency&lt;/td&gt;
&lt;td&gt;User cuts in -&amp;gt; agent stops&lt;/td&gt;
&lt;td&gt;This is the one nobody reports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Turn-taking latency is the relay-race number. Barge-in latency is the interrupt-handling number, and the field is starting to put real targets on it: interruption response under 200ms, measured from user-speech onset to TTS suppression (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;). The trap is that these two timers fight. Make the agent quicker to yield and you generate more false stops. Add a guard to kill the false stops and you slow the yield. You are not optimizing a number. You are choosing a point on a tradeoff curve, and the benchmark that reports only the first timer is hiding the second axis entirely.&lt;/p&gt;

&lt;p&gt;The research framing I found most honest measures the minimum latency required to reach 90% barge-in accuracy, rather than reporting latency and accuracy as if they were independent (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;). That is the joint metric. That is what a barge-in benchmark should look like, and almost nobody publishes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the +120ms actually fits in the budget
&lt;/h2&gt;

&lt;p&gt;To be clear about scale: in a cascade, the latency that gets all the attention is the STT-to-LLM-to-TTS chain, which even at its fastest is a few hundred milliseconds of stacked work. The barge-in path is a separate budget. It runs in parallel, on the listening side, the whole time the agent is talking. The response chain and the listening path never touch.&lt;/p&gt;

&lt;p&gt;So the +120ms does not lengthen your response. It lengthens the &lt;em&gt;interruption&lt;/em&gt;. When a user cuts in, that is the delay before the agent goes quiet. And that delay has a much lower tolerance than response latency does. People forgive an agent that takes 600ms to answer. They do not forgive an agent that keeps talking for 600ms after they have clearly told it to stop, because at that point it is not slow, it is rude. The barge-in timer is the one your users feel as a personality flaw.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 2026 turn-taking models change
&lt;/h2&gt;

&lt;p&gt;The honest version of this story is that the brute-force guard is the old way, and the field has moved. The fix for "VAD is too dumb to tell a cough from a correction" is to stop using a bare energy threshold and use a model that understands turns.&lt;/p&gt;

&lt;p&gt;This is the shift everyone is making right now. The 2026 production stack is migrating from energy-threshold VAD toward dedicated turn-taking models that classify backchannel versus barge-in versus continued silence as a learned signal (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;). The named players:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deepgram Flux&lt;/strong&gt; does model-native end-of-turn detection using acoustic, semantic, and conversational context instead of silence thresholds, landing around 250ms end-of-turn and removing the need for a separate VAD-plus-endpointing stack (&lt;a href="https://deepgram.com/learn/introducing-flux-conversational-speech-recognition" rel="noopener noreferrer"&gt;Deepgram&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Krisp Turn Prediction v3&lt;/strong&gt; pushes end-of-turn latency below 200ms, and in May 2026 benchmarks its accuracy curve sat below LiveKit's built-in and Deepgram Flux's across the operating range (&lt;a href="https://krisp.ai/blog/voice-ai-turn-taking-interruption-prediction/" rel="noopener noreferrer"&gt;Krisp&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiveKit Agents&lt;/strong&gt; ships adaptive interruption handling at 86% precision and 100% recall, with the barge-in and backchannel-suppression logic living in the orchestrator, not the ASR model (&lt;a href="https://inworld.ai/resources/vapi-vs-pipecat-vs-livekit" rel="noopener noreferrer"&gt;Inworld&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point reframed the whole problem for me. Barge-in quality lives in your orchestrator, not your speech-to-text model. The model tells you what it heard; the orchestrator decides what to do about it, and that decision is the entire game (&lt;a href="https://www.assemblyai.com/blog/vapi-vs-pipecat-vs-livekit" rel="noopener noreferrer"&gt;AssemblyAI&lt;/a&gt;). I had been tuning the wrong layer for a week.&lt;/p&gt;

&lt;p&gt;A semantic turn detector earns back most of my 120ms because it does not need a long duration guard. It can tell that "actually—" is an interruption and "yeah, mm-hm" is not, from the prosody and the words, not from how long the sound lasted. The guard was a crutch for a dumb VAD. A model that understands the turn lets you commit to the decision sooner with the same accuracy, which is the only way to move down the tradeoff curve instead of along it. Combining audio and text this way is what closes the gap to roughly 300ms without cutting users off mid-thought (&lt;a href="https://futureagi.com/blog/voice-ai-barge-in-turn-taking-2026/" rel="noopener noreferrer"&gt;Future AGI&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself before shipping it
&lt;/h2&gt;

&lt;p&gt;Three things rearranged in my head, and they are the things I wish a benchmark had told me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure the second timer.&lt;/strong&gt; If your dashboard only has turn-taking latency, you are flying with one instrument. Add barge-in latency, measured from user-speech onset to TTS suppression, and watch them as a pair. The moment you start optimizing one in isolation, you are quietly wrecking the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The guard is a tax, not a feature.&lt;/strong&gt; A minimum-duration guard is the cheapest way to stop false barge-ins and the most expensive way to feel responsive. It is fine as a first pass. It is a bad place to live. If you are still paying a 120-200ms guard tax six months in, you have not solved barge-in, you have postponed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Barge-in is an orchestrator problem.&lt;/strong&gt; I spent days assuming a better STT model would fix my interruptions. It would not have. The yield-or-hold decision lives above the model, and that is where the engineering actually is. Pick your transport and orchestrator for how they handle interruption events, because that is the layer your users will judge.&lt;/p&gt;

&lt;p&gt;The number nobody puts on the chart is the number your users feel first. An agent that answers fast but will not stop talking is not a fast agent. It is a fast bulldozer. I would rather lose 120ms and have it know when to shut up.&lt;/p&gt;




&lt;p&gt;I pulled the latency-budget framing and the cascade anatomy behind this from my book &lt;a href="https://kenimoto.dev/books/voice-ai-300ms-ux?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=voice-barge-in-120ms" rel="noopener noreferrer"&gt;The 300ms Voice-AI UX Problem&lt;/a&gt;, which is where I worked out why turn-taking is the part of the budget that does not behave like the rest of it. This post is what happened when I stopped reading about turn gaps and started measuring the one in the other direction.&lt;/p&gt;

</description>
      <category>voiceai</category>
      <category>ai</category>
      <category>ux</category>
      <category>webrtc</category>
    </item>
    <item>
      <title>When the Free Executor Cost More: 40 Trials on Opus + Local Qwen Ended Up the Most Expensive Cloud Arm</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Sat, 27 Jun 2026 23:42:17 +0000</pubDate>
      <link>https://dev.to/kenimo49/when-the-free-executor-cost-more-40-trials-on-opus-local-qwen-ended-up-the-most-expensive-cloud-4kpe</link>
      <guid>https://dev.to/kenimo49/when-the-free-executor-cost-more-40-trials-on-opus-local-qwen-ended-up-the-most-expensive-cloud-4kpe</guid>
      <description>&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%2F37dw9i6ev8y3vzkpdd7a.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%2F37dw9i6ev8y3vzkpdd7a.png" alt="Per-arm cumulative token volume. Even with Qwen's tokens billed at $0, the Opus + Qwen arm (B) has Opus reading 1.4–5.3× more tokens than Opus solo, because the orchestrator re-reads the executor's returned summaries on every iteration." width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Use a strong model to orchestrate, a cheap model to execute." This is now the default cost-aware recipe for agentic coding.&lt;/p&gt;

&lt;p&gt;I believed it and ran the experiment. Opus 4.7 as the orchestrator, locally-hosted Qwen 3.5-9B (zero token cost) as the executor. This should beat running Opus alone on cost. Has to.&lt;/p&gt;

&lt;p&gt;It did the opposite.&lt;/p&gt;

&lt;p&gt;The supposedly "free" configuration (Opus + Qwen) came out as the &lt;strong&gt;most expensive&lt;/strong&gt; cloud arm on all three of the code-repair tasks I ran. Higher than Opus solo. Higher than Opus + Haiku. And of course much higher than Haiku solo. As someone who actually built a GPU PC believing "local means cheap," I find this somewhat inconvenient.&lt;/p&gt;

&lt;p&gt;I wrote up the 40 trials worth of numbers and the mechanism as a paper, archived on Zenodo: &lt;a href="https://doi.org/10.5281/zenodo.20978074" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.20978074&lt;/a&gt; / &lt;a href="https://github.com/kenimo49/free-executor-paradox" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post walks through what happened across the 40 trials and why "free" turned out to be the most expensive option — all from real measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;40 trials × 4 configurations × 3 tasks, judged by a deterministic harness (&lt;code&gt;mypy + ruff + pytest&lt;/code&gt; exit codes only). No LLM-as-judge anywhere in the loop.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Opus orchestrates + Qwen executes&lt;/strong&gt; is the most expensive cloud arm on every task. More expensive than Opus solo.&lt;/li&gt;
&lt;li&gt;The cause is not the executor's tokens — it's the &lt;strong&gt;orchestrator's prompt-cache re-reads&lt;/strong&gt;. Opus keeps reading Qwen's returned summaries on every turn, and its own input volume grows to 1.4–5.3× that of Opus running alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Haiku solo&lt;/strong&gt; is 5.5× cheaper than Opus solo on the largest task — but fails 25% of the time within the per-arm iteration cap. Within cloud-only options, &lt;strong&gt;Opus + Haiku&lt;/strong&gt; is the most balanced.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the intuition "the executor's tokens are free, therefore this is cheap" feels obvious, this post is about why that intuition breaks.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The four arms
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;orchestrator&lt;/th&gt;
&lt;th&gt;executor&lt;/th&gt;
&lt;th&gt;role split&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Opus 4.7&lt;/td&gt;
&lt;td&gt;(solo)&lt;/td&gt;
&lt;td&gt;one model does everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Opus 4.7&lt;/td&gt;
&lt;td&gt;Qwen 3.5-9B (local / Ollama)&lt;/td&gt;
&lt;td&gt;Opus plans + verifies, Qwen edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Opus 4.7&lt;/td&gt;
&lt;td&gt;Haiku 4.5 (Anthropic SDK sub-loop)&lt;/td&gt;
&lt;td&gt;Opus plans + verifies, Haiku edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;td&gt;(solo)&lt;/td&gt;
&lt;td&gt;one cheap model does everything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All four arms use the Anthropic SDK with the same tool surface: &lt;code&gt;str_replace_editor&lt;/code&gt; (view/create/str_replace/insert) and a &lt;code&gt;bash&lt;/code&gt; tool with a 120-second timeout. The orchestrator arms (B, C) get one extra tool: &lt;code&gt;delegate_to_executor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Anthropic prompt caching is enabled identically on every call — &lt;code&gt;system&lt;/code&gt;, tool definitions, and the most recent user message are marked with &lt;code&gt;cache_control: ephemeral&lt;/code&gt;. No &lt;code&gt;temperature&lt;/code&gt; or &lt;code&gt;seed&lt;/code&gt; is set, so trial-to-trial variance reflects sampling noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three tasks
&lt;/h3&gt;

&lt;p&gt;All three operate on the &lt;a href="https://github.com/tiangolo/typer" rel="noopener noreferrer"&gt;typer&lt;/a&gt; repository at commit &lt;code&gt;b210c0e&lt;/code&gt; (v0.26.8, MIT license). Each trial starts with &lt;code&gt;git checkout -- . &amp;amp;&amp;amp; git clean -fd&lt;/code&gt; to restore the base state.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;T1 — Breakage recovery&lt;/strong&gt;: 25 errors injected via AST (10 mypy + 10 ruff + 5 pytest collection failures). The agent has to return the harness to fully green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;T2 — Refactor&lt;/strong&gt;: Move &lt;code&gt;get_params_from_function&lt;/code&gt; from &lt;code&gt;typer/utils.py&lt;/code&gt; to a new module &lt;code&gt;typer/_param_extractor.py&lt;/code&gt;. Update every import site. All tests still passing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;T3 — Feature-add&lt;/strong&gt;: Implement &lt;code&gt;get_version_banner(prefix, uppercase) -&amp;gt; str&lt;/code&gt;, re-export from &lt;code&gt;typer/__init__.py&lt;/code&gt;, and pass a SHA-256-fingerprinted test file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The judge
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mypy + ruff check + pytest&lt;/code&gt; — exit code 0 = success, anything else = failure. Per-task verifiers (&lt;code&gt;verify-T2.sh&lt;/code&gt; / &lt;code&gt;verify-T3.sh&lt;/code&gt;) add structural checks (function actually moved to the new module, fingerprinted test unmodified, etc.).&lt;/p&gt;

&lt;p&gt;No LLM is ever asked "is this OK?". The judgment is deterministic and reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results (success-only medians, n=3 per cell)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;task&lt;/th&gt;
&lt;th&gt;n_succ/total&lt;/th&gt;
&lt;th&gt;wall (s)&lt;/th&gt;
&lt;th&gt;iters&lt;/th&gt;
&lt;th&gt;cost ($)&lt;/th&gt;
&lt;th&gt;success rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A Opus solo&lt;/td&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;253&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;1.74&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A Opus solo&lt;/td&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;233&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;1.11&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A Opus solo&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.17&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B Opus+Qwen&lt;/td&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;484&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;2.27&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B Opus+Qwen&lt;/td&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;1.38&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B Opus+Qwen&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;348&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C Opus+Haiku&lt;/td&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.67&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C Opus+Haiku&lt;/td&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;275&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.92&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C Opus+Haiku&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D Haiku solo&lt;/td&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;758&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D Haiku solo&lt;/td&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;td&gt;507&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D Haiku solo&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.08&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bold = per-column winner. Total Anthropic spend across 40 trials: $35.98 — cheap for a paper.&lt;/p&gt;

&lt;p&gt;The row worth staring at is arm B. On all three tasks, its &lt;code&gt;cost ($)&lt;/code&gt; is the cloud-arm worst ($2.27 / $1.38 / $0.42). Qwen's tokens cost zero. Opus + Qwen is more expensive than Opus alone anyway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkfktt99fl4477ug26i7.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%2Fnkfktt99fl4477ug26i7.png" alt="T3 (feature-add) Pareto frontier. X-axis = cost in USD, Y-axis = wall time in seconds. Arm B (orange) is dominated by both arm A (red) and arm C (green) — it is neither cheaper nor faster." width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Free" Cost the Most
&lt;/h2&gt;

&lt;p&gt;Compare Opus-side token consumption (&lt;code&gt;input + cache_read_input&lt;/code&gt;) across arms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm role&lt;/th&gt;
&lt;th&gt;T1 (Opus-side in + cache_r)&lt;/th&gt;
&lt;th&gt;T2&lt;/th&gt;
&lt;th&gt;T3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A (Opus solo)&lt;/td&gt;
&lt;td&gt;534,586&lt;/td&gt;
&lt;td&gt;226,474&lt;/td&gt;
&lt;td&gt;13,320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B (Opus + Qwen)&lt;/td&gt;
&lt;td&gt;733,142&lt;/td&gt;
&lt;td&gt;313,914&lt;/td&gt;
&lt;td&gt;62,864&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C (Opus + Haiku)&lt;/td&gt;
&lt;td&gt;421,622&lt;/td&gt;
&lt;td&gt;159,640&lt;/td&gt;
&lt;td&gt;44,016&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;B-over-A ratio (Opus-side only): &lt;strong&gt;1.38× on T1, 1.39× on T2, 5.26× on T3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Qwen's tokens are free. But Opus itself is reading 1.4–5.3× more tokens than it would running alone.&lt;/p&gt;

&lt;p&gt;The mechanism. When Opus calls &lt;code&gt;delegate_to_executor&lt;/code&gt;, Qwen returns a stdout summary (capped at 4000 chars in my implementation). That summary lands in Opus's context. Anthropic prompt caching marks the most recent message for &lt;code&gt;cache_write&lt;/code&gt;, and the next turn reads it back via &lt;code&gt;cache_read&lt;/code&gt;. Across 30–80 turns, Opus ends up reading the "what Qwen did" summary over and over and over.&lt;/p&gt;

&lt;p&gt;Each re-read is billed at the &lt;code&gt;cache_read&lt;/code&gt; rate ($1.50/M token = 10% of Opus input). The executor is free; the orchestrator is not. Which sounds obvious in hindsight, except the word "free" in a sentence tends to short-circuit human reasoning. Mine, anyway.&lt;/p&gt;

&lt;p&gt;Stated correctly: &lt;strong&gt;the orchestrator's cost is proportional to how many times it re-reads the executor's returned summaries, not to the executor's raw token count.&lt;/strong&gt; This reads more like a middle-management observation than an LLM finding, but the data says what it says.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuub55q2p2e215lfijer7.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%2Fuub55q2p2e215lfijer7.png" alt="Free-Executor Paradox mechanism. Orchestrator (Opus) delegates to Executor (Qwen) via delegate_to_executor → Executor returns a stdout summary → the summary accumulates in the Orchestrator's context and gets re-read via cache_read on every subsequent turn. Even with Qwen's tokens free, the Orchestrator's cache_read keeps accumulating." width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why T3 Blew Up to 5.3×
&lt;/h2&gt;

&lt;p&gt;The most extreme case is T3, the smallest task — about 6 iterations.&lt;/p&gt;

&lt;p&gt;Same mechanism, different ratio. The base context (system + tools + initial prompt) is &lt;code&gt;cache_write&lt;/code&gt;-ed once on the first turn and &lt;code&gt;cache_read&lt;/code&gt; cheaply thereafter. On long tasks (T1, T2), that base is a small fraction of the cumulative input. On a short task, it's a big fraction. So "base re-read every turn + executor summary re-read every turn" overhead dominates everything else, and T3's B/A ratio spikes to 5.3×.&lt;/p&gt;

&lt;p&gt;Conversely, arm C (Opus + Haiku) has a &lt;em&gt;smaller&lt;/em&gt; &lt;code&gt;cache_read&lt;/code&gt; footprint than arm A on T1 and T2 (0.79× and 0.70× of A). Haiku does substantive work that Opus would have otherwise had to do itself, and the substance translates into useful summaries instead of dead weight. Which is the opposite end of the Qwen-summary-bloat story.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Orchestration Would Win (cases I deliberately excluded)
&lt;/h2&gt;

&lt;p&gt;The "strong orchestrator + cheap executor" recipe falters in iterative tool-loops because, over dozens of turns, the orchestrator's &lt;code&gt;cache_read&lt;/code&gt; becomes the dominant cost line. One-shot routing has no such problem.&lt;/p&gt;

&lt;p&gt;The experiment was, in that sense, designed against arm B:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Executor returns are free-form&lt;/strong&gt; (Qwen stdout summary up to 4000 chars). If you constrain returns to "one structured diff and nothing else," the orchestrator's accumulated context shrinks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks are sequential&lt;/strong&gt; (T1/T2/T3 cannot be parallelized within a single trial). Tasks where the orchestrator can dispatch "go edit these three places at once" might pay for orchestration overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Re-running arm B with tightly-bounded executor returns is the next experiment on my list. I expect T3 to invert. T1 is harder to call.&lt;/p&gt;

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

&lt;p&gt;Sitting with these numbers, here is how my own agentic coding setup changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;For tasks that finish in a handful of iterations, Opus solo is the cheapest cloud option.&lt;/strong&gt; T3: $0.17, 69 seconds, 6 iterations — cloud-best. "Opus is expensive" is a one-shot framing. Across an iterative loop, Opus's per-iteration efficiency pays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For tasks that need dozens of iterations, the model with the lowest per-iteration cost wins on dollars.&lt;/strong&gt; T1: Haiku solo at $0.30 is 5.5× cheaper than the cheapest cloud arm. But it fails 25% of the time, so retry-adjusted expected cost narrows the gap to 4.2×.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For cloud-only setups, Opus + Haiku is the most balanced.&lt;/strong&gt; Ties Opus solo on T1, wins T2 on cost, narrowly loses to Opus solo on T3. The safe pick if you don't want Haiku-solo's failure rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you're going to use a local Qwen "for free," constrain the executor return size structurally.&lt;/strong&gt; Free-form stdout returns just shift the cost to the orchestrator's &lt;code&gt;cache_read&lt;/code&gt; line.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Strong + cheap" composition has a narrower design surface than it seems. Unless you also specify what and how much the executor is allowed to return, you regenerate the "orchestrator-becomes-expensive" pattern. I regenerated it three times suspecting measurement error before finally accepting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;The honesty section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;n=3 per cell.&lt;/strong&gt; Mann-Whitney U p-values use a normal approximation where 0.050 is the small-sample floor — it means "as different as this sample size can show." Trust the Cliff's delta effect sizes; don't over-read p-value differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All three tasks are on the typer repo.&lt;/strong&gt; Generalization needs other codebases. The harness, breakage injector, runner, and analysis are all MIT-licensed in the repo, so reproducing this on your own codebase is cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The orchestrator system prompt is asymmetric.&lt;/strong&gt; It instructs "do not edit directly, delegate instead." This mirrors a real deployment shape but is a real confounder in the results.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Tested on Ubuntu 22.04 with Python 3.10+, &lt;code&gt;uv&lt;/code&gt; 0.4+, and &lt;code&gt;anthropic&lt;/code&gt; Python SDK 0.83+. Arm B uses Ollama 0.4+ running &lt;code&gt;qwen3.5:9b&lt;/code&gt;. If you skip arm B, Ollama is not needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kenimo49/free-executor-paradox
&lt;span class="nb"&gt;cd &lt;/span&gt;free-executor-paradox
&lt;span class="c"&gt;# run arm A on T3, one trial&lt;/span&gt;
python scripts/runners/runner.py &lt;span class="nt"&gt;--arm&lt;/span&gt; A &lt;span class="nt"&gt;--task&lt;/span&gt; T3 &lt;span class="nt"&gt;--trial&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo README and paper PDF have the full reproducibility setup — harness, breakage injection, runner, and analysis scripts are all included.&lt;/p&gt;

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

&lt;p&gt;The cost debate around agentic coding tends to fixate on what the executor costs per token. The dominant term is actually what the orchestrator re-reads, and how often. Qwen here is just one instance of the pattern — every "free local executor" that comes next will hit the same issue. Free executor tokens don't make orchestrator &lt;code&gt;cache_read&lt;/code&gt; free.&lt;/p&gt;

&lt;p&gt;I wrote it up as a paper because numbers are harder to argue with than vibes. The most satisfying outcome would be someone replying "got the same thing on my codebase" or "actually got the opposite, here's why."&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paper (Zenodo)&lt;/strong&gt;: &lt;a href="https://doi.org/10.5281/zenodo.20978074" rel="noopener noreferrer"&gt;When Free Executors Cost More — DOI 10.5281/zenodo.20978074&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code + data + harness&lt;/strong&gt;: &lt;a href="https://github.com/kenimo49/free-executor-paradox" rel="noopener noreferrer"&gt;https://github.com/kenimo49/free-executor-paradox&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Release&lt;/strong&gt;: &lt;a href="https://github.com/kenimo49/free-executor-paradox/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;v1.0.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Let Claude Code Run a Month of My Business Books. It Reconciled 200 Transactions and Miscategorized 11.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-let-claude-code-run-a-month-of-my-business-books-it-reconciled-200-transactions-and-513d</link>
      <guid>https://dev.to/kenimo49/i-let-claude-code-run-a-month-of-my-business-books-it-reconciled-200-transactions-and-513d</guid>
      <description>&lt;p&gt;I run a few small businesses, which means once a month I sit down with a bank export and an accounting platform and turn a pile of transactions into something a tax office will accept. It is the part of self-employment nobody warns you about. The coding is fun. The books are not.&lt;/p&gt;

&lt;p&gt;So this month I handed the books to Claude Code and watched.&lt;/p&gt;

&lt;p&gt;The result, up front: it reconciled 200 transactions and miscategorized 11. That is a 94.5% hit rate, which sounds great until you remember that the 11 wrong ones were the difference between a clean filing and a letter from the tax office. This is the story of where the agent shined, where it quietly lied to me, and the exact line I now draw between what it runs and what I sign.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwzo2kttf4n4swjp2sq6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwzo2kttf4n4swjp2sq6.png" alt="One month of business books run by a coding agent, then audited by hand: 200 transactions reconciled, 11 miscategorized. 94.5% right is great for a draft, a disaster for a tax filing." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: stop asking, start building
&lt;/h2&gt;

&lt;p&gt;My first instinct was the obvious one. Paste a CSV into a chat and say "categorize these." I did that for about ten rows before I stopped, because it was the wrong shape of work.&lt;/p&gt;

&lt;p&gt;A throwaway categorization is a thing you ask for once and then have to babysit forever. What I actually wanted was a process I could re-run next month with a different CSV and trust a little more each time. So I told Claude Code to build the bookkeeping pipeline, not to do the bookkeeping.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it sounds. When you ask an agent to &lt;em&gt;build the thing that does the work&lt;/em&gt;, you get a script you can read, a set of rules you can correct, and an audit trail you can point at later. When you ask it to &lt;em&gt;do the work&lt;/em&gt;, you get an answer and a shrug. One is an asset. The other is a chore you now share with a robot.&lt;/p&gt;

&lt;p&gt;The pipeline it wrote was unglamorous and exactly right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the month's transactions from my accounting platform over an MCP connection.&lt;/li&gt;
&lt;li&gt;Match each bank row to a receipt or invoice by amount and date.&lt;/li&gt;
&lt;li&gt;Propose a category for every row, with a one-line reason.&lt;/li&gt;
&lt;li&gt;Flag anything it was not sure about instead of guessing silently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 is the one that saved me. More on that in a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the agent earned its keep
&lt;/h2&gt;

&lt;p&gt;The reconciliation itself was genuinely good. Matching 200 bank rows against receipts is the kind of tedious pattern-matching that humans are bad at precisely because it is boring. You zone out around row 40 and start rubber-stamping. The agent does not zone out.&lt;/p&gt;

&lt;p&gt;It correctly handled the cases I expected to trip it: a subscription that renewed on a slightly different day, a refund that showed up as a negative line, a vendor whose name on the bank statement bore no resemblance to the name on the invoice. For the bulk of the month, "AWS-style charge goes to infrastructure, coffee receipt goes to meetings" was handled without me touching anything.&lt;/p&gt;

&lt;p&gt;This is not a niche experience anymore. A January 2026 Deloitte study found 63% of finance organizations have fully deployed AI somewhere in their operations, and the pattern that keeps winning is the boring one: let the model categorize at volume, then have a human review the output. Machines do the reading, people do the signing. I arrived at the same split independently, which I choose to read as validation rather than as me being unoriginal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbwyidj8cd7exnj40jh3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbwyidj8cd7exnj40jh3.png" alt="Where I let the agent run versus where I keep my hands on the wheel: the agent pulls rows over MCP, matches receipts, drafts categories, and flags ambiguity; I review flagged rows, spot-check confident ones, fix tax-relevant categories, and own what gets filed." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 11 it got wrong
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable part. The 11 mistakes were not random noise. They clustered in three places, and all three are places where the agent had no way to know what it did not know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intent it couldn't see (5 rows).&lt;/strong&gt; A laptop I bought is a business expense if I use it for work and a personal purchase if I do not. The receipt looks identical either way. The agent categorized every device as a business asset because that is the statistically likely call, and for two of them it was wrong. No amount of context in the CSV would have told it otherwise. That information lives in my head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules it didn't have (4 rows).&lt;/strong&gt; Tax categories are not universal logic; they are local law. A meal with a client and a meal alone are deductible to different degrees depending on rules the agent was never given. It made a reasonable, confident, wrong guess. Confidence is the dangerous part. A wrong answer delivered with a hedge is easy to catch. A wrong answer delivered cleanly slides right through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge cases that needed a human (2 rows).&lt;/strong&gt; A single payment that covered two unrelated things, split across categories. The agent picked one. A person would have asked.&lt;/p&gt;

&lt;p&gt;Notice what is not on this list: arithmetic. It never added wrong, never lost a row, never double-counted. The failures were all judgment, not math. Which is the whole point. The agent is a tireless clerk, not an accountant, and the moment I treated it like an accountant was the moment it would have cost me money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line I now draw
&lt;/h2&gt;

&lt;p&gt;After this month I have a rule, and it is not "trust the agent" or "don't trust the agent." It is narrower than that.&lt;/p&gt;

&lt;p&gt;The agent runs everything that is high-volume and low-judgment: pulling data, matching receipts, drafting categories, flagging doubt. I personally sign off on everything that is low-volume and high-stakes: every flagged row, a spot-check of the confident ones, and every category that touches a tax outcome.&lt;/p&gt;

&lt;p&gt;The flagging mechanism is what makes this tractable. Because I told the agent to surface its own uncertainty rather than bury it, my review was not "re-check 200 rows." It was "check the 18 it wasn't sure about, then sample the rest." Seven of the 11 errors were already sitting in its own flagged pile. The other four I caught on the sample. That is the difference between an agent that helps and an agent that just moves the work somewhere you can't see it.&lt;/p&gt;

&lt;p&gt;One thing I will say loudly, because the internet is full of people who won't: the screenshot-friendly version of this post would be "AI did my taxes in an afternoon." That version is a lie of omission. The honest version is "AI did 94.5% of my taxes and I did the 5.5% that could get me audited." The second one is less viral and considerably more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you to copy
&lt;/h2&gt;

&lt;p&gt;If you want to try this on your own books, three things carried the result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build the pipeline, don't request the answer.&lt;/strong&gt; A script you can read and correct beats a chat reply you have to trust. Next month you re-run it; you don't re-explain it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make uncertainty a first-class output.&lt;/strong&gt; An agent that flags its own doubt turns a 200-row audit into an 18-row one. Telling it to never guess silently was the single most useful instruction I gave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep judgment on your side of the line.&lt;/strong&gt; Volume to the machine, stakes to the human. Intent, local law, and genuine edge cases are not data problems you can prompt your way out of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spent years learning that the bottleneck in any process is rarely the part everyone optimizes. With bookkeeping, everyone wants to automate the data entry. The data entry was never the hard part. The hard part is the eleven rows that need a human who knows what the business actually did, and an agent that is honest enough to point at them.&lt;/p&gt;

&lt;p&gt;I write more about this kind of human-and-agent division of labor in &lt;a href="https://kenimoto.dev/books/claude-code-mastery?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=claude-code-bookkeeping" rel="noopener noreferrer"&gt;Claude Code Mastery&lt;/a&gt;, where a full chapter goes into using coding agents for financial and business work without handing over the parts that bite.&lt;/p&gt;

&lt;p&gt;Next month the agent runs the books again. I will still read every flagged row. Let's keep it interesting.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AI Citations Have a Half-Life. I Tracked Mine for 9 Weeks and Watched Them Decay.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Tue, 23 Jun 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/ai-citations-have-a-half-life-i-tracked-mine-for-9-weeks-and-watched-them-decay-3f0k</link>
      <guid>https://dev.to/kenimo49/ai-citations-have-a-half-life-i-tracked-mine-for-9-weeks-and-watched-them-decay-3f0k</guid>
      <description>&lt;p&gt;I have written about measuring AI citations more times than I have written about my own family, which says something I would rather not examine. KPIs, tracker comparisons, how broad my citation footprint is. All of it was a snapshot: how many engines cite me &lt;em&gt;today&lt;/em&gt;. It took me an embarrassingly long time to ask the obvious follow-up. Not "how many," but "how long."&lt;/p&gt;

&lt;p&gt;So I started logging citations weekly instead of once. Same prompts, same five engines, every Monday morning, for nine weeks. The result is the thing nobody screenshots on LinkedIn: a citation is not a trophy you win and keep. It is a perishable good. Mine peaked around week three and then lost more than half its value, and the strangest part is that my Google search traffic for the exact same pages did not budge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xwwlbzbt2ai6uktry0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xwwlbzbt2ai6uktry0a.png" alt="A bar chart of AI citation rate rising to a week-three peak then decaying to about half, with a flat dashed line for Google Search Console clicks across all nine weeks" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually measured
&lt;/h2&gt;

&lt;p&gt;I had three articles that consistently got cited across ChatGPT, Perplexity, Gemini, Claude, and Brave AI. I picked those three because a citation that only fires once is noise, and I wanted signal I could track over time.&lt;/p&gt;

&lt;p&gt;Every Monday I ran the same 30 prompts, three times each, and logged how many returned one of my three pages as a clickable citation. I also pulled the weekly Google Search Console clicks for those exact URLs, so I had two curves drawn on the same calendar.&lt;/p&gt;

&lt;p&gt;One curve is AI citations. The other is plain old search traffic. I expected them to roughly track each other. They did not even rhyme.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decay curve
&lt;/h2&gt;

&lt;p&gt;Here is what nine weeks looked like, normalized so the peak week is 100.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Week:        1    2    3    4    5    6    7    8    9
AI cites:   48   82  100   91   70   54   47   44   46
GSC clicks: 88   92   95  100   97   99   96   98   97
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI citation line climbs for three weeks, tops out, and then slides down a ramp until it settles at roughly half its peak. The Google line is a heartbeat monitor on a calm patient: it wanders inside a narrow band and never does anything dramatic.&lt;/p&gt;

&lt;p&gt;If I fit a half-life to the decay portion, my citations lost 50% of their peak rate in about four to five weeks. That lined up unnervingly well with what other people are measuring out loud right now. One platform-by-platform study put the median AI citation &lt;a href="https://authoritytech.io/curated/ai-citation-half-life-platform-refresh-playbook-2026" rel="noopener noreferrer"&gt;half-life at 4.5 weeks&lt;/a&gt;, with ChatGPT churning fastest at about 3.4 weeks and Perplexity holding longest near 5.8. Another found that &lt;a href="https://machinerelations.ai/research/ai-citation-decay-how-brands-lose-visibility-over-time" rel="noopener noreferrer"&gt;AI-cited domains turn over 40 to 60 percent every month&lt;/a&gt;. I was not looking at a quirk of my tiny blog. I was looking at the weather.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the two clocks disagree
&lt;/h2&gt;

&lt;p&gt;The reason the curves diverge is that they are pulling from different places.&lt;/p&gt;

&lt;p&gt;Google's organic ranking for an established page is sticky. The page earned its position over months, the ranking signals are slow-moving, and one week of nothing-in-particular does not dislodge it. That is the heartbeat line.&lt;/p&gt;

&lt;p&gt;AI citations are pulled from a live index at answer time, and that index is biased toward fresh material. Industry measurements in 2026 keep landing on the same shape: roughly &lt;a href="https://salespeak.ai/aeo-news/content-freshness-ai-search" rel="noopener noreferrer"&gt;half of all AI-cited content is under 13 weeks old&lt;/a&gt;, and pages updated within the last 30 days earn several times more citations than older ones. One freshness study even found ChatGPT cites pages that are, on average, &lt;a href="https://apiserpent.com/blog/freshness-wins-chatgpt-citation-study" rel="noopener noreferrer"&gt;over a year newer&lt;/a&gt; than the URLs Google ranks organically for the same query. My three pages were great answers in March. By May they were still great answers, but the engine had three newer "great answers" to choose from, and it is graded on recency, not on loyalty to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgl8po7aijtcnnjmvirq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgl8po7aijtcnnjmvirq1.png" alt="Two cards comparing the slow Google clock — about 0% drop in clicks over nine weeks, ranking is sticky — against the fast AI clock — a 4.5-week citation half-life where roughly half of cited pages are under 13 weeks old" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the decay is not a quality problem. My pages did not get worse. The pool they compete in got younger.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where I tried to fight it
&lt;/h2&gt;

&lt;p&gt;Naturally, my first instinct was to refresh the pages and reset the clock. The freshness research says a substantive update (a new stat, a corrected claim, a visible timestamp change) is enough to &lt;a href="https://authoritytech.io/curated/ai-citation-half-life-platform-refresh-playbook-2026" rel="noopener noreferrer"&gt;re-trigger the freshness signal&lt;/a&gt;. So in week seven I rewrote the intro of my best-performing page, added a 2026 data point, and bumped the dateline.&lt;/p&gt;

&lt;p&gt;You can see the result in the table. Week eight ticked from 47 to 44, then week nine recovered to 46. Within the noise. A genuine nothing.&lt;/p&gt;

&lt;p&gt;I want to be honest about that, because the tidy version of this post would have the refresh save the day in a triumphant week-nine spike. It didn't. One refresh of one page over two weeks is not enough signal to conclude the tactic failed, but it is enough to stop me from promising you it works. What I changed was small, and the engines treated it as small. The lesson I am taking is that a cosmetic timestamp bump is not a refresh. The studies that show refreshes working describe &lt;em&gt;substantive&lt;/em&gt; rewrites, and "I added one sentence" is the SEO equivalent of changing your shirt and calling it a new wardrobe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you write about LLMO
&lt;/h2&gt;

&lt;p&gt;Three things rearranged in my head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A citation count is a flow, not a stock.&lt;/strong&gt; I had been treating "I am cited by five engines" like money in a bank account. It is closer to water in a leaky bucket. If you measure once and frame it, you are reading the level at one instant and assuming it holds. It does not hold. The only honest version of the metric is a rate over time, which means you have to measure on a cadence or you are measuring nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The refresh cadence is the actual job.&lt;/strong&gt; If half your citation value evaporates in roughly a month, then the maintenance schedule matters more than the next new post. I spent six months optimizing the title and schema of pages at the moment of publication, which is the one moment the freshness clock is on my side anyway. The hard part is week six, when the clock has turned against me and the only move is real work on an old page. And the cadence is not one-size-fits-all: the platform studies suggest refreshing ChatGPT-facing content roughly biweekly while Perplexity tolerates a six-week cycle, which means a single refresh calendar is already a compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO and AEO are not the same investment.&lt;/strong&gt; They feel adjacent because they both start with a URL and a query. But my data shows them moving on different timescales for the same pages, which means a single content calendar optimized for one is probably mistimed for the other. Search rewards the durable asset. AI citation rewards the recently-touched asset. Those are not the same content strategy wearing two hats.&lt;/p&gt;

&lt;p&gt;For the framework side of this, where the goal is a citation-maintenance loop instead of a one-time launch checklist, the &lt;a href="https://llmoframework.com/" rel="noopener noreferrer"&gt;LLMO Framework&lt;/a&gt; splits its pillars into Citability (do you get cited at all) and the Authority and Coherence signals (do you stay cited). Its refresh-strategy guidance is the part I had ignored. Nine weeks of staring at a decay curve made me realize I had spent all my effort on the first pillar and almost none on the rest. Getting cited is a launch problem. Staying cited is a retention problem, and retention is where the freshness clock lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing now
&lt;/h2&gt;

&lt;p&gt;I turned the weekly log into a standing job instead of a one-time experiment. Every Monday the same 30 prompts run, the same three URLs get checked, and the number goes into a spreadsheet next to the GSC clicks. When a page's citation rate drops below half its peak, it goes on a refresh list, and "refresh" now means a real new section or a new dataset, not a dateline nudge.&lt;/p&gt;

&lt;p&gt;I am also going to stop reporting my citation footprint as a single proud number. "Five engines cite me" is true on a Monday and a lie by the next month. The number I trust now is the trend line, and the trend line only exists if you keep measuring after the launch-week dopamine wears off.&lt;/p&gt;

&lt;p&gt;The blog never stopped being a measurement target. I just learned that the measurement has a time axis, and the time axis is where all the interesting bad news lives.&lt;/p&gt;

&lt;p&gt;I will re-run the full nine-week log on a different set of pages next quarter to see if the half-life holds at four-to-five weeks or if it varies by topic. My guess is that evergreen how-to pages decay slower than my "here's what broke this week" experience reports, because the how-to answers a stable question and the experience report answers a question nobody will type again. We'll see. The nice thing about a leaky bucket is that it makes a very honest dashboard.&lt;/p&gt;




&lt;p&gt;If you want the measurement loop I keep referring to — the five-prompt, three-retry, monthly-cadence setup with the GA4 segment regex and the Python visibility script — I wrote it up in &lt;a href="https://kenimoto.dev/books/llmo-ai-search-optimization?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=citations-half-life" rel="noopener noreferrer"&gt;Optimizing for AI Search (LLMO)&lt;/a&gt;. This post is what happened when I added a time axis to that loop and watched the numbers fall.&lt;/p&gt;

</description>
      <category>llmo</category>
      <category>geo</category>
      <category>aisearch</category>
      <category>measurement</category>
    </item>
    <item>
      <title>I Gave My Agent Persistent Memory. It Remembered the Wrong 3 Things for a Week.</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-gave-my-agent-persistent-memory-it-remembered-the-wrong-3-things-for-a-week-50c9</link>
      <guid>https://dev.to/kenimo49/i-gave-my-agent-persistent-memory-it-remembered-the-wrong-3-things-for-a-week-50c9</guid>
      <description>&lt;p&gt;I gave my coding agent persistent memory in March. By the end of the week it was telling me, with total confidence, that I preferred Poetry for dependency management, that our staging database lived in &lt;code&gt;us-east-1&lt;/code&gt;, and that I had already approved a migration plan I had explicitly rejected two days earlier.&lt;/p&gt;

&lt;p&gt;All three were wrong. I use &lt;code&gt;uv&lt;/code&gt;. Staging moved to &lt;code&gt;ap-northeast-1&lt;/code&gt; back in February. And I never approved that plan.&lt;/p&gt;

&lt;p&gt;The annoying part is that none of these were hallucinations in the usual sense. The agent wasn't making things up on the spot. It was faithfully recalling facts it had written down about me at some point and never corrected. The memory worked exactly as designed. That was the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitch everyone believes
&lt;/h2&gt;

&lt;p&gt;The standard story about agent memory goes like this. Every session starts with a blank context window. The agent forgets you the moment the conversation ends. So you give it persistent memory: a place to write down your preferences, your project state, the decisions you've made. Next session, it reads those notes first and picks up where it left off.&lt;/p&gt;

&lt;p&gt;Anthropic shipped exactly this for all Claude users in March 2026. Claude now scans your history, synthesizes a summary of who you are and what you're working on, and refreshes it roughly every 24 hours. Letta (the framework formerly known as MemGPT) goes further: the agent edits its own memory blocks through tool calls, deciding what's worth keeping.&lt;/p&gt;

&lt;p&gt;The selling point is continuity. No more re-explaining your stack every morning. And for the first few days, it genuinely felt like working with someone who remembered me.&lt;/p&gt;

&lt;p&gt;Then it started remembering things that were no longer true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stale memory is worse than no memory
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable asymmetry. An agent with no memory asks you the same question every day. That's annoying, but it's honest. You answer, it acts, nothing rots.&lt;/p&gt;

&lt;p&gt;An agent with persistent memory answers the question itself, using a fact it learned three weeks ago. If that fact has changed, you don't get a question. You get a confident wrong action. And because the answer sounds informed, you're less likely to catch it.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;us-east-1&lt;/code&gt; bug is the clearest example. The agent had recorded our staging region back when it was true. We migrated. Nobody told the memory. So for a week the agent kept generating deploy commands pointed at a region with nothing in it, and every command looked perfectly reasonable because the region string was a real region we had genuinely used.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ubxp2jkmzr60fjd2dr4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ubxp2jkmzr60fjd2dr4.png" alt="Diagram contrasting an agent with no memory, which asks the user a question, against an agent with stale persistent memory, which confidently acts on an outdated fact" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the part the memory pitch skips. "Remembering" and "remembering correctly" are different features, and persistence only gives you the first one for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways memory goes bad
&lt;/h2&gt;

&lt;p&gt;After staring at my agent's memory file for an embarrassingly long evening, I sorted the failures into three buckets. They're not exotic. They show up the moment a memory system runs longer than a few days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale facts.&lt;/strong&gt; Something that was true when it was written and isn't anymore. Regions, versions, deadlines, who owns which service. The world moves; the note doesn't. This was most of my pain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poisoned facts.&lt;/strong&gt; A wrong fact gets written once and then quoted forever. The "I approved the migration" entry came from a single ambiguous message where I said "yeah that approach makes sense" about the &lt;em&gt;general shape&lt;/em&gt; of a plan. The agent compressed that into approval, wrote it down, and from then on treated it as settled history. No amount of arguing in later sessions dislodged it, because it kept reloading the poisoned note at the start of each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-confident summaries.&lt;/strong&gt; The Poetry thing was this. I'd mentioned Poetry once, months ago, in the context of a different repo. The summarization pass that builds the daily profile flattened "used Poetry on one old project" into "prefers Poetry." Summaries are lossy by design, and the loss tends to drift toward overconfident generalizations.&lt;/p&gt;

&lt;p&gt;The first one is a freshness problem. The second is an integrity problem. The third is a compression problem. Lumping them together as "the agent got confused" is exactly why they're hard to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-editing memory doesn't save you
&lt;/h2&gt;

&lt;p&gt;The obvious response is: let the agent manage its own memory. It edits, it prunes, it corrects. Letta is built on this idea, and it's a genuinely good idea.&lt;/p&gt;

&lt;p&gt;But teams moving Letta from prototype to production keep hitting the same wall, and I hit it too: self-editing memory is &lt;em&gt;unpredictable&lt;/em&gt;. The agent decides what to keep based on the same flawed judgment that wrote the bad fact in the first place. When I corrected the region in a session, the agent sometimes updated the memory block, sometimes wrote a &lt;em&gt;second&lt;/em&gt; entry that contradicted the first, and once helpfully "consolidated" both into a summary that kept the wrong one. Letta even shipped a Recovery-Bench benchmark in 2026 specifically to measure how well agents climb out of corrupted states, which tells you the industry knows this is real.&lt;/p&gt;

&lt;p&gt;The deeper issue: an agent editing its own memory has no external source of truth to check against. It's grading its own homework. If it believed &lt;code&gt;us-east-1&lt;/code&gt; yesterday, "us-east-1" looks consistent with everything it knows today.&lt;/p&gt;

&lt;p&gt;I learned this lesson once before, the hard way, with a junior engineer I onboarded years ago. Brilliant, fast, and absolutely certain about a deployment process he'd learned on his first day. The process had changed in month two. He kept doing it the old way for weeks, confidently, because nobody handed him a reason to doubt his own notes. Persistent memory gave my agent the exact same failure mode, minus the part where a human eventually overhears the mistake at lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually started working
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend I solved this. But three changes cut the wrong-fact rate to something I can live with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamp everything, and decay it.&lt;/strong&gt; Every memory entry now carries when it was written. Facts about volatile things (regions, versions, deadlines) get treated as suspect after a set window and re-confirmed rather than trusted. A region string from three weeks ago isn't a fact; it's a hypothesis with an expiry date.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ysaz79lqyo1ulth5ulk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ysaz79lqyo1ulth5ulk.png" alt="Diagram showing the same memory entry treated as a trusted fact when fresh and as a hypothesis requiring re-confirmation once past its freshness window" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate "observed" from "inferred."&lt;/strong&gt; The Poetry disaster came from the agent storing a generalization as if it were a stated preference. Now there's a hard line: things I literally said go in one bucket, things the agent concluded go in another, and the inferred bucket needs more evidence before it gets to drive an action. Augment's framing stuck with me here: memory should guide decisions, but never be treated as infallible truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make corrections destructive.&lt;/strong&gt; When I correct a fact, the old entry doesn't get a polite contradicting neighbor. It gets overwritten and logged. The audit log matters more than I expected. The first time the agent confidently cited a fact I didn't recognize, being able to see &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;from which message&lt;/em&gt; it was written turned a mystery into a one-line fix.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's basically the discipline you'd apply to any cache: TTLs, write provenance, explicit invalidation. We just forgot to apply it to memory because the word "memory" makes it sound like something more trustworthy than a cache. It isn't. It's a cache that talks back.&lt;/p&gt;

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

&lt;p&gt;Persistent memory doesn't make an agent reliable. It makes an agent &lt;em&gt;consistent&lt;/em&gt;, which is a different thing, and occasionally the opposite one. A consistent agent repeats yesterday's truth whether or not it's still true today.&lt;/p&gt;

&lt;p&gt;The fix isn't more memory. It's treating every remembered fact as a claim with a timestamp and a source, not as ground truth. The agents that stay useful over weeks aren't the ones that remember the most. They're the ones that know which of their memories to distrust.&lt;/p&gt;

&lt;p&gt;If you want to go deeper on how context, state, and memory actually interact in production agents, I wrote about the full picture in &lt;a href="https://kenimoto.dev/books/context-engineering?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-persistent-memory" rel="noopener noreferrer"&gt;Context Engineering&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My agent still remembers the wrong region sometimes. But now it asks before it deploys there. That's the whole game.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>memory</category>
      <category>contextengineering</category>
    </item>
    <item>
      <title>I Stopped Adding Context to My Agent and Pruned Tool Outputs Instead — My 3-Hour Task Stopped Forgetting Its Own Plan</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Fri, 19 Jun 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/kenimo49/i-stopped-adding-context-to-my-agent-and-pruned-tool-outputs-instead-my-3-hour-task-stopped-4og6</link>
      <guid>https://dev.to/kenimo49/i-stopped-adding-context-to-my-agent-and-pruned-tool-outputs-instead-my-3-hour-task-stopped-4og6</guid>
      <description>&lt;p&gt;For a long time I treated context like savings: the more I put in, the richer I'd be. Thick CLAUDE.md, every file that might be relevant, the full output of every tool left sitting in the window. More information, smarter agent. That was the theory.&lt;/p&gt;

&lt;p&gt;The theory fell apart three hours into a migration task. The agent had set itself a design rule in the first twenty minutes: don't touch the legacy adapters, wrap them. By hour three it had forgotten its own rule and edited two of them directly. It also wandered into a directory I had explicitly told it to leave alone. The prompt wasn't the problem. The context had gotten so fat that the one instruction that mattered was buried under everything else I'd helpfully shoveled in.&lt;/p&gt;

&lt;p&gt;So I did the opposite of my instinct. I stopped adding and started pruning. Tokens dropped from about 140K to about 84K, roughly 40%, and the long task got &lt;em&gt;more&lt;/em&gt; accurate, not less. This is the story of what I cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point where "more is smarter" turns into a lie
&lt;/h2&gt;

&lt;p&gt;Context has a ceiling on how much of it actually works, and the ceiling sits well below the advertised number.&lt;/p&gt;

&lt;p&gt;Claude Sonnet markets a 200K-token window. But Sourcegraph's Geoffrey Huntley &lt;a href="https://ghuntley.com/redlining/" rel="noopener noreferrer"&gt;reported quality starting to slide somewhere around 147,000–152,000 tokens&lt;/a&gt;, what he calls redlining. The capacity of the window and the capacity you can use are two different numbers.&lt;/p&gt;

&lt;p&gt;This is not my anecdote talking. Chroma's research team ran the experiment properly: they tested &lt;a href="https://research.trychroma.com/context-rot" rel="noopener noreferrer"&gt;18 frontier models on how rising input length affects output quality&lt;/a&gt;, and every one degraded as the context grew. They named it &lt;strong&gt;context rot&lt;/strong&gt;. A model with a 200K window can show measurable degradation long before it's full. And the kicker: &lt;em&gt;how&lt;/em&gt; you fill the window matters. Padding it with tool operations that partly cancel each other out hurt performance more than padding it with neutral text. Raw tool dumps are close to the worst-case filler.&lt;/p&gt;

&lt;p&gt;Picture a new hire. Hand them three pages and they're useful by lunch. Bury the same desk under three hundred pages and they'll spend the day just figuring out which page matters. Information and usefulness stop being friends at some point on that curve. My agent had hit that point, and I was the one stacking the pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things I pruned
&lt;/h2&gt;

&lt;p&gt;When I went looking for what to cut, it sorted into three buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Raw tool outputs
&lt;/h3&gt;

&lt;p&gt;This was the big one. The full log of &lt;code&gt;npm test&lt;/code&gt;. The four hundred lines &lt;code&gt;grep&lt;/code&gt; returned. The giant JSON body from an API call. The agent hoards all of it, verbatim. But the only thing that moves the next step forward is the conclusion: "three tests failed, here are the files." The rest is ballast.&lt;/p&gt;

&lt;p&gt;Anthropic now ships this as an actual feature, which told me I hadn't invented anything; I'd just been doing it by hand. Their &lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-editing" rel="noopener noreferrer"&gt;context editing&lt;/a&gt; clears old tool results past a token threshold and leaves a small placeholder so the model knows something was removed. In a 100-turn web-search eval, Anthropic measured context editing cutting token use by 84% while keeping workflows alive that would otherwise have run out of room. As of mid-2026 the whole platform leans this way: dynamic tool filtering and programmatic tool calling all chase the same goal of keeping the window lean. The mechanism I'd been hacking together with notes-on-the-side had a name and a measured number.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Irrelevant files
&lt;/h3&gt;

&lt;p&gt;The "let me read this just in case" files. On a migration task I'd opened five components that had nothing to do with the migration. I'd told myself it was insurance. It was noise I paid for in tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Stale conversation turns
&lt;/h3&gt;

&lt;p&gt;The early flailing. Once "we're going with approach B" is decided, the three rejected approaches that got us there are dead weight. Keep the decision, drop the path to it. A &lt;code&gt;/compact&lt;/code&gt; with a custom instruction does this without throwing away the parts you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, before and after
&lt;/h2&gt;

&lt;p&gt;Same migration task, run with the fat context and then with the pruned one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before pruning&lt;/th&gt;
&lt;th&gt;After pruning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens used&lt;/td&gt;
&lt;td&gt;~140K&lt;/td&gt;
&lt;td&gt;~84K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design rule held?&lt;/td&gt;
&lt;td&gt;Drifted near the end&lt;/td&gt;
&lt;td&gt;Held to the end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Times I had to re-instruct&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong files touched&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tokens fell about 40%. But the number I actually cared about was the re-instruction count going from six to one. The agent kept its own hour-one decision all the way to the finish because it never climbed into the 147K–152K redline where the rot sets in. I didn't make it smarter. I stopped making it dumber.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35baeolun8dj5f6qbq1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35baeolun8dj5f6qbq1l.png" alt="Before and after: token count and plan retention across the migration task" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And notice the direction here. A while back I ran the &lt;a href="https://kenimoto.dev/blog/full-context-engineering-rag-80-percent/" rel="noopener noreferrer"&gt;opposite experiment, stacking four more context layers on top of RAG&lt;/a&gt; and measuring the gain. That was about adding structure and watching the curve rise (until it fell over on the smaller model). This is the mirror image: removing noise and watching accuracy come back. Same window, opposite vector. Both experiments point at the same uncomfortable truth: the window is not a bucket you should try to fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "don't put it in" is harder than "put it in"
&lt;/h2&gt;

&lt;p&gt;Honestly, pruning is the harder discipline.&lt;/p&gt;

&lt;p&gt;Adding is free of judgment. Nervous about a file? Open it. No decision required. Pruning forces you to say "this isn't needed" and then sit with the fear that it was. Every cut is a small bet against your own anxiety.&lt;/p&gt;

&lt;p&gt;My rule for the bet is one question: &lt;em&gt;does this directly help the single step in front of the agent right now?&lt;/em&gt; If not, it stays out. If it turns out I was wrong, the agent can go read the file again; it's an agent, retrieval is its job. Pre-loading everything wasn't serving the model. It was sedating me.&lt;/p&gt;

&lt;p&gt;Anthropic's newer models track how much of their own context is left, a kind of context self-awareness, so they can pace a long task instead of sprinting into the wall. But that only helps if there's headroom to track. Fill the window with raw logs on turn one and there's no runway left to be aware of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;When a long task started losing accuracy, my first instinct was "it doesn't have enough information." Exactly backwards. It had too much, and the one instruction that mattered had been diluted to nothing.&lt;/p&gt;

&lt;p&gt;What I actually did was three cuts: replace raw tool output with its conclusion, stop opening files "just in case," and throw away the trial-and-error once a decision is made. Forty percent fewer tokens, no trip into the rot valley, and a plan that survived three hours intact.&lt;/p&gt;

&lt;p&gt;Context engineering sounds like a question of what to add and how to arrange it. On a long-running task, the move that paid off was the other one: deciding what never goes in. Clear the desk down to three pages. That's the moment the new hire becomes useful again.&lt;/p&gt;




&lt;p&gt;The full map of context design (how System Prompt, few-shot, and RAG fit together, and where adding more crosses the 80-20 line into actively hurting you) is in my &lt;strong&gt;&lt;a href="https://kenimoto.dev/books/context-engineering?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=pruned-tool-outputs" rel="noopener noreferrer"&gt;Context Engineering Practical Guide&lt;/a&gt;&lt;/strong&gt;. This post is the subtraction side of that book, stress-tested on a task long enough to make context rot show up.&lt;/p&gt;

</description>
      <category>contextengineering</category>
      <category>claudecode</category>
      <category>llm</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
