<?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: Recca Tsai</title>
    <description>The latest articles on DEV Community by Recca Tsai (@recca0120).</description>
    <link>https://dev.to/recca0120</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50274%2F427ea12d-13dd-4a0e-8955-dddfbf0a39ea.png</url>
      <title>DEV Community: Recca Tsai</title>
      <link>https://dev.to/recca0120</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/recca0120"/>
    <language>en</language>
    <item>
      <title>Code Quest: A Claude Code Web UI That Runs in Interactive Mode — Just in Time for the June 15 Billing Change</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 16 May 2026 09:47:01 +0000</pubDate>
      <link>https://dev.to/recca0120/code-quest-a-claude-code-web-ui-that-runs-in-interactive-mode-just-in-time-for-the-june-15-4m0i</link>
      <guid>https://dev.to/recca0120/code-quest-a-claude-code-web-ui-that-runs-in-interactive-mode-just-in-time-for-the-june-15-4m0i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/16/code-quest-claude-code-web-ui/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Starting June 15, 2026, &lt;code&gt;claude -p&lt;/code&gt; and Agent SDK usage no longer count toward subscription plan limits. Instead, eligible plans get a separate monthly Agent SDK credit — and once that's exhausted, usage moves to standard API billing. If you run automated scripts or headless pipelines today, that cost picture changes after June 15.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/recca0120/code-quest" rel="noopener noreferrer"&gt;code-quest&lt;/a&gt; takes a different approach: it spawns Claude Code CLI directly, parses the full NDJSON interactive protocol, and delivers session management, file browsing, and Git integration in the browser. Interactive mode isn't covered by the new billing change — and that architectural choice turns out to be well-timed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes on June 15?
&lt;/h2&gt;

&lt;p&gt;From the official documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Starting June 15, 2026, Agent SDK and &lt;code&gt;claude -p&lt;/code&gt; usage on subscription plans will draw from a new monthly Agent SDK credit, separate from your interactive usage limits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In short: SDK and &lt;code&gt;claude -p&lt;/code&gt; usage moves out of your subscription's shared pool into a separate credit bucket — which then bills at API rates once exhausted.&lt;/p&gt;

&lt;p&gt;Affected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;claude -p&lt;/code&gt; (pipe / headless mode)&lt;/li&gt;
&lt;li&gt;Agent SDK (Python / TypeScript)&lt;/li&gt;
&lt;li&gt;Claude Code GitHub Actions&lt;/li&gt;
&lt;li&gt;Third-party apps calling via Agent SDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not affected&lt;/strong&gt;: interactive Claude Code sessions.&lt;/p&gt;

&lt;p&gt;Once the monthly credit runs out, additional Agent SDK usage bills at standard API rates. If your workflow involves &lt;code&gt;claude -p&lt;/code&gt; pipelines or SDK-wrapped automation, the cost math after June 15 is worth revisiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How code-quest Works
&lt;/h2&gt;

&lt;p&gt;Three-tier architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (React 19 + Tailwind v4)
    ↓ WebSocket /ws
Server (Express + Drizzle ORM)
    ↓ WebSocket /summoner
Summoner (local binary, compiled with Bun)
    ↓ child_process spawn
Claude Code CLI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Summoner&lt;/strong&gt; is the core piece. Compiled into a standalone binary with Bun, it runs on your local machine and handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spawning Claude Code CLI with &lt;code&gt;--output-format stream-json --input-format stream-json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Parsing each line of NDJSON output (&lt;code&gt;system&lt;/code&gt;, &lt;code&gt;assistant&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt;, &lt;code&gt;stream_event&lt;/code&gt;, &lt;code&gt;control_request&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Forwarding permission / elicitation prompts to the browser via WebSocket, then writing the response back to CLI stdin&lt;/li&gt;
&lt;li&gt;All local filesystem, Git, and OpenSpec operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Server&lt;/strong&gt; runs in the cloud, handling only routing and persistence (SQLite or MySQL) — it never touches local resources. The browser connects to the server via WebSocket; the server connects to the local Summoner via a separate WebSocket.&lt;/p&gt;

&lt;p&gt;This Split deployment design means the server can run anywhere without being co-located with the Claude Code CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Differs from Other Web UIs
&lt;/h2&gt;

&lt;p&gt;Existing Claude Code web interfaces follow roughly two patterns.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;UI layer&lt;/strong&gt; approach: the interface manages config files and session history, but actual Claude Code execution still happens in a terminal the user runs separately. These tools are useful for browsing and reviewing, but they can't intercept permission prompts from the UI, and session fork/resume requires manual steps.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;bridge approach&lt;/strong&gt;: the CLI connects back to the server via an SDK WebSocket path, and the UI receives the streamed output. This creates a real connection between UI and CLI — but the underlying transport is SDK mode. After June 15, that usage counts against the Agent SDK credit.&lt;/p&gt;

&lt;p&gt;code-quest takes a third path: &lt;strong&gt;spawn the CLI directly, in interactive mode&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not affected by the June 15 change&lt;/strong&gt;: interactive mode usage stays within subscription limits, outside Agent SDK credit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full protocol control&lt;/strong&gt;: every &lt;code&gt;control_request&lt;/code&gt; (permission prompts, elicitation) is handled in the browser in real time — not just observed as output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split deployment&lt;/strong&gt;: server runs in the cloud, Summoner runs locally, no co-location required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-class session operations&lt;/strong&gt;: fork, resume, rename are built-in features with DB persistence, not manual workarounds&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Session Management
&lt;/h3&gt;

&lt;p&gt;Each Claude Code session maps to a channel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spawn&lt;/strong&gt;: create a new session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume&lt;/strong&gt;: restore from DB; CLI restarts with &lt;code&gt;--resume &amp;lt;session-id&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork&lt;/strong&gt;: branch from an existing session state to explore a different approach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename&lt;/strong&gt;: label sessions for easy retrieval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full NDJSON event history is stored in DB. &lt;code&gt;content_block_delta&lt;/code&gt; events — streaming deltas that account for roughly 80% of all traffic — are split into a separate table and excluded from session history reads by default, keeping queries lightweight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Worktree Support
&lt;/h3&gt;

&lt;p&gt;code-quest has full worktree lifecycle management: create, list, delete, archive, rename. Each session can bind to its own worktree, letting multiple tasks run on separate branches without stepping on each other.&lt;/p&gt;

&lt;p&gt;This is especially useful when running multiple Claude Code sessions in parallel — each works in an isolated worktree, so changes don't conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  File Explorer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Browse directories, read files, view diffs&lt;/li&gt;
&lt;li&gt;Git status integration (modified, added, deleted files)&lt;/li&gt;
&lt;li&gt;Fuzzy search via Fuse.js&lt;/li&gt;
&lt;li&gt;RootGuard prevents directory traversal; &lt;code&gt;EXPLORER_ROOTS&lt;/code&gt; configures which paths are accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real-Time Push
&lt;/h3&gt;

&lt;p&gt;File, Git, and OpenSpec state changes don't require polling. &lt;code&gt;packages/broadcaster&lt;/code&gt; uses a DataSource / pub-sub pattern backed by chokidar file watching — changes flow automatically from Summoner → Server → browser as they happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket Reconnection
&lt;/h3&gt;

&lt;p&gt;The custom ResumableSocket tracks sequence numbers. On reconnect, it replays missed events from a 500-event circular buffer. If the gap is too large to recover, it signals the client to refresh the full state instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenSpec Integration
&lt;/h3&gt;

&lt;p&gt;code-quest has built-in support for the OpenSpec format. You can create, archive, and toggle spec tasks directly from the interface, with real-time spec state synced to the browser automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&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/recca0120/code-quest.git
&lt;span class="nb"&gt;cd &lt;/span&gt;code-quest
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Copy &lt;code&gt;apps/server/.env.example&lt;/code&gt; and adjust as needed. Key environment variables:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APP_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_SQLITE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;SQLite path, e.g. &lt;code&gt;file:./data/code-quest.db&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SUMMONER_MODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;local&lt;/code&gt; (same machine) or &lt;code&gt;remote&lt;/code&gt; (split deployment)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SUMMONER_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Bearer token for remote Summoner auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLI_AUTO_MODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass &lt;code&gt;--auto-mode&lt;/code&gt; to Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXPLORER_ROOTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;home dir&lt;/td&gt;
&lt;td&gt;Comma-separated allowed root paths&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For remote mode (server in the cloud, Summoner on your machine): set &lt;code&gt;SUMMONER_MODE=remote&lt;/code&gt; on the server, configure &lt;code&gt;SUMMONER_TOKEN&lt;/code&gt; on the Summoner, and point it at the server's WebSocket endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an API on Top
&lt;/h2&gt;

&lt;p&gt;The architecture is already structured for this. The Server is the central hub for all requests — adding HTTP API endpoints there, then routing them through the existing WebSocket channel to the Summoner, which writes to CLI stdin, closes the loop. The plumbing is already in place; it just needs an external-facing interface.&lt;/p&gt;

&lt;p&gt;That API runs over interactive mode underneath, so it doesn't touch Agent SDK credit. If you need to drive Claude Code programmatically without SDK billing, the architecture has a clear path for it.&lt;/p&gt;

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

&lt;p&gt;Interactive mode, full protocol implementation, Split deployment — these three architectural choices give code-quest a distinct position after the June 15 billing change. Subscription usage stays in interactive limits, permission prompts are handled in the browser, and the server runs independently from the local Summoner. If you're looking for a way to operate Claude Code fully from the browser, this project is worth trying.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/recca0120/code-quest" rel="noopener noreferrer"&gt;code-quest GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/agent-sdk/overview" rel="noopener noreferrer"&gt;Claude Agent SDK documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan" rel="noopener noreferrer"&gt;Use the Claude Agent SDK with your Claude plan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webui</category>
    </item>
    <item>
      <title>Three Ways to Git Clone with a Different SSH Key</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 16 May 2026 02:33:38 +0000</pubDate>
      <link>https://dev.to/recca0120/three-ways-to-git-clone-with-a-different-ssh-key-5e8m</link>
      <guid>https://dev.to/recca0120/three-ways-to-git-clone-with-a-different-ssh-key-5e8m</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/16/git-clone-different-ssh-key/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have access to a GitHub repo, but the SSH key isn't the default &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt; or &lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;. Running &lt;code&gt;git clone&lt;/code&gt; directly gives you &lt;code&gt;Repository not found&lt;/code&gt;. This post covers three ways to specify which SSH key to use, including the common pitfall with &lt;code&gt;~/.ssh/config&lt;/code&gt; Host aliases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I needed to clone a private repo:&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 git@github.com:client-org/project.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SSH key required for this repo is &lt;code&gt;~/.ssh/client_key&lt;/code&gt;, not the system default. Cloning without specifying it returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Repository not found.
fatal: Could not read from remote repository.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message is misleading — the repo exists. GitHub returned "not found" because the SSH key it received doesn't have access. GitHub intentionally hides whether a repo exists at all when the key has no permission.&lt;/p&gt;

&lt;p&gt;I tried setting a Host alias in &lt;code&gt;~/.ssh/config&lt;/code&gt; first, but clone still failed with the same error. The two methods below are what actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: GIT_SSH_COMMAND (One-Off)
&lt;/h2&gt;

&lt;p&gt;The fastest approach — prefix the command with an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GIT_SSH_COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ssh -i ~/.ssh/client_key'&lt;/span&gt; git clone git@github.com:client-org/project.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GIT_SSH_COMMAND&lt;/code&gt; tells Git to use the SSH command you specify. &lt;code&gt;-i ~/.ssh/client_key&lt;/code&gt; points to the private key. This only applies to that one command — subsequent push/pull won't use it automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: git config core.sshCommand (Single Repo)
&lt;/h2&gt;

&lt;p&gt;After cloning, set it in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;project
git config core.sshCommand &lt;span class="s1"&gt;'ssh -i ~/.ssh/client_key'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes to &lt;code&gt;.git/config&lt;/code&gt; and applies to all future git operations (pull, push, fetch) inside that repo.&lt;/p&gt;

&lt;p&gt;Do both in one shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GIT_SSH_COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ssh -i ~/.ssh/client_key'&lt;/span&gt; git clone git@github.com:client-org/project.git
&lt;span class="nb"&gt;cd &lt;/span&gt;project
git config core.sshCommand &lt;span class="s1"&gt;'ssh -i ~/.ssh/client_key'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the simplest approach for a single repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: ~/.ssh/config Host Alias (With a Catch)
&lt;/h2&gt;

&lt;p&gt;The textbook approach is to set a Host alias in &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; github-client
    &lt;span class="k"&gt;HostName&lt;/span&gt; github.com
    &lt;span class="k"&gt;User&lt;/span&gt; git
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/client_key
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then clone with the alias:&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 git@github-client:client-org/project.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Catch: Existing Host github.com Overrides the Alias
&lt;/h3&gt;

&lt;p&gt;If your &lt;code&gt;~/.ssh/config&lt;/code&gt; already has a &lt;code&gt;Host github.com&lt;/code&gt; block (e.g., for your personal account), SSH may ignore the alias and use the wrong key — clone fails with the same error.&lt;/p&gt;

&lt;p&gt;Verify the alias is working before cloning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@github-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response is &lt;code&gt;Hi &amp;lt;username&amp;gt;!&lt;/code&gt; with the correct account. If the wrong account shows up, the alias isn't routing correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add url.insteadOf to Route an Entire Org
&lt;/h3&gt;

&lt;p&gt;If you have multiple repos under the same org that all need the same key, use a URL rewrite rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; url.&lt;span class="s2"&gt;"git@github-client:client-org/"&lt;/span&gt;.insteadOf &lt;span class="s2"&gt;"git@github.com:client-org/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, any git operation targeting &lt;code&gt;client-org&lt;/code&gt; repos automatically rewrites the URL to use the &lt;code&gt;github-client&lt;/code&gt; alias — no need to set &lt;code&gt;core.sshCommand&lt;/code&gt; on each repo individually.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For a single repo, &lt;code&gt;core.sshCommand&lt;/code&gt; from Method 2 is more direct and doesn't touch global config.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GIT_SSH_COMMAND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One-time clone&lt;/td&gt;
&lt;td&gt;Single command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git config core.sshCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single repo, ongoing use&lt;/td&gt;
&lt;td&gt;Single repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~/.ssh/config&lt;/code&gt; + &lt;code&gt;url.insteadOf&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Entire org, multiple repos&lt;/td&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-personal-account/managing-multiple-accounts" rel="noopener noreferrer"&gt;GitHub Docs — Managing multiple accounts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh" rel="noopener noreferrer"&gt;GitHub Docs — About SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/docs/git#Documentation/git.txt-codeGITSSHCOMMANDcode" rel="noopener noreferrer"&gt;Git Documentation — GIT_SSH_COMMAND&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>ssh</category>
      <category>github</category>
    </item>
    <item>
      <title>Free Claude Code: Route Claude Code API Calls to Free Alternatives</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Fri, 15 May 2026 14:09:35 +0000</pubDate>
      <link>https://dev.to/recca0120/free-claude-code-route-claude-code-api-calls-to-free-alternatives-hik</link>
      <guid>https://dev.to/recca0120/free-claude-code-route-claude-code-api-calls-to-free-alternatives-hik</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/15/free-claude-code-proxy/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude Code's developer experience is excellent, but the API costs add up fast. &lt;a href="https://github.com/Alishahryar1/free-claude-code" rel="noopener noreferrer"&gt;free-claude-code&lt;/a&gt; is an open-source proxy that lets you keep using Claude Code's CLI, VS Code extension, and JetBrains integration while routing the underlying API calls to free-tier cloud APIs or self-hosted local models.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Every Claude Code operation goes through the Anthropic API. This proxy sits in between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code CLI / VS Code / JetBrains
           ↓
    free-claude-code proxy
           ↓
  NVIDIA NIM / OpenRouter / Ollama / ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy exposes Anthropic-compatible endpoints (&lt;code&gt;/v1/messages&lt;/code&gt;, &lt;code&gt;/v1/models&lt;/code&gt;, etc.), translates incoming requests to each provider's format, then translates the responses back to Anthropic's format. From the Claude Code client's perspective, it's just a regular Anthropic API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supported Providers
&lt;/h2&gt;

&lt;p&gt;Ten backends are currently supported:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NVIDIA NIM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier at build.nvidia.com; includes Kimi K2.5, GLM 4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenRouter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Aggregates many models; some with free tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;deepseek-chat, much cheaper than Opus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kimi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moonshot's platform.moonshot.ai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wafer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;wafer.ai; DeepSeek-V4-Pro, GLM-5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Z.ai&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GLM-5.1, GLM-5-turbo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenCode Zen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;opencode.ai; includes deepseek-v4-flash-free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LM Studio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local server, default localhost:1234&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;llama.cpp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local server, default localhost:8080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ollama&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Containerized local models, default localhost:11434&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Per-Tier Model Routing
&lt;/h2&gt;

&lt;p&gt;Claude Code splits requests into three tiers: Opus (main agent), Sonnet, and Haiku (sub-agents). The proxy lets you route each tier to a different model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MODEL_OPUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openrouter/qwen/qwen3-235b-a22b:free
&lt;span class="nv"&gt;MODEL_SONNET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek/deepseek-chat
&lt;span class="nv"&gt;MODEL_HAIKU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama/llama3.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opus requests (typically the most expensive) can be routed to a free model; Haiku requests can run locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and Setup
&lt;/h2&gt;

&lt;p&gt;Prerequisites: Claude Code CLI and Python uv.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the proxy&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; git+https://github.com/Alishahryar1/free-claude-code.git

&lt;span class="c"&gt;# Start the proxy server&lt;/span&gt;
fcc-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After starting, open the displayed localhost address in your browser to access the Admin UI and configure provider API keys.&lt;/p&gt;

&lt;p&gt;Then use &lt;code&gt;fcc-claude&lt;/code&gt; instead of the regular &lt;code&gt;claude&lt;/code&gt; command — the launcher automatically injects the required environment variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  VS Code
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claude.env"&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;"ANTHROPIC_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8082"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"freecc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&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;h3&gt;
  
  
  JetBrains
&lt;/h3&gt;

&lt;p&gt;Edit the ACP configuration file (path varies by platform) with the same three environment variables.&lt;/p&gt;

&lt;p&gt;Once configured, the IDE's model picker also works — the proxy's &lt;code&gt;/v1/models&lt;/code&gt; endpoint exposes all available models for visual selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional Features
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Discord / Telegram bots&lt;/strong&gt;: Wrap Claude Code sessions in a bot for remote task management, streaming progress, and conversation branches. Requires bot tokens and channel IDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice transcription&lt;/strong&gt;: Connect Whisper or NVIDIA NIM for voice-to-text input via the messaging platforms.&lt;/p&gt;

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

&lt;p&gt;A few real constraints to keep in mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model capability gap&lt;/strong&gt;: Many of Claude Code's strengths — long context, accurate tool calls, complex reasoning — are specific to Claude models. Switching to alternatives may degrade agentic reliability, especially tool call accuracy, which drives most of Claude Code's workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tier rate limits&lt;/strong&gt;: NVIDIA NIM and OpenRouter free models typically have RPM/TPD caps. Heavy usage will hit rate limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local model resource requirements&lt;/strong&gt;: Running llama.cpp or Ollama needs sufficient VRAM/RAM. Performance is noticeably slower than cloud APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It Makes Sense
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trying out Claude Code without committing to Anthropic API costs&lt;/li&gt;
&lt;li&gt;Mostly doing simple tasks (file edits, formatting, small features) that don't need top-tier models&lt;/li&gt;
&lt;li&gt;You have a GPU and prefer paying with electricity instead of API fees&lt;/li&gt;
&lt;li&gt;Comparing how different models perform under the Claude Code interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your work depends on Claude's long-context handling or complex agentic tasks, swapping models will likely cause tool call failures or reasoning errors. In that case, analyzing your usage structure and optimizing cache usage may be more practical than switching models — see the &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;earlier posts in this series&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/Alishahryar1/free-claude-code" rel="noopener noreferrer"&gt;free-claude-code GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://build.nvidia.com/" rel="noopener noreferrer"&gt;NVIDIA NIM Free API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openrouter.ai/models?q=free" rel="noopener noreferrer"&gt;OpenRouter Free Models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>proxy</category>
      <category>opensource</category>
    </item>
    <item>
      <title>36 Days of Claude Code Logs: Silent Model Switching, 11.5x Efficiency Gap</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 09 May 2026 08:42:56 +0000</pubDate>
      <link>https://dev.to/recca0120/36-days-of-claude-code-logs-silent-model-switching-115x-efficiency-gap-1bgm</link>
      <guid>https://dev.to/recca0120/36-days-of-claude-code-logs-silent-model-switching-115x-efficiency-gap-1bgm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/09/claude-code-model-cost-efficiency/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;The first post&lt;/a&gt; scanned 95 days of logs and found sub-agent cache TTL silently dropped to 5m. &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;The second&lt;/a&gt; tracked it to 17 consecutive days of 100% 5m — conclusion: it's the new default.&lt;/p&gt;

&lt;p&gt;This time I broke down the model dimension. Scanning March through May 7, I originally wanted to confirm whether the cache TTL had reverted (it hasn't). Instead I found something bigger: &lt;strong&gt;the server doesn't just control cache TTL — it silently switched the main agent model three times&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Source
&lt;/h2&gt;

&lt;p&gt;Same as before: &lt;code&gt;~/.claude/projects/{project-path}/{session-uuid}.jsonl&lt;/code&gt;. This time I also checked the &lt;code&gt;message.model&lt;/code&gt; field in API responses:&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;"message"&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&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;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;142&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_read_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;892041&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3847&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_creation"&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;"ephemeral_5m_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ephemeral_1h_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8234&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;model&lt;/code&gt; field comes from the server, not the client. Whatever the API says it used, that's what it used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only Four Models
&lt;/h2&gt;

&lt;p&gt;Scanning all JSONL files, only four models appeared:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model ID&lt;/th&gt;
&lt;th&gt;Short&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Cache Read&lt;/th&gt;
&lt;th&gt;Cache Write 5m&lt;/th&gt;
&lt;th&gt;Cache Write 1h&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;opus-4-6&lt;/td&gt;
&lt;td&gt;O4.6&lt;/td&gt;
&lt;td&gt;$15/MTok&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$18.75&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;$75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opus-4-7&lt;/td&gt;
&lt;td&gt;O4.7&lt;/td&gt;
&lt;td&gt;$15/MTok&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$18.75&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;$75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sonnet-4-6&lt;/td&gt;
&lt;td&gt;S4.6&lt;/td&gt;
&lt;td&gt;$3/MTok&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;td&gt;$3.75&lt;/td&gt;
&lt;td&gt;$6&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;haiku-4-5&lt;/td&gt;
&lt;td&gt;H4.5&lt;/td&gt;
&lt;td&gt;$0.80/MTok&lt;/td&gt;
&lt;td&gt;$0.08&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;td&gt;$1.60&lt;/td&gt;
&lt;td&gt;$4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cache read costs differ 5x between Opus and Sonnet ($1.50 vs $0.30), output 5x ($75 vs $15). Since cache reads dominate Claude Code API calls, &lt;strong&gt;model choice directly determines cost magnitude&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Agent Silently Switched Three Times
&lt;/h2&gt;

&lt;p&gt;Using cc-office (my primary project) as an example, the main agent model timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Date         O4.6     O4.7     S4.6     Total  Dominant
───────────────────────────────────────────────────────
2026-04-07   3,707        0        0    3,707  O4.6 100%
2026-04-13   2,821        0        0    2,821  O4.6 100%
2026-04-14   3,385        0      315    3,704  O4.6 91%   ← S4.6 appears
2026-04-15       0        0    3,445    3,449  S4.6 100%  ← First switch
2026-04-16       0        0    5,949    5,949  S4.6 100%
2026-04-17       0    1,855    3,621    5,476  S4.6 66%
2026-04-18       0    1,973        0    1,973  O4.7 100%  ← Second switch
2026-04-25     211    5,386        0    5,597  O4.7 96%
2026-04-26   2,308        0        0    2,308  O4.6 100%  ← Back to O4.6
2026-04-29   2,149        0        0    2,149  O4.6 100%
2026-04-30     514        0    1,213    1,727  S4.6 70%   ← Third switch
2026-05-01       0        0    3,492    3,492  S4.6 100%
2026-05-05     350        0    3,187    3,537  S4.6 90%
2026-05-06   2,347        0        0    2,347  O4.6 100%  ← Back again
2026-05-07   4,197       44        0    4,241  O4.6 99%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had opus-4-6 1m context selected the entire time. But the server returned a different model three times:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;4/15-4/17&lt;/strong&gt;: Downgraded to sonnet-4-6 (3 days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4/18-4/25&lt;/strong&gt;: Switched to opus-4-7 (8 days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4/30-5/5&lt;/strong&gt;: Downgraded to sonnet-4-6 again (6 days)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each switch was binary — 100% one model the day before, 100% another the next day. Same pattern as the cache TTL regression: sharp switch, no announcement, client unaware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sub-Agent Models Are Not Your Choice
&lt;/h2&gt;

&lt;p&gt;Sub-agent models are decided by Claude Code autonomously, not by user settings. The distribution varies dramatically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Main&lt;/th&gt;
&lt;th&gt;Sub O4.6&lt;/th&gt;
&lt;th&gt;Sub O4.7&lt;/th&gt;
&lt;th&gt;Sub S4.6&lt;/th&gt;
&lt;th&gt;Sub H4.5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3/26-4/14&lt;/td&gt;
&lt;td&gt;O4.6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4/15-4/17&lt;/td&gt;
&lt;td&gt;S4.6&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4/18-4/25&lt;/td&gt;
&lt;td&gt;O4.7&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;73%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4/26-4/30&lt;/td&gt;
&lt;td&gt;O4.6&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5/01-5/05&lt;/td&gt;
&lt;td&gt;S4.6&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5/06-5/07&lt;/td&gt;
&lt;td&gt;O4.6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When main uses Opus, sub-agents tend to also use Opus (76%). When main is downgraded to Sonnet, sub-agents switch to mostly Haiku (92%). This correlation isn't coincidental — the server adjusts sub-agent model allocation alongside main model changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure Efficiency
&lt;/h2&gt;

&lt;p&gt;The previous posts focused on cache TTL. This time: &lt;strong&gt;how much did you spend for how much main agent output&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why main output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main agent output is what you're paying for — code, edits, answers&lt;/li&gt;
&lt;li&gt;Sub-agents are overhead — their job is to search and gather for the main agent&lt;/li&gt;
&lt;li&gt;Sub-agent output feeds into main agent input, not into your deliverables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Core metric:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Total cost (main + sub) per million main output tokens&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Lower = more efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Efficiency Rankings Across Seven Periods
&lt;/h2&gt;

&lt;p&gt;Segmented by dominant main agent model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Main&lt;/th&gt;
&lt;th&gt;S/M ratio&lt;/th&gt;
&lt;th&gt;$/M main output&lt;/th&gt;
&lt;th&gt;$/day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 ⚡&lt;/td&gt;
&lt;td&gt;5/01-5/05&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;S4.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$167&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4/15-4/17&lt;/td&gt;
&lt;td&gt;S4.6&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;$218&lt;/td&gt;
&lt;td&gt;$583&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3/09-3/21&lt;/td&gt;
&lt;td&gt;S4.6&lt;/td&gt;
&lt;td&gt;2.04&lt;/td&gt;
&lt;td&gt;$875&lt;/td&gt;
&lt;td&gt;$144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4/26-4/30&lt;/td&gt;
&lt;td&gt;O4.6&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;td&gt;$896&lt;/td&gt;
&lt;td&gt;$1,450&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4/18-4/25&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O4.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;td&gt;$1,134&lt;/td&gt;
&lt;td&gt;$3,148&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5/06-5/07&lt;/td&gt;
&lt;td&gt;O4.6&lt;/td&gt;
&lt;td&gt;0.35&lt;/td&gt;
&lt;td&gt;$1,554&lt;/td&gt;
&lt;td&gt;$2,836&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 🐌&lt;/td&gt;
&lt;td&gt;3/26-4/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O4.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1,925&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2,137&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;11.5x gap&lt;/strong&gt; between the most and least efficient periods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most Efficient: Main S4.6 + Sub S4.6/H4.5 (5/01-5/05)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main:  21,082 calls (4,216/day)  Model: S4.6 98%
Sub:   19,266 calls (3,853/day)  Model: S4.6 64%, H4.5 36%
Total: $1,596 ($319/day)
Main output: 9,559,468 (1,911,894/day)
$/M main output: $167
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S/M ratio of 0.91 looks high — nearly one sub call per main call. But subs only use Sonnet and Haiku, so overhead is just $56/M main output. Cheap sub calls don't hurt even when frequent.&lt;/p&gt;

&lt;p&gt;Best bang for buck: 5,991 main output tokens per dollar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Least Efficient: Main O4.6 + Sub 76% O4.6 (3/26-4/14)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main:  41,086 calls (2,162/day)  Model: O4.6 99%
Sub:   22,460 calls (1,182/day)  Model: O4.6 76%, S4.6 4%, H4.5 19%
Total: $40,594 ($2,137/day)
Main output: 21,092,340 (1,110,123/day)
$/M main output: $1,925
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S/M ratio is only 0.55 — looks disciplined. But sub-agents used 76% Opus, meaning every sub call pays Opus-rate cache reads. Sub overhead hits $477/M main output.&lt;/p&gt;

&lt;p&gt;Main output per day was only 1.11M — the lowest across all periods. Most money spent, least produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most Expensive but Not Most Efficient: Opus 4.7 (4/18-4/25)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main:  31,204 calls (3,900/day)  Model: O4.7 99%
Sub:    7,233 calls (904/day)    Model: O4.7 27%, H4.5 73%
Total: $25,187 ($3,148/day)
Main output: 22,219,870 (2,777,484/day)
$/M main output: $1,134
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Highest daily output (2.77M tokens), lowest S/M ratio (0.23), sub overhead only $15/M. Looks lean, but $3,148/day is steep — the Sonnet period (4/15-4/17) produced 2.67M/day for just $583.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sub-Agent Overhead Rankings
&lt;/h2&gt;

&lt;p&gt;Sub cost divided by main output — pure overhead measurement:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;S/M ratio&lt;/th&gt;
&lt;th&gt;Sub Composition&lt;/th&gt;
&lt;th&gt;Sub $/M main output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 ✅&lt;/td&gt;
&lt;td&gt;4/15-4/17&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;H4.5 92%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4/18-4/25&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;td&gt;H4.5 73%, O4.7 27%&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5/01-5/05&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;td&gt;S4.6 64%, H4.5 36%&lt;/td&gt;
&lt;td&gt;$56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;3/26-4/14&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O4.6 76%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$477&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 ❌&lt;/td&gt;
&lt;td&gt;3/09-3/21&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.04&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O4.6 20%, S4.6 71%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$695&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sub-agents should use Haiku&lt;/strong&gt;. Period 4/15-4/17 with 92% Haiku had $6/M overhead — 1/80th of using Opus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High S/M ratio isn't inherently bad&lt;/strong&gt;. Period 5/01-5/05 had 0.91 ratio but cheap models, so overhead was only $56. Period 3/26-4/14 had 0.55 ratio but 76% Opus, pushing overhead to $477&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;S/M ratio isn't the problem. &lt;strong&gt;What model the sub uses is the problem&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Best S4.6&lt;br&gt;(5/01-5/05)&lt;/th&gt;
&lt;th&gt;O4.6&lt;br&gt;(3/26-4/14)&lt;/th&gt;
&lt;th&gt;O4.7&lt;br&gt;(4/18-4/25)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily cost&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$319&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2,137&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3,148&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily main output&lt;/td&gt;
&lt;td&gt;1,911,894&lt;/td&gt;
&lt;td&gt;1,110,123&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,777,484&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$/M main output&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$167&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$1,925&lt;/td&gt;
&lt;td&gt;$1,134&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per dollar&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,991&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;td&gt;882&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub overhead/M&lt;/td&gt;
&lt;td&gt;$56&lt;/td&gt;
&lt;td&gt;$477&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub primary model&lt;/td&gt;
&lt;td&gt;S4.6+H4.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O4.6 76%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;H4.5 73%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What You Can't Control
&lt;/h2&gt;

&lt;p&gt;All analysis in this post comes with a caveat: &lt;strong&gt;model choice isn't fully in your hands&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What you control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selecting a model in Claude Code settings (I selected opus-4-6 1m)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you don't control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server may silently swap your main agent model&lt;/li&gt;
&lt;li&gt;Sub-agent models are assigned by Claude Code autonomously&lt;/li&gt;
&lt;li&gt;Cache TTL is server-decided (sub-agent stuck at 100% 5m for 29 consecutive days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "Using Opus 4.6" label in Claude Code may not reflect reality. Scanning JSONL for the API response &lt;code&gt;model&lt;/code&gt; field is the only reliable way to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache TTL Status: Still 100% 5m
&lt;/h2&gt;

&lt;p&gt;Updating the cache TTL situation. Scanning 4/30-5/7:&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;Main Agent&lt;/th&gt;
&lt;th&gt;Sub Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total API calls&lt;/td&gt;
&lt;td&gt;37,366&lt;/td&gt;
&lt;td&gt;26,160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1h cache write&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5m cache write&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since the &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;first post's&lt;/a&gt; 4/9 mark, &lt;strong&gt;sub-agents have been at 100% 5m for 29 consecutive days, zero 1h writes&lt;/strong&gt;. No sign of reverting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Scan Your Own Data
&lt;/h2&gt;

&lt;p&gt;Building on the Python from previous posts, now with model breakdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.claude/projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;jsonl&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jsonl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="n"&gt;inp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;cr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inp&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt;
                 &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it to see what model your main agent actually used — and whether it matches what you selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model choice is the biggest cost factor&lt;/strong&gt;. Cache TTL affects cost ~2x; model affects 5-11x. The cache read price gap between Opus and Sonnet (5x) translates to thousands of dollars per day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server silently switches models&lt;/strong&gt;. I selected opus-4-6, but across 36 days, 17 were switched to sonnet-4-6 or opus-4-7. Same pattern as the cache TTL regression — no announcement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-agents using Opus is the biggest waste&lt;/strong&gt;. Sub-agent work is search and exploration; Haiku is sufficient. Sub overhead with 76% Opus is 80x higher than with 92% Haiku&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High S/M ratio isn't inherently bad&lt;/strong&gt;. What model the sub uses matters more than how many times it runs. Many cheap sub calls beat one expensive Opus sub call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The most efficient combination&lt;/strong&gt; ($167/M main output) and &lt;strong&gt;the least efficient&lt;/strong&gt; ($1,925/M) differ by &lt;strong&gt;11.5x&lt;/strong&gt; — same user, same project, same type of work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What model you select in Claude Code doesn't matter — what matters is what the server actually gives you. Scanning your own JSONL is the only reliable method.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;95 Days of Claude Code Logs: Discovering a Second Cache TTL Silent Regression&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;17 Consecutive Days at 100% 5m — This Is the New Default&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Claude Code Session Cost and Cache Misconceptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic Prompt Caching Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/46829" rel="noopener noreferrer"&gt;Cache TTL silently regressed — GitHub Issue #46829&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>promptcaching</category>
      <category>agents</category>
      <category>python</category>
    </item>
    <item>
      <title>JetBrains Air: An Agentic IDE That Runs Multiple AI Agents in Parallel</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Wed, 06 May 2026 03:23:13 +0000</pubDate>
      <link>https://dev.to/recca0120/jetbrains-air-an-agentic-ide-that-runs-multiple-ai-agents-in-parallel-jo5</link>
      <guid>https://dev.to/recca0120/jetbrains-air-an-agentic-ide-that-runs-multiple-ai-agents-in-parallel-jo5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/06/jetbrains-air-agentic-ide/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have a bug to fix, tests to write, and a module to refactor. The old way is to do them one at a time, or juggle multiple terminals yourself. &lt;a href="https://air.dev/" rel="noopener noreferrer"&gt;JetBrains Air&lt;/a&gt; lets you delegate each task to a different AI agent and run them all simultaneously without interference.&lt;/p&gt;

&lt;p&gt;Air isn't an AI panel bolted onto an existing IDE. It's a new development environment designed from the ground up for delegating tasks to agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supported Agents
&lt;/h2&gt;

&lt;p&gt;Air ships with four agents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Agent&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Long-context understanding, code generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Codex&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Code-specialized model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Configurable thinking mode for reasoning depth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Junie&lt;/td&gt;
&lt;td&gt;JetBrains&lt;/td&gt;
&lt;td&gt;JetBrains' own agent, included with AI subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once a task starts, you can switch models but not providers. A Claude task can move from Sonnet to Opus, but can't switch to Codex mid-task.&lt;/p&gt;

&lt;p&gt;Air also supports the Agent Client Protocol (ACP), so more third-party agents can plug in over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Execution Environments
&lt;/h2&gt;

&lt;p&gt;Each task runs in one of three environments:&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Workspace
&lt;/h3&gt;

&lt;p&gt;Runs directly in your working directory. Fastest startup, zero configuration, but agent changes hit your files directly.&lt;/p&gt;

&lt;p&gt;Best for: quick iterations where isolation isn't needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Worktree
&lt;/h3&gt;

&lt;p&gt;Creates a separate working copy of the same repo, isolating changes to another branch.&lt;/p&gt;

&lt;p&gt;Configuration lives in &lt;code&gt;.air/worktree.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NODE_ENV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"envFile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.env.test"&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;"setup"&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;"macos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npm install"&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;Best for: running multiple tasks that touch the same files without conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker
&lt;/h3&gt;

&lt;p&gt;Runs inside a container with complete isolation. Requires Docker Desktop.&lt;/p&gt;

&lt;p&gt;Configuration lives in &lt;code&gt;.air/docker.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ubuntu:24.04"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&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;"setup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apt-get update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apt-get install -y jq"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&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;Custom images must include Git and &lt;code&gt;/bin/sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Best for: full isolation where nothing should touch the host system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Local&lt;/th&gt;
&lt;th&gt;Worktree&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup speed&lt;/td&gt;
&lt;td&gt;Fastest&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation level&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;File/branch&lt;/td&gt;
&lt;td&gt;Complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration needed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Depends on host env&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Task Lifecycle
&lt;/h2&gt;

&lt;p&gt;A task moves through these stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define&lt;/strong&gt;: press &lt;code&gt;⌘+\&lt;/code&gt; to create a new task, describe what you want in chat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add context&lt;/strong&gt;: attach files, folders, Git commits, symbols, images, MCP servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose permission mode&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Ask Permission: prompts before every file edit or command&lt;/li&gt;
&lt;li&gt;Auto-Edit: automatically accepts file changes&lt;/li&gt;
&lt;li&gt;Plan: analyzes without making changes&lt;/li&gt;
&lt;li&gt;Full Access: no prompts at all&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt;: the agent starts working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input required&lt;/strong&gt;: the agent pauses and notifies you when it needs a decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done&lt;/strong&gt;: review changes, commit, push&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use &lt;code&gt;⌘+1&lt;/code&gt; to see all task states and switch between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Multitasking
&lt;/h2&gt;

&lt;p&gt;This is Air's core value proposition. You can run multiple tasks simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One agent fixing a bug&lt;/li&gt;
&lt;li&gt;Another writing tests&lt;/li&gt;
&lt;li&gt;A third refactoring a module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each task runs independently with its own agent session. When a task needs your attention, you get a notification. Handle it and switch back.&lt;/p&gt;

&lt;p&gt;If multiple tasks touch the same files, use Git Worktree or Docker to prevent conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Server Integration
&lt;/h2&gt;

&lt;p&gt;Air supports Model Context Protocol for extending agent capabilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open settings with &lt;code&gt;⌘+,&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to AI | MCP Servers&lt;/li&gt;
&lt;li&gt;Click Add Global MCP Server&lt;/li&gt;
&lt;li&gt;Paste the server configuration in JSON format&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This lets agents access databases, call APIs, or interact with external systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web Preview
&lt;/h2&gt;

&lt;p&gt;For web projects, Air includes a built-in preview. The dev server launches automatically and shows a preview window. You can switch between preview and source modes, with responsive sizing built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and Availability
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform&lt;/strong&gt;: macOS only for now; Windows and Linux planned for 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: Air itself is free to download&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI billing&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;JetBrains AI Pro subscribers get all agents included&lt;/li&gt;
&lt;li&gt;You can bring your own API keys (Anthropic, OpenAI, Google)&lt;/li&gt;
&lt;li&gt;If both are configured, your own keys are used first&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Experience (From a JetBrains Developer)
&lt;/h2&gt;

&lt;p&gt;Valerii Tepliakov, a developer on the Air team, shared his adoption journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Started skeptical — cared too much about code quality to trust AI output&lt;/li&gt;
&lt;li&gt;Began with small, reversible tasks alongside IntelliJ IDEA&lt;/li&gt;
&lt;li&gt;Gradually expanded scope, delegating multiple concurrent tasks&lt;/li&gt;
&lt;li&gt;Eventually Air became his primary tool; IntelliJ reserved for debugging only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;His key insight: &lt;strong&gt;agents struggle with architecture and design decisions&lt;/strong&gt;. Make the foundational choices yourself, then delegate implementation details.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Air Differs from Claude Code / Cursor
&lt;/h2&gt;

&lt;p&gt;Air occupies a different niche:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt;: single agent in a terminal, one task at a time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt;: AI embedded in an IDE, augmenting your editing experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Air&lt;/strong&gt;: multi-agent task manager where you're the manager and agents are workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Air doesn't replace your IDE. It handles agent workflow; you keep using IntelliJ / VS Code for fine-grained editing and debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Air Makes Sense
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your project is large enough to have multiple independent tasks running in parallel&lt;/li&gt;
&lt;li&gt;You'd rather review code than write it yourself&lt;/li&gt;
&lt;li&gt;You want to compare agents (e.g., run the same task with Claude and Codex to see which produces better results)&lt;/li&gt;
&lt;li&gt;Your team already decomposes work into well-defined tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workflow is "focus on one thing at a time, think while you code," Air's multi-task architecture may not be what you need.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://air.dev/" rel="noopener noreferrer"&gt;Air official site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/help/air/" rel="noopener noreferrer"&gt;JetBrains Air Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.jetbrains.com/air/2026/03/air-launches-as-public-preview-a-new-wave-of-dev-tooling-built-on-26-years-of-experience/" rel="noopener noreferrer"&gt;Air Launches as Public Preview — The Air Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.jetbrains.com/air/2026/04/my-journey-to-agent-first-development-with-air/" rel="noopener noreferrer"&gt;My Journey to Agent-First Development With Air&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>ide</category>
      <category>jetbrains</category>
    </item>
    <item>
      <title>Find Dead Code with Knip: The Blind Spots ESLint and depcheck Miss</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 02 May 2026 08:38:34 +0000</pubDate>
      <link>https://dev.to/recca0120/find-dead-code-with-knip-the-blind-spots-eslint-and-depcheck-miss-19i</link>
      <guid>https://dev.to/recca0120/find-dead-code-with-knip-the-blind-spots-eslint-and-depcheck-miss-19i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/02/knip-dead-code-detector/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The project is two years old. There are 80 entries in &lt;code&gt;package.json&lt;/code&gt; and you can't say with confidence which ones are still being used. A &lt;code&gt;utils.ts&lt;/code&gt; file hasn't been touched in three months — you're not sure if anyone imports it. &lt;code&gt;shared/helpers.ts&lt;/code&gt; exports a dozen functions, some of which were replaced by newer approaches months ago, but nobody deleted the old ones.&lt;/p&gt;

&lt;p&gt;Dead code doesn't accumulate overnight. A little left over from each refactor, one forgotten dependency from each package swap. Over time the project gets heavier, but no tool ever tells you exactly what's dead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webpro-nl/knip" rel="noopener noreferrer"&gt;Knip&lt;/a&gt; is built for exactly this. One command finds everything you thought was being used but isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blind Spots in ESLint and depcheck
&lt;/h2&gt;

&lt;p&gt;Most teams reach for ESLint and depcheck to handle this kind of thing. Both have clear limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ESLint&lt;/strong&gt; only sees a single file. It can tell you "there's a &lt;code&gt;const x&lt;/code&gt; in this function that's never used," but if an entire &lt;code&gt;utils.ts&lt;/code&gt; is never imported anywhere, ESLint won't notice. Its view stops at the file boundary — it can't trace cross-file reference chains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;depcheck&lt;/strong&gt; only looks at &lt;code&gt;package.json&lt;/code&gt;. It scans for &lt;code&gt;require&lt;/code&gt; and &lt;code&gt;import&lt;/code&gt; statements to see which packages are actually referenced, then flags anything that's installed but unused. But it doesn't understand TypeScript export/import semantics, and it has no concept of which files are never referenced at all.&lt;/p&gt;

&lt;p&gt;Together they still leave a gap: cross-file export usage — nothing tracks it.&lt;/p&gt;

&lt;p&gt;Knip works differently. Starting from configured entry points, it builds a complete module graph tracing every import, export, and dependency. Anything not connected to the graph is dead code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Finds
&lt;/h2&gt;

&lt;p&gt;A single &lt;code&gt;knip&lt;/code&gt; run finds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unused npm dependencies&lt;/strong&gt;: installed but never imported anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlisted dependencies&lt;/strong&gt;: imported in code but missing from &lt;code&gt;package.json&lt;/code&gt; (implicit reliance on a transitive dep)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unused exports&lt;/strong&gt;: exported but never imported anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unused files&lt;/strong&gt;: entire files never referenced from anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unresolved imports&lt;/strong&gt;: imports pointing to paths or modules that don't exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These problems used to require several tools stitched together, with gaps remaining. Knip covers all of them.&lt;/p&gt;

&lt;p&gt;Vercel used Knip to delete nearly 300,000 lines of code. That's their own number, not marketing copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and Usage
&lt;/h2&gt;

&lt;p&gt;No installation required — just run it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Or add it to devDependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; knip
npx knip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Knip ships with around 150 plugins covering Vite, Vitest, Next.js, Astro, ESLint, GitHub Actions, and more. In most projects, zero configuration is needed — it auto-detects the tooling in use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Output
&lt;/h2&gt;

&lt;p&gt;After a run, the output looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unused files (2)
src/legacy/old-helper.ts
src/utils/deprecated.ts

Unused dependencies (3)
lodash
moment
@types/node  (devDependencies)

Unused exports (5)
src/shared/helpers.ts: formatDate, parseQuery
src/utils/string.ts: capitalize, truncate, slugify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each category is clear: which files are entirely unreferenced, which packages can be removed, which exports have no importers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The first run usually surfaces a lot. Don't try to fix everything at once. Start with unused dependencies (easy wins), then work through unused exports, and finally tackle entire unused files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;If you need to customize, add a &lt;code&gt;knip.json&lt;/code&gt; at the project root (or a &lt;code&gt;knip&lt;/code&gt; key in &lt;code&gt;package.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/pages/**/*.tsx"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**/*.{ts,tsx}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ignore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/legacy/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/*.stories.ts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ignoreDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"some-cli-tool"&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;ul&gt;
&lt;li&gt;
&lt;code&gt;entry&lt;/code&gt;: where to start tracing references from&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;project&lt;/code&gt;: which files belong to this project&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignore&lt;/code&gt;: paths to skip&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignoreDependencies&lt;/code&gt;: dependencies to keep even if they appear unused (e.g. CLI tools)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Auto-Fix
&lt;/h2&gt;

&lt;p&gt;Some issues can be fixed automatically with &lt;code&gt;--fix&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx knip &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Currently &lt;code&gt;--fix&lt;/code&gt; handles removing unused entries from &lt;code&gt;package.json&lt;/code&gt; and some unused exports. Not everything is auto-fixable, but it saves a lot of manual work on the dependency side.&lt;/p&gt;

&lt;h2&gt;
  
  
  VSCode Extension and MCP
&lt;/h2&gt;

&lt;p&gt;Knip has a &lt;a href="https://marketplace.visualstudio.com/items?itemName=knip.vscode-knip" rel="noopener noreferrer"&gt;VSCode extension&lt;/a&gt; that shows unused exports directly in the editor — no need to run the CLI to find out.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;@knip/mcp&lt;/code&gt;, which lets AI assistants call Knip when analyzing a project, helping them understand which code is actually in use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead Code Is Technical Debt
&lt;/h2&gt;

&lt;p&gt;Removing unused code isn't just about making the project smaller. Every unused export is a cognitive burden — a new developer doesn't know if that function matters and has to spend time tracing it. Every unused dependency is a potential security risk and an update to manage.&lt;/p&gt;

&lt;p&gt;Knip turns "find dead code" from a manual chore into an automated step. Run it once to clean house, then wire it into CI so dead code can't quietly accumulate again.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://knip.dev/" rel="noopener noreferrer"&gt;Knip official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/webpro-nl/knip" rel="noopener noreferrer"&gt;GitHub — webpro-nl/knip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://effectivetypescript.com/2023/07/29/knip/" rel="noopener noreferrer"&gt;Effective TypeScript — Recommendation: Use knip to detect dead code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>typescript</category>
    </item>
    <item>
      <title>vitest-fail-on-console: Stop Ignoring console.error in Your Tests</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 02 May 2026 00:51:28 +0000</pubDate>
      <link>https://dev.to/recca0120/vitest-fail-on-console-stop-ignoring-consoleerror-in-your-tests-1io9</link>
      <guid>https://dev.to/recca0120/vitest-fail-on-console-stop-ignoring-consoleerror-in-your-tests-1io9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/05/02/vitest-fail-on-console/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All tests pass, but the terminal is full of red &lt;code&gt;console.error&lt;/code&gt; output. This is common and easy to ignore — the tests passed, after all. But those errors don't appear out of nowhere. Something went wrong; nobody just noticed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thomasbrodusch/vitest-fail-on-console" rel="noopener noreferrer"&gt;vitest-fail-on-console&lt;/a&gt; does one thing: if &lt;code&gt;console.error&lt;/code&gt; or &lt;code&gt;console.warn&lt;/code&gt; appears during a test, that test fails. It forces you to acknowledge these messages instead of letting them drown in noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why console.error in Tests Is a Code Smell
&lt;/h2&gt;

&lt;p&gt;Vitest doesn't care about console output by default. You can &lt;code&gt;console.error&lt;/code&gt; all day and tests still pass.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;code&gt;console.error&lt;/code&gt; usually means something. It might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A React prop type warning&lt;/li&gt;
&lt;li&gt;An async error that was caught but not properly handled&lt;/li&gt;
&lt;li&gt;A third-party package telling you you're using it wrong&lt;/li&gt;
&lt;li&gt;An error handler in your own code getting triggered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these appear in tests, the test is running in a slightly broken state — it just didn't throw. Over time the test output becomes pure noise. Nobody reads it anymore, and the real signals get buried.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vitest-fail-on-console&lt;/code&gt; flips this: make console output a test failure, so you're forced to address it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; vitest-fail-on-console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Import and call it in your setup file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/setup.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;failOnConsole&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest-fail-on-console&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;failOnConsole&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then wire up the setup file in &lt;code&gt;vitest.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tests/setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Any test that triggers &lt;code&gt;console.error&lt;/code&gt; or &lt;code&gt;console.warn&lt;/code&gt; will now fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;failOnConsole()&lt;/code&gt; accepts an options object to control which console methods trigger failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;failOnConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;shouldFailOnError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// default true&lt;/span&gt;
  &lt;span class="na"&gt;shouldFailOnWarn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// default true&lt;/span&gt;
  &lt;span class="na"&gt;shouldFailOnLog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// default false&lt;/span&gt;
  &lt;span class="na"&gt;shouldFailOnInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// default false&lt;/span&gt;
  &lt;span class="na"&gt;shouldFailOnDebug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// default false&lt;/span&gt;
  &lt;span class="na"&gt;shouldFailOnAssert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default false&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;error&lt;/code&gt; and &lt;code&gt;warn&lt;/code&gt; are usually enough. Whether to include &lt;code&gt;log&lt;/code&gt; / &lt;code&gt;info&lt;/code&gt; / &lt;code&gt;debug&lt;/code&gt; depends on your project's conventions.&lt;/p&gt;

&lt;h3&gt;
  
  
  allowMessage
&lt;/h3&gt;

&lt;p&gt;Allow specific messages through without failing — useful for known third-party issues you can't fix right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;failOnConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;allowMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/ResizeObserver loop limit exceeded/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  silenceMessage
&lt;/h3&gt;

&lt;p&gt;Like &lt;code&gt;allowMessage&lt;/code&gt;, but also suppresses the console output entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;failOnConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;silenceMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/Not implemented: navigation/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  skipTest
&lt;/h3&gt;

&lt;p&gt;Skip specific test files or test names entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;failOnConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;skipTest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;testName&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/legacy/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  afterEachDelay
&lt;/h3&gt;

&lt;p&gt;Sometimes async operations call console methods after a test ends. This option adds a delay before checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;failOnConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;afterEachDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// wait 100ms, default is 0&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Expected console.error Calls
&lt;/h2&gt;

&lt;p&gt;After installing &lt;code&gt;vitest-fail-on-console&lt;/code&gt;, if a test is specifically verifying that &lt;code&gt;console.error&lt;/code&gt; gets called, letting it fire naturally will cause the test to fail.&lt;/p&gt;

&lt;p&gt;The correct approach is to mock it with &lt;code&gt;vi.spyOn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logs an error when request fails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// mock it so the message doesn't actually reach the console&lt;/span&gt;
  &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mockImplementation&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

  &lt;span class="nf"&gt;triggerSomethingThatLogsError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// assert it was called with the expected message&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does two things: the test explicitly declares "I know an error will be logged here," and it asserts the exact message. Much stricter than silently letting &lt;code&gt;console.error&lt;/code&gt; through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pair It with a Clean Test Environment
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vitest-fail-on-console&lt;/code&gt; handles the console output side. If your tests also have I/O boundaries to replace — filesystem, file watchers — you can pair it with memfs using the same philosophy: every aspect of the test environment should be under your control.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Testing a Filesystem Service with memfs + FakeWatchService&lt;/a&gt; for that approach.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/thomasbrodusch/vitest-fail-on-console" rel="noopener noreferrer"&gt;vitest-fail-on-console — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/vitest-fail-on-console" rel="noopener noreferrer"&gt;vitest-fail-on-console — npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vitest.dev/config/#setupfiles" rel="noopener noreferrer"&gt;Vitest — setupFiles configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>vitest</category>
      <category>node</category>
    </item>
    <item>
      <title>Testing a Filesystem Service with memfs + FakeWatchService: No Disk Required</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Tue, 28 Apr 2026 05:58:11 +0000</pubDate>
      <link>https://dev.to/recca0120/testing-a-filesystem-service-with-memfs-fakewatchservice-no-disk-required-4d8j</link>
      <guid>https://dev.to/recca0120/testing-a-filesystem-service-with-memfs-fakewatchservice-no-disk-required-4d8j</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/28/memfs-fake-watch-testing/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;How do you test a Node.js service that operates on the filesystem? The obvious approach is to create real files under &lt;code&gt;/tmp&lt;/code&gt;, run the tests, then clean up. But that comes with problems: slow I/O, inconsistent cross-platform behavior, CI permission differences, and file watcher events whose timing you can't control.&lt;/p&gt;

&lt;p&gt;This post dissects a real &lt;code&gt;FileService&lt;/code&gt; test suite to show how &lt;a href="https://github.com/streamich/memfs" rel="noopener noreferrer"&gt;memfs&lt;/a&gt; and a hand-written FakeWatchService solve all of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Service Looks Like
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FileService&lt;/code&gt; implements a &lt;code&gt;IFileService&lt;/code&gt; interface. It handles directory browsing, file listing (with fuzzy search), file reading/writing, and CRUD operations. It takes two optional external dependencies via constructor injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FileService&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;IFileService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;roots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;WatchService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;fsImpl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;roots&lt;/code&gt;: allowed root directories&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;watch&lt;/code&gt;: optional &lt;code&gt;WatchService&lt;/code&gt; for cache invalidation on file changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsImpl&lt;/code&gt;: optional &lt;code&gt;fs&lt;/code&gt; module implementation, passed to &lt;code&gt;glob&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are injected through the constructor — real implementations in production, fakes in tests. This is the foundation of the entire testing strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing the Real Filesystem with memfs
&lt;/h2&gt;

&lt;p&gt;The first step is swapping out &lt;code&gt;node:fs&lt;/code&gt; and &lt;code&gt;node:fs/promises&lt;/code&gt; entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;memfs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;memfs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./file-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;memfs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;memfs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/streamich/memfs" rel="noopener noreferrer"&gt;memfs&lt;/a&gt; is a fully in-memory &lt;code&gt;fs&lt;/code&gt; implementation. Its API is identical to Node.js's native &lt;code&gt;fs&lt;/code&gt;, but everything happens in memory — no disk touched.&lt;/p&gt;

&lt;p&gt;The dynamic import inside &lt;code&gt;vi.mock&lt;/code&gt; factory functions is a Vitest requirement, but actual usage of &lt;code&gt;vol&lt;/code&gt; and &lt;code&gt;memfs&lt;/code&gt; is through top-level imports.&lt;/p&gt;

&lt;p&gt;Before each test, &lt;code&gt;vol.fromJSON()&lt;/code&gt; declaratively creates the needed file structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/test-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FileService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromJSON&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alpha/.keep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beta/nested/.keep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hidden/.keep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/.keep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.git/.keep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export {}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/utils.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export const x = 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileService&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memfs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: in-memory operations, no disk I/O&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: &lt;code&gt;vol.reset()&lt;/code&gt; gives you a fresh filesystem — zero cross-test interference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability&lt;/strong&gt;: the JSON shows the entire file structure at a glance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform&lt;/strong&gt;: no worrying about Windows path separators or &lt;code&gt;/tmp&lt;/code&gt; permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Replacing chokidar with FakeWatchService
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FileService&lt;/code&gt; has an internal caching layer: the first &lt;code&gt;listFiles()&lt;/code&gt; call runs glob to scan the directory tree, caching the result. Subsequent calls return the cache until a &lt;code&gt;WatchService&lt;/code&gt; event invalidates it.&lt;/p&gt;

&lt;p&gt;In production, chokidar watches for file changes, but chokidar events are asynchronous and non-deterministic. There's no way to precisely control "trigger an event now" in a test.&lt;/p&gt;

&lt;p&gt;The solution is a Fake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeWatchService&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;WatchService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;subs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WatchCallback&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WatchCallback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Unsubscribe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WatchEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[FakeWatchService] subscriber threw:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a mock — it's a Fake with real behavior. It genuinely manages subscribers, genuinely executes unsubscriptions, and genuinely dispatches events to all callbacks. The only difference is the event source: instead of OS-level inotify/FSEvents, events come from &lt;code&gt;simulate()&lt;/code&gt; calls in test code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For more on the difference between Fakes and Mocks, see &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;DI + Fake + in-memory: Writing Maintainable Frontend Tests&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Three Cache Invalidation Scenarios
&lt;/h2&gt;

&lt;p&gt;With FakeWatchService, cache behavior becomes precisely verifiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1: No event → cache hit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second call without watcher event reuses cached file list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FakeWatchService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileService&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memfs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after-cache.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after-cache.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A file was added via &lt;code&gt;vol.writeFileSync&lt;/code&gt; before the second call, but no watch event was fired, so the cache stays valid. The new file doesn't appear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 2: Event fired → cache invalidated&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;watcher event invalidates cache so next call rebuilds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FakeWatchService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileService&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memfs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after-invalidate.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after-invalidate.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after-invalidate.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;watch.simulate()&lt;/code&gt;, the cache is cleared and the next &lt;code&gt;listFiles()&lt;/code&gt; rescans, picking up the new file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 3: Concurrent first calls subscribe only once&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;concurrent first calls do not subscribe duplicate watchers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FakeWatchService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;subscribeCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;realSubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subscribeCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;realSubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileService&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memfs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscribeCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two concurrent &lt;code&gt;listFiles()&lt;/code&gt; calls should only subscribe once. This verifies the &lt;code&gt;inflight promise&lt;/code&gt; deduplication mechanism works correctly.&lt;/p&gt;

&lt;p&gt;All three scenarios would be nearly impossible to write reliably with real chokidar — event timing and frequency aren't controllable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Tests Are First-Class Citizens
&lt;/h2&gt;

&lt;p&gt;Security assertions are distributed across every &lt;code&gt;describe&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// browseDirectories filters hidden directories&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filters hidden directories&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browseDirectories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DirEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// readFile blocks path traversal&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects path traversal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../etc/passwd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Path traversal not allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// All mutations reject paths outside allowed roots&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all mutations reject paths outside allowed roots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/etc/passwd-clone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/etc/passwd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// rename, copy, move likewise...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't in a separate "security test suite" — they live alongside feature tests. Every entry point has its own security verification.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;isInsideRoot&lt;/code&gt; boundary tests are worth noting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;returns false for prefix-similar but not actually inside&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sibling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sibling`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// /test-root-sibling&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInsideRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/test-root-sibling&lt;/code&gt; has &lt;code&gt;/test-root&lt;/code&gt; as a string prefix, but it's not inside the root. The implementation uses &lt;code&gt;path.relative()&lt;/code&gt; to handle this correctly, and the test ensures that behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests Are Grouped by Behavior
&lt;/h2&gt;

&lt;p&gt;The test file isn't organized as "one describe per method." It's grouped by behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;browseDirectories&lt;/strong&gt;: full browsing behavior including filtering, sorting, security checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;listFiles&lt;/strong&gt;: three pattern modes (empty, trailing slash, fuzzy) plus a dedicated describe for cache invalidation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;readFile&lt;/strong&gt;: normal reads + path traversal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mutations&lt;/strong&gt;: isolated &lt;code&gt;MROOT&lt;/code&gt; environment, full CRUD + out-of-bounds rejection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;isInsideRoot&lt;/strong&gt;: pure logic boundary testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cache invalidation gets its own &lt;code&gt;describe('cache invalidation via WatchService')&lt;/code&gt; because it's an independent behavioral concern with its own setup (requires FakeWatchService injection).&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Reuse This Pattern
&lt;/h2&gt;

&lt;p&gt;The core of this strategy is three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;memfs replaces fs&lt;/strong&gt;: applicable to any service using &lt;code&gt;node:fs&lt;/code&gt;. Two lines of &lt;code&gt;vi.mock&lt;/code&gt;, declarative setup with &lt;code&gt;vol.fromJSON()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-written Fakes replace non-deterministic dependencies&lt;/strong&gt;: file watchers, WebSockets, event emitters — anything async and event-driven benefits from the Fake + &lt;code&gt;simulate()&lt;/code&gt; pattern&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constructor injection makes replacement possible&lt;/strong&gt;: instead of &lt;code&gt;new Chokidar()&lt;/code&gt; inside the service, inject a &lt;code&gt;WatchService&lt;/code&gt; interface. Tests are just a beneficiary of this design&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your project has similar I/O boundaries — filesystem, database, external APIs, message queues — the same approach applies: define an interface, inject the dependency, swap in an in-memory Fake for tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This approach is even more effective in a monorepo. When Fakes are extracted into shared packages, both frontend and backend can use the same test doubles with guaranteed behavioral consistency. See &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Monorepo Shared Fakes: One Test Double from Frontend to Backend&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/streamich/memfs" rel="noopener noreferrer"&gt;memfs — JavaScript file system utilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vitest.dev/guide/mocking" rel="noopener noreferrer"&gt;Vitest — Mocking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fusejs.io/" rel="noopener noreferrer"&gt;Fuse.js — Lightweight fuzzy-search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/articles/mocksArentStubs.html" rel="noopener noreferrer"&gt;Martin Fowler — Mocks Aren't Stubs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>vitest</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Add Old Models to Claude Code /model Picker: 3 Methods Tested</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sun, 26 Apr 2026 18:17:29 +0000</pubDate>
      <link>https://dev.to/recca0120/how-to-add-old-models-to-claude-code-model-picker-3-methods-tested-4ie9</link>
      <guid>https://dev.to/recca0120/how-to-add-old-models-to-claude-code-model-picker-3-methods-tested-4ie9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/27/claude-code-model-picker-config/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The day Opus 4.7 launched, Claude Code's &lt;code&gt;opus&lt;/code&gt; alias silently pointed to the new version. No notification, no changelog reminder. Open the &lt;code&gt;/model&lt;/code&gt; picker — Opus 4.6 was gone.&lt;/p&gt;

&lt;p&gt;I'd previously written a &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;3-month billing analysis&lt;/a&gt; showing 4.7's quota burn is 2.4× that of 4.6. Switching back seemed obvious, but the picker had no option for it. I spent an afternoon testing every configuration method and combing through GitHub issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Configuration Mechanisms
&lt;/h2&gt;

&lt;p&gt;Claude Code currently offers three ways to modify the &lt;code&gt;/model&lt;/code&gt; picker.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;availableModels&lt;/code&gt;: Replaces, Doesn't Extend
&lt;/h3&gt;

&lt;p&gt;Add this to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availableModels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"claude-haiku-4-5"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/model&lt;/code&gt; picker now shows &lt;strong&gt;only these three&lt;/strong&gt;. The default opus / sonnet / haiku aliases all disappear, replaced entirely by your list.&lt;/p&gt;

&lt;p&gt;This is the biggest gotcha: many people assume &lt;code&gt;availableModels&lt;/code&gt; means "add these on top of the defaults." It doesn't — it's a complete replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;modelOverrides&lt;/code&gt;: For Bedrock / Vertex
&lt;/h3&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;"modelOverrides"&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;"claude-opus-4-7"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-7-v1:0"&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;This maps model IDs to provider-specific endpoints. If you're using the Anthropic API directly, this setting does nothing for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/code&gt;: One Extra, That's It
&lt;/h3&gt;

&lt;p&gt;Supported since v2.1.78, this environment variable adds a single custom entry to the bottom of the &lt;code&gt;/model&lt;/code&gt; picker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6[1m]"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_CUSTOM_MODEL_OPTION_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Opus 4.6 (1M)"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Opus 4.6 with 1M context window"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't touch the default picker, but &lt;strong&gt;you can only add one&lt;/strong&gt;. Want both Opus 4.6 and Sonnet 4.6 1M? No luck — there's no &lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION_2&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Aliases Don't Work in availableModels
&lt;/h3&gt;

&lt;p&gt;You might try:&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;"availableModels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"opus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sonnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6[1m]"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The picker shows 4 items, but &lt;code&gt;opus&lt;/code&gt;, &lt;code&gt;sonnet&lt;/code&gt;, &lt;code&gt;haiku&lt;/code&gt; behave inconsistently when mixed with full model IDs. Aliases aren't valid model IDs in this context — stick to full IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same-Family Deduplication
&lt;/h3&gt;

&lt;p&gt;If you list both &lt;code&gt;claude-opus-4-6&lt;/code&gt; and &lt;code&gt;claude-opus-4-7&lt;/code&gt; in &lt;code&gt;availableModels&lt;/code&gt;, same-family deduplication may collapse them into one entry. This behavior is undocumented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock Version with &lt;code&gt;model&lt;/code&gt;, Not &lt;code&gt;availableModels&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;availableModels&lt;/code&gt; controls what's in the picker. The &lt;code&gt;model&lt;/code&gt; field controls what's actually used at startup:&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6[1m]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availableModels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6[1m]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-haiku-4-5"&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;Set both. If you only set &lt;code&gt;availableModels&lt;/code&gt; without &lt;code&gt;model&lt;/code&gt;, startup still uses whatever the default alias points to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the GitHub Community Says
&lt;/h2&gt;

&lt;p&gt;This isn't a niche issue. There are plenty of related issues on GitHub:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/anthropics/claude-code/issues/14443" rel="noopener noreferrer"&gt;#14443&lt;/a&gt; — Request for Configurable Model Picker
&lt;/h3&gt;

&lt;p&gt;User joerivwijn asked for the &lt;code&gt;/model&lt;/code&gt; picker to be configurable via settings.json. Especially relevant for Bedrock users whose model IDs need &lt;code&gt;us.&lt;/code&gt; prefixes and &lt;code&gt;:0&lt;/code&gt; suffixes that don't match the default picker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Marked as duplicate of #12969 by bot and auto-closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/anthropics/claude-code/issues/12738" rel="noopener noreferrer"&gt;#12738&lt;/a&gt; — Opus 4.5 Disappeared from Picker
&lt;/h3&gt;

&lt;p&gt;User grigb reported Opus 4.5 missing from the CLI picker on the Max plan, despite being available in the web app. Multiple users confirmed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cleanspin&lt;/strong&gt; found &lt;code&gt;/model opus&lt;/code&gt; pointed to Opus 4.1 instead of 4.5 — the alias mapping was stale. Workaround: use the full ID &lt;code&gt;/model claude-opus-4-5-20251101&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;todddrinkwater&lt;/strong&gt; reported the VS Code extension also affected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zerzerzerz&lt;/strong&gt; and &lt;strong&gt;PavelProdan&lt;/strong&gt; posted screenshots confirming "it was there yesterday, gone today"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern repeats with every new model release: the alias points to the new version, the old version vanishes from the picker, no notice given.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Auto-closed by stale bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/anthropics/claude-code/issues/35630" rel="noopener noreferrer"&gt;#35630&lt;/a&gt; — ANTHROPIC_CUSTOM_MODEL_OPTION Undocumented
&lt;/h3&gt;

&lt;p&gt;User coygeek noticed v2.1.78's changelog mentioned this env var, but official docs had zero coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Fixed — both the env-vars and model-config doc pages now include full documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Related Open Issues
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/52310" rel="noopener noreferrer"&gt;#52310&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bedrock ignores &lt;code&gt;availableModels&lt;/code&gt;, shows only one model per family&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/47164" rel="noopener noreferrer"&gt;#47164&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Enterprise custom model IDs can't appear in interactive picker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/40501" rel="noopener noreferrer"&gt;#40501&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Duplicate entries when settings.json model matches a built-in option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/49566" rel="noopener noreferrer"&gt;#49566&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ANTHROPIC_DEFAULT_*_MODEL&lt;/code&gt; creates duplicate "Custom" entry on Bedrock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/53006" rel="noopener noreferrer"&gt;#53006&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;VS Code extension missing Sonnet 4.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/38238" rel="noopener noreferrer"&gt;#38238&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1M context model not available in picker on WSL2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The issues cluster around two themes: &lt;strong&gt;stale alias mappings&lt;/strong&gt; and &lt;strong&gt;no mechanism to extend the default list&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Configuration
&lt;/h2&gt;

&lt;p&gt;After testing everything, &lt;code&gt;availableModels&lt;/code&gt; is unreliable due to same-family deduplication — list 4 models and you might only see 3. The most practical approach: &lt;strong&gt;don't touch &lt;code&gt;availableModels&lt;/code&gt;, keep the default picker, lock your version with &lt;code&gt;model&lt;/code&gt;, and add one extra option with &lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;~/.claude/settings.json&lt;/code&gt;&lt;/strong&gt; (global):&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6[1m]"&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;One line. The default picker stays intact (opus / sonnet / haiku all present), but every session starts on Opus 4.6 1M. The &lt;code&gt;opus&lt;/code&gt; alias in the picker points to the latest version (currently 4.7), so you can switch to it anytime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;~/.zshrc&lt;/code&gt;&lt;/strong&gt; (add a 4th option via &lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-4-6[1m]"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_CUSTOM_MODEL_OPTION_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Sonnet 4.6 (1M)"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Sonnet 4.6 with 1M context window"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives 4 options in &lt;code&gt;/model&lt;/code&gt;: the default opus / sonnet / haiku, plus Sonnet 4.6 1M. Covers all daily scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus 4.6 1M&lt;/strong&gt;: locked via &lt;code&gt;model&lt;/code&gt; field, used on startup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opus 4.7&lt;/strong&gt;: the &lt;code&gt;opus&lt;/code&gt; alias in the picker, switch when needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet 4.6&lt;/strong&gt;: the &lt;code&gt;sonnet&lt;/code&gt; alias, for review / fix / test tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet 4.6 1M&lt;/strong&gt;: the 4th option via env var, for large context scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Why not use &lt;code&gt;availableModels&lt;/code&gt;? It's a full replacement, and same-family dedup silently eats entries. Leaving it unset gives you the most stable default picker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Claude Code's model picker assumes everyone wants the latest version. &lt;code&gt;availableModels&lt;/code&gt; is a full replacement with dedup issues; &lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/code&gt; only supports one entry.&lt;/p&gt;

&lt;p&gt;In practice, &lt;code&gt;model&lt;/code&gt; + &lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/code&gt; covers most needs: lock your version with &lt;code&gt;model&lt;/code&gt;, add one extra option via env var, and leave the default picker alone.&lt;/p&gt;

&lt;p&gt;Plenty of GitHub issues have been filed, but most get marked duplicate or stale-closed. If you need more than one custom model in the picker, there's no official solution yet.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/docs/claude-code/settings#model-configuration" rel="noopener noreferrer"&gt;Claude Code Model Configuration&lt;/a&gt; — Official model settings docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables" rel="noopener noreferrer"&gt;Claude Code Environment Variables&lt;/a&gt; — Environment variable reference&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/14443" rel="noopener noreferrer"&gt;#14443 — Configure custom models in /model picker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/12738" rel="noopener noreferrer"&gt;#12738 — Opus 4.5 missing from model picker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/35630" rel="noopener noreferrer"&gt;#35630 — ANTHROPIC_CUSTOM_MODEL_OPTION env var missing from docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/52310" rel="noopener noreferrer"&gt;#52310 — Bedrock availableModels ignored&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>I Audited 3 Months of Claude Code Billing — Most Community Cost-Saving Tips Don''t Work</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sun, 26 Apr 2026 00:26:04 +0000</pubDate>
      <link>https://dev.to/recca0120/i-audited-3-months-of-claude-code-billing-every-cost-saving-tip-i-believed-was-wrong-2ghj</link>
      <guid>https://dev.to/recca0120/i-audited-3-months-of-claude-code-billing-every-cost-saving-tip-i-believed-was-wrong-2ghj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/26/claude-code-3-month-billing-postmortem/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This past week, chasing a vague "quota burns faster lately" feeling, I scanned three months of my own Claude Code logs. ~\$127K equivalent cost, 127K turns, four models, hundreds of sessions.&lt;/p&gt;

&lt;p&gt;The uncomfortable finding: &lt;strong&gt;the cost-saving tips floating around on Reddit / HN / Twitter mostly don't survive real data.&lt;/strong&gt; "Sessions are too long, run &lt;code&gt;/clear&lt;/code&gt;," "too many skills, prune them," "MCP servers should be lean" — these all sound right. But against three months of actual data, &lt;strong&gt;almost none holds up.&lt;/strong&gt; Only two things actually shrink the bill, and neither is about "optimizing your habits."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Earlier I wrote &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Scanned 95 days of Claude Code logs, found a second cache TTL silent regression&lt;/a&gt; and &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;17-day follow-up&lt;/a&gt; covering server-side behavior. This post is the extension: with server behavior confirmed unfixable, what's left for the user side.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Three Months of Bills
&lt;/h2&gt;

&lt;p&gt;A single primary development project (one codebase, solo dev), monthly Claude Code totals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Equiv \$&lt;/th&gt;
&lt;th&gt;Dominant Model&lt;/th&gt;
&lt;th&gt;Key Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-02&lt;/td&gt;
&lt;td&gt;\$1,015&lt;/td&gt;
&lt;td&gt;Five models mixed&lt;/td&gt;
&lt;td&gt;Trial period, low volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-03&lt;/td&gt;
&lt;td&gt;\$48,623&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99.6% Opus 4.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy usage starts; per-call prefix jumped 58K → 417K in one step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-04&lt;/td&gt;
&lt;td&gt;\$77,754&lt;/td&gt;
&lt;td&gt;Opus 4.6 \$51K + Opus 4.7 \$25K&lt;/td&gt;
&lt;td&gt;Opus 4.7 release on 4/16, alias auto-upgraded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two key observations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;From March to April, Opus 4.6 cost barely changed&lt;/strong&gt; (\$48K → \$51K, +7%). \$/turn went from \$0.692 → \$0.713, a 3% delta. Usage habits stayed flat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The extra \$25K in April is almost entirely the Opus 4.7 layer.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the "lately it got expensive" feeling isn't because I changed anything — it's because &lt;strong&gt;Opus 4.7 shipped on 4/16 and the &lt;code&gt;opus&lt;/code&gt; alias automatically pointed to the new version.&lt;/strong&gt; With no version pinned in settings, the next session jumped to it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is normal alias behavior, not something hidden. But for subscription users, the quota impact is real — as we'll see, the new version's adaptive thinking burns quota at 2.4× the old.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Multi-Dimensional Breakdown for April
&lt;/h2&gt;

&lt;p&gt;Here's the full cut by model for one month:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Opus 4.6&lt;/th&gt;
&lt;th&gt;Opus 4.7&lt;/th&gt;
&lt;th&gt;Sonnet 4.6&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions (main/sub)&lt;/td&gt;
&lt;td&gt;24/138&lt;/td&gt;
&lt;td&gt;18/84&lt;/td&gt;
&lt;td&gt;5/46&lt;/td&gt;
&lt;td&gt;1/376&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total turns&lt;/td&gt;
&lt;td&gt;72,431&lt;/td&gt;
&lt;td&gt;31,621&lt;/td&gt;
&lt;td&gt;15,182&lt;/td&gt;
&lt;td&gt;16,138&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;% of total turns&lt;/td&gt;
&lt;td&gt;47.4%&lt;/td&gt;
&lt;td&gt;20.7%&lt;/td&gt;
&lt;td&gt;9.9%&lt;/td&gt;
&lt;td&gt;10.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall-clock hours&lt;/td&gt;
&lt;td&gt;635&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active hours (no idle)&lt;/td&gt;
&lt;td&gt;237.9&lt;/td&gt;
&lt;td&gt;106.5&lt;/td&gt;
&lt;td&gt;40.2&lt;/td&gt;
&lt;td&gt;6.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output Profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turns/active hour&lt;/td&gt;
&lt;td&gt;305&lt;/td&gt;
&lt;td&gt;297&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;378&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,614&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools/turn&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;0.64&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens/turn&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;667&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;456&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub:Main turn ratio&lt;/td&gt;
&lt;td&gt;1:1.32&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1:15.56&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1:14.95&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equivalent \$&lt;/td&gt;
&lt;td&gt;\$51,700&lt;/td&gt;
&lt;td&gt;\$24,595&lt;/td&gt;
&lt;td&gt;\$773&lt;/td&gt;
&lt;td&gt;\$114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost share&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;31.9%&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quota burn rate&lt;/td&gt;
&lt;td&gt;1.0×&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.4×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.2×&lt;/td&gt;
&lt;td&gt;0.05×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\$/turn&lt;/td&gt;
&lt;td&gt;\$0.714&lt;/td&gt;
&lt;td&gt;\$0.778&lt;/td&gt;
&lt;td&gt;\$0.051&lt;/td&gt;
&lt;td&gt;\$0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cross-column observations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opus 4.7 emits 2.9× more output tokens per turn&lt;/strong&gt; (667 vs 227). It's not verbose — adaptive thinking's reasoning chain counts as output. To complete the same task, 4.7 burns roughly 3× the output of 4.6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opus 4.7 doesn't delegate.&lt;/strong&gt; Sub:Main turn ratio jumped from 4.6's 1:1.32 to 1:15.56 — 4.6 is a "give half to sub-agents" collaborator, 4.7 is a "think it through alone" lone wolf. This explains the 3× output per turn: thinking is all done in-house.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sonnet 4.6 \$/turn is 1/16 of Opus.&lt;/strong&gt; But it only made up 9.9% of turns — clearly underused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Haiku is the invisible workhorse.&lt;/strong&gt; Zero main sessions, 376 sub-sessions, 16K turns for \$114 — all triggered automatically by Claude Code's built-in Explore / Plan agents. Untouched, still doing 10% of total turns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Common "Cost-Saving Tips" Debunked
&lt;/h2&gt;

&lt;p&gt;Community lore (Reddit / HN / Discord) graded against real data.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ "Long sessions are the culprit"
&lt;/h3&gt;

&lt;p&gt;The intuition: longer sessions mean longer conversation history, more cache prefix re-read per turn, more cost as the session drags on.&lt;/p&gt;

&lt;p&gt;The data: March vs April Opus 4.6 usage is nearly identical (69,980 vs 72,510 turns), but \$/turn moved from \$0.692 → \$0.713, a 3% bump. If long sessions were the driver, &lt;strong&gt;the per-turn cost should creep up month over month&lt;/strong&gt;. It doesn't.&lt;/p&gt;

&lt;p&gt;More precisely: cache_read accounts for 77–88% of cost on both Opus versions. The number is huge, but the ratio &lt;strong&gt;has been that way since heavy Claude Code usage started&lt;/strong&gt; — it's the inherent cost of "talking to an LLM," not the price of "not splitting sessions." &lt;code&gt;/clear&lt;/code&gt; doesn't recover much.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ "Run &lt;code&gt;/clear&lt;/code&gt; after 5+ min idle"
&lt;/h3&gt;

&lt;p&gt;The intuition: 5-minute cache TTL means a brief idle expires the cache, so the next turn pays for a rewrite.&lt;/p&gt;

&lt;p&gt;The data: my &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;second audit&lt;/a&gt; shows the main agent has been writing 100% to 1h TTL for 17 straight days since 4/9, with &lt;strong&gt;zero 5m writes&lt;/strong&gt;. Idle a while and come back, cache is still there. No extra write cost.&lt;/p&gt;

&lt;p&gt;The forced 5m downgrade only hits sub-agents (same post). But sub-agents only contributed a small slice of April's cost (~\$1,500 estimated), two orders of magnitude less than the \$25K from 4.7.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ "Too many skills"
&lt;/h3&gt;

&lt;p&gt;The intuition: loaded skills inject metadata into the system prompt every turn.&lt;/p&gt;

&lt;p&gt;The data: I actually measured. 40 skill descriptions add up to ~5–10K tokens. In a 425K per-call prefix, that's 1–2%. &lt;strong&gt;Deleting all of them saves &amp;lt;\$1K/month&lt;/strong&gt; — not worth the effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ "Too many MCP servers"
&lt;/h3&gt;

&lt;p&gt;The intuition: MCP tool definitions land in the prefix every turn.&lt;/p&gt;

&lt;p&gt;The data: setup is 3–4 MCPs (pixel-mcp, the Google Workspace trio), several of which fail to connect and don't load. Already lean, nothing to trim.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ "CLAUDE.md is too long"
&lt;/h3&gt;

&lt;p&gt;The intuition: CLAUDE.md gets re-read every turn.&lt;/p&gt;

&lt;p&gt;The data: the project root CLAUDE.md is &lt;strong&gt;1 byte&lt;/strong&gt; (essentially empty), the global one is 0 bytes. Zero impact.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These five aren't wrong in every scenario. For someone with a 50K-token CLAUDE.md or 20 loaded MCP servers, they apply. But as &lt;strong&gt;generic advice spread to everyone&lt;/strong&gt;, data shows they barely help a heavy single-project workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ✅ The Two Things That Actually Work
&lt;/h2&gt;

&lt;p&gt;After the intuition reckoning, only two things hold up against the data:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pin Specific Model Versions in settings.json
&lt;/h3&gt;

&lt;p&gt;Don't use &lt;code&gt;opus&lt;/code&gt; / &lt;code&gt;sonnet&lt;/code&gt; aliases. When Anthropic ships a new version, the alias auto-points to it — invisible to the user but quota behavior shifts dramatically.&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...your existing..."&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;This way when opus-4.8 / 4.9 ships, you don't auto-follow. New versions &lt;strong&gt;aren't always more economical&lt;/strong&gt; — for 4.6 vs 4.7:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;\$/turn +9%&lt;/li&gt;
&lt;li&gt;Output/turn +190%&lt;/li&gt;
&lt;li&gt;Quota burn +140%&lt;/li&gt;
&lt;li&gt;Turns to complete same work only −12%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Net CP value is 1.9× higher on 4.6. Every model release, check &lt;a href="https://github.com/cnighswonger/claude-code-cache-fix" rel="noopener noreferrer"&gt;cnighswonger's advisory&lt;/a&gt; and run your own data for a while before deciding to upgrade.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On adaptive thinking&lt;/strong&gt;: 4.7 burns hard mainly because adaptive thinking counts the reasoning chain as output tokens. Opus 4.6 / Sonnet 4.6 let you disable it via &lt;code&gt;CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1&lt;/code&gt;, but &lt;strong&gt;Opus 4.7 forces it on with no toggle&lt;/strong&gt; — that's why "lock 4.6" is the practical fix instead of trying to mitigate 4.7. Auto mode and adaptive thinking are independent features; pinning 4.6 doesn't affect auto mode.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Route Review / Fix / Test to Sonnet
&lt;/h3&gt;

&lt;p&gt;The \$/turn gap is real (Opus \$0.71 vs Sonnet \$0.045 — 16×). My April: 14K turns on Sonnet for \$643, same turns on Opus 4.6 would have been \$10K.&lt;/p&gt;

&lt;p&gt;Switch to Sonnet for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code review, reading PR diffs&lt;/li&gt;
&lt;li&gt;Small bug fixes, type annotations, null checks&lt;/li&gt;
&lt;li&gt;Writing tests, adding test cases&lt;/li&gt;
&lt;li&gt;Docs, commit messages, changelogs&lt;/li&gt;
&lt;li&gt;Renames, simple refactors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay on Opus for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-file architectural rewrites&lt;/li&gt;
&lt;li&gt;Design decisions needing long reasoning chains&lt;/li&gt;
&lt;li&gt;Complex debugging (race conditions, memory leaks)&lt;/li&gt;
&lt;li&gt;Exploring an unfamiliar codebase the first time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How: inside a session, &lt;code&gt;/model claude-sonnet-4-6&lt;/code&gt; to switch over for a few rounds, then &lt;code&gt;/model claude-opus-4-6&lt;/code&gt; to switch back. &lt;strong&gt;Don't lock Sonnet in settings&lt;/strong&gt; — you'll forget to switch when you need Opus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Magnitudes
&lt;/h2&gt;

&lt;p&gt;If both levers are in place, expected April-baseline change (against \$77K):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Expected Savings&lt;/th&gt;
&lt;th&gt;% of Monthly Bill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pin 4.6 (cancel 4.7 auto-follow)&lt;/td&gt;
&lt;td&gt;\$25K/mo&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route review/fix/test to Sonnet (expand to 30% of turns)&lt;/td&gt;
&lt;td&gt;\$10–15K/mo&lt;/td&gt;
&lt;td&gt;13–20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;\$35–40K/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45–52%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The remaining 50% is the inherent cost of "heavy Opus 4.6 usage on a primary project" — not optimizable, and shouldn't be. That's the work itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;p&gt;The biggest takeaway from turning myself into a dataset isn't the savings — it's seeing &lt;strong&gt;how unreliable community intuition is&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"Shorter session = cheaper," "fewer skills = cleaner" might hold in some scenarios, but &lt;strong&gt;for single-project heavy-use workflows they're flat wrong&lt;/strong&gt;. Without breaking cost down to model × session × turn, I'd never have spotted that "the Opus 4.7 alias upgrade" is the single biggest reason April got expensive.&lt;/p&gt;

&lt;p&gt;Broader lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Floating optimization tips are noise&lt;/strong&gt; — without data, "cost-saving advice" often optimizes the wrong thing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aliases hand cost control to the vendor&lt;/strong&gt; — the mechanism isn't bad, but it's a real risk for subscription users with quota planning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-model strategy beats single-model tuning&lt;/strong&gt; — same dollar, Sonnet does 16× the turn volume&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to scan your own, the 60-line Python from &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;the first post&lt;/a&gt; is reusable — adjust the cost calc and you'll get this analysis for your data. Make yourself a dataset and re-check what the community thinks it knows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Background: Claude Code session cost &amp;amp; cache misconception&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/issues/46829" rel="noopener noreferrer"&gt;Cache TTL silently regressed — GitHub Issue #46829&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/50213" rel="noopener noreferrer"&gt;Subagent trailing block missing cache_control — Issue #50213&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/41930" rel="noopener noreferrer"&gt;Widespread quota drain since 2026-03-23 — Issue #41930&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cnighswonger/claude-code-cache-fix" rel="noopener noreferrer"&gt;cnighswonger/claude-code-cache-fix&lt;/a&gt; — Opus 4.7 quota burn advisory + cache fix proxy&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Claude API Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/2026/04/13/claude_code_cache_confusion/" rel="noopener noreferrer"&gt;Anthropic: Claude quota drain not caused by cache tweaks — The Register&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>promptcaching</category>
      <category>agents</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>12 More Days Scanned: Claude Code Sub-Agent Cache TTL Has Been 100% 5m for 17 Straight Days — This Isn''t a Regression, It''s the New Default</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 25 Apr 2026 23:16:26 +0000</pubDate>
      <link>https://dev.to/recca0120/12-more-days-scanned-claude-code-sub-agent-cache-ttl-has-been-100-5m-for-17-straight-days-this-7ff</link>
      <guid>https://dev.to/recca0120/12-more-days-scanned-claude-code-sub-agent-cache-ttl-has-been-100-5m-for-17-straight-days-this-7ff</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/26/claude-code-cache-ttl-17-days/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Two weeks ago&lt;/a&gt; I scanned 95 days of Claude Code logs and found that since 4/9, sub-agents had been 100% downgraded to 5m TTL — 5 consecutive days, 4,840 API calls, with the main agent completely untouched. I left the conclusion at "monitoring," since 5 days could still be rollout flapping.&lt;/p&gt;

&lt;p&gt;Today 4/26, I re-ran the same Python. The streak is now &lt;strong&gt;17 days&lt;/strong&gt;, &lt;strong&gt;15,727 API calls, 0 1h writes&lt;/strong&gt;. This isn't flapping — Anthropic's server has quietly &lt;strong&gt;hard-coded the sub-agent default TTL to 5m&lt;/strong&gt;. No changelog, no announcement, and the main issue was just closed without resolution.&lt;/p&gt;

&lt;p&gt;This is a follow-up: latest data, cost math, community and media state, and why cnighswonger's proxy can't save you here either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Past Two Weeks of Data
&lt;/h2&gt;

&lt;p&gt;Scan covers 4/13–4/25 (cut-off of last post → today):&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;Main agent&lt;/th&gt;
&lt;th&gt;Sub-agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total API calls&lt;/td&gt;
&lt;td&gt;60,291&lt;/td&gt;
&lt;td&gt;15,727&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1h writes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100%&lt;/strong&gt; (150.7M tokens)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5m writes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;100%&lt;/strong&gt; (60.4M tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consecutive 1h-write days&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consecutive 5m-write days&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add the 4/9–4/12 stretch and &lt;strong&gt;the sub-agent has run 17 straight days at 100% 5m, with 0 1h writes&lt;/strong&gt;. Sub-agent workload didn't drop — 4/14 (the day I posted last) hit 2,648 calls, 4/17 spiked to 2,821, both two-week highs. The full cost impact landed on me.&lt;/p&gt;

&lt;p&gt;Key contrast: &lt;strong&gt;the main agent stayed 100% 1h the entire time, untouched&lt;/strong&gt;. So this is unambiguously server-side discrimination against the "sub-agent identity" — not quota throttling, not a client version, not a workflow change.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much More Expensive: The Math
&lt;/h2&gt;

&lt;p&gt;Anthropic's official cache pricing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache write to 5m TTL: &lt;strong&gt;1.25× base input price&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Cache write to 1h TTL: &lt;strong&gt;2× base input price&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Cache read (both): &lt;strong&gt;0.1× base input price&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Intuition says 5m writes are cheaper — 1.25× vs 2×, a 37.5% saving. But sub-agent workflows defeat that intuition.&lt;/p&gt;

&lt;p&gt;A typical sub-agent runs 30 minutes, 5 turns. Between turns it waits for the LLM to think, runs tools, parses results. &lt;strong&gt;3 inter-turn gaps over 5 minutes&lt;/strong&gt; is normal. Each gap past TTL expires the cache and forces a rewrite next turn.&lt;/p&gt;

&lt;p&gt;Total cost (with base input as 1×):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old (1h TTL):
  1 cache write @ 2×  = 2.0
  4 cache reads @ 0.1× = 0.4
  Total = 2.4×

New (5m TTL):
  4 cache writes @ 1.25× = 5.0
  1 cache read   @ 0.1× = 0.1
  Total = 5.1×
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;About 2.1×&lt;/strong&gt;. A heavy sub-agent workflow (parallel Task fan-out, long plan-execute, code-review pipelines) that used to cost \$10 now costs \$21.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This assumes inter-turn gaps average over 5m. If your sub-agent finishes every turn within 5m (e.g. pure retrieval), the impact is much smaller. The hardest-hit are sub-agents that "run long, wait for tool results."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  GitHub Activity Past Week
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue #46829: Closed by Anthropic
&lt;/h3&gt;

&lt;p&gt;cnighswonger's &lt;a href="https://github.com/anthropics/claude-code/issues/46829" rel="noopener noreferrer"&gt;#46829&lt;/a&gt; was &lt;strong&gt;closed by Anthropic without a fix&lt;/strong&gt;. Comments are uniformly angry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DaQue&lt;/strong&gt;: "I don't like the stealth nerf."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rinchen&lt;/strong&gt;: "Yet another issue closed without resolution by Anthropic."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lizthegrey&lt;/strong&gt; (Engineering Director at Honeycomb, jumped in 4/25): posted her own grep one-liner, listed her affected versions and dates (4/01 v2.1.81, 4/09 v2.1.85, 4/13–4/17 v2.1.92, 4/21 v2.1.114), and explicitly stated she &lt;strong&gt;provided redacted jsonl transcripts to Anthropic&lt;/strong&gt;. The most credible piece of evidence submitted so far.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# lizthegrey's one-liner&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'ephemeral_.*_input_tokens'&lt;/span&gt; ~/.claude | &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="s1"&gt;'select(.isSidechain == false and (.message.model | startswith("claude-haiku") | not) and .message.usage.cache_creation.ephemeral_5m_input_tokens &amp;gt; 0) | .timestamp + "," + .version'&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/T.*,/,/'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same data source as my 60-line Python from the last post, just more concise. Drop-in usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue #50213: Sub-agent Trailing Block Missing cache_control
&lt;/h3&gt;

&lt;p&gt;ofekron added measurements on &lt;a href="https://github.com/anthropics/claude-code/issues/50213" rel="noopener noreferrer"&gt;#50213&lt;/a&gt; on 4/17: every built-in sub-agent (Explore, Plan, general-purpose) shows nonzero &lt;code&gt;cache_creation&lt;/code&gt; on second spawn — the trailing system-context block has no cache_control marker, so each fresh spawn wastes ~4.7K tokens rewriting. &lt;strong&gt;0 new comments past week&lt;/strong&gt; — this issue is being ignored.&lt;/p&gt;

&lt;p&gt;Together the two issues say the same thing: &lt;strong&gt;Anthropic's posture toward sub-agent cache leans toward "save where we can," not "optimize where we can."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  No Movement from Anthropic Staff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;bcherny&lt;/strong&gt;'s earlier mention of a "per-request env var / flag for TTL" — &lt;strong&gt;still not shipped&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jarred Sumner&lt;/strong&gt;'s earlier defense in The Register that "sub-agent 5m is a one-shot optimization" — &lt;strong&gt;no response to the 4/9 100% 5m data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic posted nothing on these issues in the past week&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Update (2026-04-26): Official Position vs My Data
&lt;/h2&gt;

&lt;p&gt;After publishing, I dug into Anthropic's public posture. Boris Cherny (creator of Claude Code), via The Register:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;strong&gt;One-hour cache has been implemented in some places for subscribers&lt;/strong&gt;, while a &lt;strong&gt;five-minute cache is the true default.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So Anthropic's official line is "5m is the true default; 1h is opt-in for some subscriber scenarios" — which actually &lt;strong&gt;agrees&lt;/strong&gt; with this post's framing of "this is the new default, not a regression."&lt;/p&gt;

&lt;p&gt;But the official stance &lt;strong&gt;can't explain one thing&lt;/strong&gt;: time-series data from &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;the first audit&lt;/a&gt; shows that from 2026-02-07 to 03-05, 28 consecutive days, sub-agents received 100% 1h (not mixed, not 50%). If those 28 days of "1h treatment" were a "special case," it was a stably-allocated special case, not an occasional gift.&lt;/p&gt;

&lt;p&gt;This post's 17 days of 100% 5m can be re-positioned: &lt;strong&gt;the sub-agent 1h treatment subscribers used to receive is being stably revoked.&lt;/strong&gt; Anthropic didn't "change the default," but the "1h special case formerly granted to sub-agents" effectively disappeared. That's a fact the official statement can't paper over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media Coverage and a Bigger Thread
&lt;/h2&gt;

&lt;p&gt;This isn't just blowing up on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Register (4/13)&lt;/strong&gt;: &lt;a href="https://www.theregister.com/2026/04/13/claude_code_cache_confusion/" rel="noopener noreferrer"&gt;Anthropic: Claude quota drain not caused by cache tweaks&lt;/a&gt; — Anthropic publicly denies a cache link, with Sumner's defense quoted in full&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XDA Developers&lt;/strong&gt;: &lt;a href="https://www.xda-developers.com/anthropic-quietly-nerfed-claude-code-hour-cache-token-budget/" rel="noopener noreferrer"&gt;Anthropic quietly nerfed Claude Code's 1-hour cache&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps.com&lt;/strong&gt;: &lt;a href="https://devops.com/claude-code-quota-limits-usage-problems/" rel="noopener noreferrer"&gt;Developers Using Anthropic Claude Code Hit by Token Drain Crisis&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worth tracking: &lt;a href="https://github.com/anthropics/claude-code/issues/41930" rel="noopener noreferrer"&gt;Issue #41930&lt;/a&gt; — &lt;strong&gt;since 3/23 every paid tier has been hit by abnormal quota burn&lt;/strong&gt;, Pro / Max 5× / Max 20× included. Single prompts eat 3–7% of session quota; 5h windows drain in as little as 19 minutes. The community treats cache TTL regression, autocompact cascades, and sub-agent fan-out as &lt;strong&gt;stacked root causes&lt;/strong&gt;. My 4/9 second-wave finding fills in the timeline of "sub-agent specifically got worse again on 4/9."&lt;/p&gt;

&lt;h2&gt;
  
  
  Can cnighswonger's Proxy Save This? My Take
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cnighswonger/claude-code-cache-fix" rel="noopener noreferrer"&gt;cnighswonger/claude-code-cache-fix&lt;/a&gt; v3.0.3 has nice A/B numbers on CC v2.1.117: through the proxy &lt;strong&gt;95.5% cache hit rate&lt;/strong&gt;, direct &lt;strong&gt;82.3%&lt;/strong&gt;. It runs 7 hot-reloadable extensions, including &lt;code&gt;ttl-management&lt;/code&gt;, which "detects server TTL tier and injects correct cache_control markers."&lt;/p&gt;

&lt;p&gt;But for the "server force-writes sub-agent into 5m" problem, &lt;strong&gt;the proxy probably can't save you&lt;/strong&gt;. My read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The proxy fixes &lt;strong&gt;"caches that should hit but miss because of client bugs"&lt;/strong&gt; (unstable fingerprint, non-deterministic tool ordering, inconsistent cache_control markers)&lt;/li&gt;
&lt;li&gt;It can't fix &lt;strong&gt;"client marks 1h, server still writes 5m"&lt;/strong&gt; — that's server-side behavior, the proxy can't rewrite responses&lt;/li&gt;
&lt;li&gt;From our 17 days of 100% 5m / 0 1h writes, the server is doing the latter for sub-agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Easy to verify: install the proxy, run the same script against &lt;code&gt;~/.claude/projects/*.jsonl&lt;/code&gt;, see if sub-agent &lt;code&gt;ephemeral_1h_input_tokens&lt;/code&gt; ever goes from 0 to nonzero. If it stays 0, the server-side change is confirmed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This isn't a knock on cnighswonger's proxy — it has demonstrated value for the main agent and any cache-miss scenario. Just don't expect it to "bring back sub-agent 1h TTL."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion: This Is the New Default
&lt;/h2&gt;

&lt;p&gt;In the 4/14 post I called the 4/9 wave "a second silent regression." On 4/26 I'm revising the wording: &lt;strong&gt;this is no longer a regression — it's Anthropic's new default for sub-agents.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Evidence weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;17 consecutive days&lt;/strong&gt; (4/9–4/25)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15,727 API calls&lt;/strong&gt; in just the past 13 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0 1h writes&lt;/strong&gt; (not low — actually zero)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Main agent untouched&lt;/strong&gt; (clear differential treatment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media + GitHub + community on fire&lt;/strong&gt;, Anthropic stays silent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you lean heavily on sub-agents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan your own data first&lt;/strong&gt; — use the Python from the last post, or lizthegrey's jq one-liner above&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculate the actual cost impact&lt;/strong&gt; — it's not "a bit more," it's about 2×&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-evaluate your sub-agent workflows&lt;/strong&gt; — anything doable in the main agent shouldn't fan out to sub-agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop a data point on &lt;a href="https://github.com/anthropics/claude-code/issues/46829" rel="noopener noreferrer"&gt;issue #46829&lt;/a&gt;&lt;/strong&gt; — closed but still indexed. With Honeycomb-tier voices already pushing, more data makes external coverage easier to follow up&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;Background — Claude Code session cost &amp;amp; cache misconception&lt;/a&gt; covers the cache cost logic. &lt;a href="https://dev.to{{&amp;lt;%20ref%20"&gt;}}"&amp;gt;First audit&lt;/a&gt; covers how to scan your own logs to verify. Read both for the full picture.&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/issues/46829" rel="noopener noreferrer"&gt;Cache TTL silently regressed — GitHub Issue #46829&lt;/a&gt; — closed, community still commenting&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/claude-code/issues/50213" rel="noopener noreferrer"&gt;Subagent trailing block missing cache_control — Issue #50213&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/41930" rel="noopener noreferrer"&gt;Widespread quota drain since 2026-03-23 — Issue #41930&lt;/a&gt; — parent issue with stacked root causes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theregister.com/2026/04/13/claude_code_cache_confusion/" rel="noopener noreferrer"&gt;Anthropic: Claude quota drain not caused by cache tweaks — The Register&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xda-developers.com/anthropic-quietly-nerfed-claude-code-hour-cache-token-budget/" rel="noopener noreferrer"&gt;Anthropic quietly nerfed Claude Code's 1-hour cache — XDA Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devops.com/claude-code-quota-limits-usage-problems/" rel="noopener noreferrer"&gt;Developers Hit by Token Drain Crisis — DevOps.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/whoffagents/claude-prompt-caching-in-2026-the-5-minute-ttl-change-thats-costing-you-money-4363"&gt;The 5-Minute TTL Change That's Costing You Money — dev.to&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cnighswonger/claude-code-cache-fix" rel="noopener noreferrer"&gt;cnighswonger/claude-code-cache-fix&lt;/a&gt; — proxy + extension package&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>promptcaching</category>
      <category>agents</category>
      <category>python</category>
    </item>
    <item>
      <title>Node.js spawn stdout Gets Truncated: Compared 6 Fixes, Only the File Trick Works</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Sat, 25 Apr 2026 21:27:12 +0000</pubDate>
      <link>https://dev.to/recca0120/nodejs-spawn-stdout-gets-truncated-compared-6-fixes-only-the-file-trick-works-3o95</link>
      <guid>https://dev.to/recca0120/nodejs-spawn-stdout-gets-truncated-compared-6-fixes-only-the-file-trick-works-3o95</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/26/nodejs-spawn-stdout-truncated/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One day I was spawning a CLI from Node (the Claude CLI, which dumps a lot of JSON) and piping its stdout back to parse. Short outputs were fine. But the moment output grew past a few hundred KB, the last few KB &lt;strong&gt;just disappeared&lt;/strong&gt; — JSON.parse blew up on the final line, and the truncation point shifted run to run.&lt;/p&gt;

&lt;p&gt;After digging through Node's official issues, Linux pipe docs, and community deep-dives, the verdict is blunt: &lt;strong&gt;this is a known Node behavior since 2015, and the only reliable pure-stdlib fix is writing to a temp file.&lt;/strong&gt; This post lays out the trade-offs across six approaches so you don't have to repeat the journey.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--big-output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// blows up on large outputs&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bigger the output, the higher the chance. MB-scale almost always truncates; hundreds of KB occasionally. The cut-off point isn't fixed — sometimes you get 1.2 MB, sometimes 1.18 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Truncates
&lt;/h2&gt;

&lt;p&gt;The root cause is how Node writes stdio. The child's stdout connects to a pipe, and &lt;strong&gt;writes to a pipe are async&lt;/strong&gt;. When the child calls &lt;code&gt;process.exit()&lt;/code&gt;, Node doesn't wait for buffered data to flush — the process exits immediately, and whatever sat in the pipe unread gets lost.&lt;/p&gt;

&lt;p&gt;If stdout is a TTY or a regular file, writes are sync and this never happens. The bug only triggers on "non-TTY, non-file fds" — pipes, FIFOs, and sockets.&lt;/p&gt;

&lt;p&gt;This was first tracked in &lt;a href="https://github.com/nodejs/node/issues/3669" rel="noopener noreferrer"&gt;Node issue #3669&lt;/a&gt; (2015), then revisited in &lt;a href="https://github.com/nodejs/node/issues/6379" rel="noopener noreferrer"&gt;#6379&lt;/a&gt; and &lt;a href="https://github.com/nodejs/node/issues/9633" rel="noopener noreferrer"&gt;#9633&lt;/a&gt;. The community consensus: &lt;strong&gt;user-land's only reliable workaround is writing to a file.&lt;/strong&gt; Core has no plans to change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Approaches Side by Side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Viability&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A. Temp file&lt;/td&gt;
&lt;td&gt;Pure stdlib&lt;/td&gt;
&lt;td&gt;One extra disk I/O, ~10ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. node-pty / get-pty-output&lt;/td&gt;
&lt;td&gt;Child sees a TTY&lt;/td&gt;
&lt;td&gt;Needs native build; CLI may inject ANSI codes that pollute JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. &lt;code&gt;F_SETPIPE_SZ&lt;/code&gt; to enlarge pipe&lt;/td&gt;
&lt;td&gt;Linux only&lt;/td&gt;
&lt;td&gt;macOS lacks the API; only delays the cut-off point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D. Named pipe (FIFO)&lt;/td&gt;
&lt;td&gt;Same dead end&lt;/td&gt;
&lt;td&gt;FIFOs are non-file fds too — same truncation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E. UNIX socket&lt;/td&gt;
&lt;td&gt;Same dead end&lt;/td&gt;
&lt;td&gt;Sockets are also non-file fds, async writes still truncate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F. Fix the child CLI itself&lt;/td&gt;
&lt;td&gt;Root cause&lt;/td&gt;
&lt;td&gt;Usually not under your control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why D / E Don't Work Either
&lt;/h3&gt;

&lt;p&gt;A common first thought: "Skip the pipe, use a FIFO or UNIX socket — that should work, right?" I tried it. Same truncation.&lt;/p&gt;

&lt;p&gt;The reason: "async pipe writes" isn't a property of pipes specifically — it's a property of &lt;strong&gt;non-file, non-TTY fds&lt;/strong&gt;. Linux and macOS route writes to such fds through the async path, and FIFOs and sockets fall in the same bucket. Identical behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why C Looks Promising but Isn't
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html" rel="noopener noreferrer"&gt;&lt;code&gt;fcntl(F_SETPIPE_SZ)&lt;/code&gt;&lt;/a&gt; can grow the Linux pipe buffer from 64 KB default to 1 MB (more than that needs root). Sounds great — fill the buffer big enough and nothing gets cut, right?&lt;/p&gt;

&lt;p&gt;Three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Linux only.&lt;/strong&gt; macOS doesn't have &lt;code&gt;F_SETPIPE_SZ&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only delays the cut-off.&lt;/strong&gt; Outputs &amp;gt; 1 MB still truncate — no real fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still needs a native binding.&lt;/strong&gt; &lt;code&gt;fcntl&lt;/code&gt; isn't exposed in Node — you'd write a C++ addon or use &lt;code&gt;ffi-napi&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want pure stdlib and cross-platform, this path is out.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Actually Works: Pipe stdout to a File
&lt;/h3&gt;

&lt;p&gt;The trick is to use &lt;code&gt;fs.openSync&lt;/code&gt; to grab a file fd and hand it to &lt;code&gt;spawn&lt;/code&gt;'s &lt;code&gt;stdio&lt;/code&gt; option. This way &lt;strong&gt;the child writes stdout directly to the file, bypassing any pipe&lt;/strong&gt; — writes are sync, &lt;code&gt;process.exit()&lt;/code&gt; won't truncate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawnSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;closeSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlinkSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tmpdir&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tmpdir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;`cli-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.out`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;openSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// stdio: [stdin, stdout, stderr]&lt;/span&gt;
  &lt;span class="c1"&gt;// Send child stdout straight to a file fd, no pipe in between&lt;/span&gt;
  &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--big-output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;closeSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no longer blows up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an async version, use &lt;code&gt;spawn&lt;/code&gt; + &lt;code&gt;child.on('close', ...)&lt;/code&gt;. Same principle: &lt;strong&gt;fd to a file, never a pipe.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If stderr is also high-volume, do the same with a second fd. &lt;code&gt;'inherit'&lt;/code&gt; forwards to the parent's stderr cleanly but you can't capture it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The ~10ms Disk I/O Cost
&lt;/h2&gt;

&lt;p&gt;The only downside is the extra disk I/O — about 10ms on SSD. For a CLI that runs for several seconds, this is noise. If you genuinely care about that 10ms, the only remaining path is &lt;a href="https://github.com/microsoft/node-pty" rel="noopener noreferrer"&gt;node-pty&lt;/a&gt;, but you'll deal with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native build (extra compile step in CI)&lt;/li&gt;
&lt;li&gt;Child sees a TTY and may inject ANSI color codes into stdout — strip them&lt;/li&gt;
&lt;li&gt;macOS and Windows backends differ (forkpty vs conpty), test both&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My take: if a temp file works, use a temp file. Trading 10ms to avoid a native dependency and ANSI pollution is an easy win.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/issues/3669" rel="noopener noreferrer"&gt;Node.js issue #3669 — process.stdout/.stderr may lose data on process.exit()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/issues/6379" rel="noopener noreferrer"&gt;Node.js issue #6379 — stdout/stderr buffering considerations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/issues/9633" rel="noopener noreferrer"&gt;Node.js issue #9633 — Output data lost from spawned process if process ends before all data read&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html" rel="noopener noreferrer"&gt;pipe(7) — Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/node-pty" rel="noopener noreferrer"&gt;microsoft/node-pty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/child_process.html" rel="noopener noreferrer"&gt;Node.js child_process documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>stdio</category>
      <category>spawn</category>
    </item>
  </channel>
</rss>
