●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
Passing the Request, Not the Secret — Where Sandbox Credential Masking Works and Where Substitution Breaks
Claude Code's sandbox credential masking lets processes read sentinel values while a proxy swaps in the real secret at send time. I rebuilt it as a minimal proxy and measured which auth schemes survive the swap, which break, and what happens when the secret rides in the body.
Denying reads is a strong control. But any job that legitimately calls an external API still needs the real token somewhere. Deny can hide the secrets a job does not need — it cannot protect the one secret the job actually uses.
Then the August 5, 2026 changelog landed with mode: "mask" for sandbox credentials on Linux and WSL, and one sentence stopped me: commands inside the sandbox read a sentinel copy of the value, and the sandbox proxy swaps in the real value at send time.
Not hiding the secret. Handing the process a decoy, then swapping in the original at the exit.
How far that design actually protects you is not something the docs alone can answer. So I rebuilt the core of the mechanism — the byte-level swap — as a minimal proxy and pushed on its boundaries myself.
From "Can't Read It" to "Reads a Decoy"
Deny and mask protect different things.
Deny controls exposure. It hides ~/.aws/credentials and unrelated environment variables from the sandbox — the same instinct as tightening filesystem isolation from the write surface: shrink what can be touched at all.
Mask protects the next layer. Even a secret the job legitimately uses is never visible to the process. What the process reads is a harmless string like SBX_SENTINEL_...; the real value only exists in the request for the instant it passes through the egress proxy.
The implication is significant. If prompt injection or malicious code runs inside the sandbox, all it can exfiltrate is the sentinel — a string with no authority anywhere in the outside world.
And yet: a mechanism defined as "replace bytes at send time" must have cases it structurally cannot cover. To find them, I wrote the smallest proxy that could.
Rebuilding the Mechanism in Miniature
My test environment is Python 3.10.12 in a Linux sandbox. Real implementations involve TLS handling and more, but here I isolate the one core operation — replacing sentinel bytes on the wire with the real value — over a plaintext path.
The naive version replaces within each received chunk and forwards it:
# Naive: substitute the sentinel per received chunkdef handle(client_sock): up = socket.create_connection((UPSTREAM_HOST, UPSTREAM_PORT)) client_sock.settimeout(0.5) try: while True: chunk = client_sock.recv(8192) if not chunk: break # Only sentinels fully contained in one chunk get replaced up.sendall(chunk.replace(SENTINEL, REAL_VALUE)) except socket.timeout: pass
It appears to work. But it has a classic stream-processing hole: the moment a sentinel straddles two recv chunks, neither chunk contains the complete pattern, and the substitution silently misses.
The fix is a rolling buffer that always retains a tail of length len(sentinel) - 1:
# Rolling buffer: catches sentinels split across chunk boundariesS = SENTINEL # byteskeep = len(S) - 1 # longest prefix that could straddle a boundarydef handle(client_sock): up = socket.create_connection((UPSTREAM_HOST, UPSTREAM_PORT)) client_sock.settimeout(0.5) buf = b"" try: while True: chunk = client_sock.recv(8192) if not chunk: break buf += chunk buf = buf.replace(S, REAL_VALUE) if len(buf) > keep: # Hold back the last `keep` bytes until the next chunk arrives up.sendall(buf[:-keep]) buf = buf[-keep:] except socket.timeout: pass if buf: # Final flush: replace anything left in the tail before sending up.sendall(buf.replace(S, REAL_VALUE))
Error handling is trimmed for clarity, but the two essentials are the carried-over tail and the final flush. Drop either one and you get a bug that only fires when a sentinel lands at the end of a request — the kind that is miserable to reproduce.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Learn to classify auth schemes into ones sentinel substitution can protect and ones it structurally cannot, backed by base64 and HMAC measurements
✦See why per-chunk replacement silently leaks sentinels that straddle recv boundaries, with reusable naive vs. rolling-buffer proxy code
✦Learn why a secret carried in the request body throws off the Content-Length declaration — a longer real value truncates into a JSONDecodeError, a shorter one blocks upstream for 3.003 seconds — plus the length-correction code and a five-step rollout checklist
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Measurement 1: Straddled Sentinels Slip Past Naive Replacement
I forced recv into 8-byte chunks so the 19-byte sentinel always straddles a boundary, put the sentinel in a Bearer header, and ran it through both proxies.
Implementation
Real value reached upstream
Sentinel leaked verbatim
Naive (per-chunk replace)
No
Yes
Rolling buffer
Yes
No
With the naive version, the upstream server's receive log contained the sentinel byte-for-byte. If you are ever tempted to hand-roll this kind of proxy, this single result is a good reason to reconsider. Stream substitution is a notch harder than it looks, and production implementations carry rolling-buffer logic for a reason.
Measurement 2: Transformed Secrets Cannot Be Swapped
This is where the results diverged from my expectations.
Substitution presupposes that the sentinel appears verbatim in the bytes on the wire. So what happens with auth schemes that transform the secret before sending?
First, Basic auth — user:password base64-encoded into a header. I put the sentinel in the password slot and sent it through the rolling-buffer proxy:
Authorization header received upstream, after base64 decode: user:SBX_SENTINEL_a7f3e9Swap performed: none (the sentinel survived inside the base64 envelope)
The proxy scans wire bytes, but what travels is the base64-transformed string. The sentinel's original form never appears, so there is nothing to replace. Upstream receives credentials that sincerely believe the sentinel is a password — and rejects them with a 401.
Next, HMAC signing (think AWS SigV4). I compared a signature keyed on the sentinel against one keyed on the real value:
First 16 hex chars of HMAC-SHA256("payload"): sentinel key: c22efffa06d956f2 real key: 37c6b2d919ceadfd
Different, of course. The signature is computed inside the sandbox, so a sentinel-keyed signature is what leaves. The proxy only sees the finished artifact passing by — it can swap the secret's raw form, never the computation the secret fed into. Verification fails every time.
So the comfortable reading — "enable masking and all secrets are protected" — turned out to be imprecise. Masking protects exactly those schemes where the secret appears verbatim on the wire. Schemes with encoding or signing in between are structurally out of reach. I doubt I would have extracted that from one line of documentation without running it.
Measurement 3: A Sentinel in the Body Throws Off the Length
Everything so far assumed the secret rides in a header. Reviewing my own jobs, though, one API takes the key inside the JSON request body. Whether that deserves the same treatment was worth checking.
Substitution replaces a sentinel with the real value. Their lengths almost never match. And the body's length has already been declared, up in the Content-Length header.
The moment you substitute, the declared length and the bytes actually on the wire disagree.
I built a JSON body containing the 19-byte sentinel and varied the length of the real value, watching what upstream did with each.
Real value length
Declared Content-Length
Body actually sent
Outcome upstream
32 bytes (longer than the sentinel)
73
86 bytes
Truncated at 73 bytes, JSONDecodeError
10 bytes (shorter than the sentinel)
52
43 bytes
Waited for 9 missing bytes, blocked 3.003 seconds
The longer case fails loudly. The body gets cut at the declared offset and the upstream JSON parser dies immediately. The cause may be obscure, but the failure itself is unmistakable.
The shorter case is the troublesome one. Upstream waits for all 52 declared bytes, and the 9 that never arrive hold the connection until timeout. It surfaces as latency, not as an error. In a job with retries, that degrades into a vague "sometimes it's slow."
Correcting Content-Length by the delta in the proxy resolves both:
import redef swap_with_length_fix(head: bytes, body: bytes) -> tuple[bytes, bytes]: """Adjust the declared length by the per-occurrence size delta, then substitute.""" delta = body.count(SENTINEL) * (len(REAL_VALUE) - len(SENTINEL)) if delta: head = re.sub( rb"(?i)Content-Length: *(\d+)", lambda m: b"Content-Length: " + str(int(m.group(1)) + delta).encode(), head, count=1, ) return head, body.replace(SENTINEL, REAL_VALUE)
With the correction in place, upstream saw 86 declared against 86 received and parsed the JSON cleanly. Production implementations surely handle this as a matter of course — but the shape of the problem is worth remembering: an obvious fact, that the sentinel and the real value differ in length, only bites on the body path.
Chunked transfer encoding (Transfer-Encoding: chunked) carries the same property in each chunk's size prefix. Anywhere a length is declared separately from the bytes it describes deserves the same suspicion.
Which Auth Schemes You Can Hand Over
Here is how the measurements sort things:
Auth scheme
Secret's form on the wire
Fit with mask mode
Bearer token in the Authorization header
Verbatim
Good
Custom headers such as X-API-Key
Verbatim
Good
Basic auth
Base64-encoded
Does not work
HMAC-signed requests (SigV4 and similar)
Transformed into a signature
Does not work
JWTs signed with a private key
Transformed into a signature
Does not work
In the scheduled pipelines I run as an indie developer, the secrets that matter are a GitHub PAT for pushes and an Anthropic API key — both verbatim header schemes, so both are a good fit for masking. If I ever wire up a signature-based API, the signing step will have to live outside the sandbox, likely combined with a design that leans on short-lived credentials.
Measured Overhead, and Five Steps Before Enabling
I also measured the scanning cost: 200 requests with 1KB bodies, direct versus through the proxy (loopback, plaintext — treat these as reference values).
Path
Median
p95
Direct
0.612 ms
0.848 ms
Via proxy (with substitution)
1.277 ms
1.547 ms
+0.665ms at the median — roughly 2.1x the direct path. Against real network round-trips of tens to hundreds of milliseconds, that disappears into noise for ordinary API calls. Only tight inner loops firing many small requests need to think about the accumulation.
Before enabling mask mode, I would work through this sequence:
List every external API your jobs call and its auth scheme. Split them into verbatim-header schemes versus encode/sign schemes
Put only the verbatim-scheme secrets under mask. Bearer-style tokens belong here
Keep transformed-scheme secrets on the deny/unset side. If signing is required, do it outside the sandbox
Set apart any API that carries the secret in the request body. Unlike verbatim headers, these add the question of keeping the declared length consistent (Measurement 3)
Watch upstream 401 rates and p95 latency together after the switch. Misclassification shows up as auth errors; a length mismatch shows up as delay
Latency joined step 5 because Measurement 3 broke one of my assumptions. When I started writing, I believed a masking misclassification never fails silently — that it always announces itself with a 401. On the header path, that holds. But on the body path with a shorter real value, it arrives as waiting time instead. One class of failure slips past anyone watching only for 401s.
Measure the Boundary, Then Delegate
Deny narrows the exposure surface, filesystem isolation narrows the write surface, and now mask keeps even the in-use secret away from the process. The sandbox's defenses are visibly becoming layered.
But the work of measuring each new layer's boundary cannot be skipped. Had I applied mask mode to a signature-based API without knowing that substitution only touches verbatim bytes, I would have lost hours to auth errors with no obvious cause. The length mismatch would have been quieter still.
Start by making the table for your own jobs: each secret, each auth scheme, transformed or not. Once the table is filled in, the range you can safely hand to masking becomes self-evident.
The verification code above is yours to reuse. If it shortens someone's debugging session even a little, writing this up was worth it.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.