[BUG] Cloud sandbox proxy resets Chromium's TLS 1.3 handshake; WebKit unaffected (corrects #11791)

Status Open
Reported on v2.1.251
Maintainer reply None cached
Activity 0 comments · opened Aug 29, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

In the Claude Code cloud sandbox, headless Chromium cannot complete a TLS 1.3 handshake through the egress proxy. Every external HTTPS host fails with net::ERR_CONNECTION_RESET — including hosts that are plainly allowlisted, and that curl and Node reach fine over the same proxy at the same moment.

WebKit is unaffected. Same proxy, same hosts, both navigation and in-page fetch, real 200s. Only Chromium fails, and only at TLS 1.3: capping it at TLS 1.2 makes the handshake succeed.

This corrects #11791, which is open and unanswered since Nov 2025. That issue concludes the proxy "does NOT support the HTTP CONNECT method" and that browser automation is fundamentally incompatible with the sandbox, recommending users switch to requests + BeautifulSoup. WebKit tunnelling successfully through that same proxy disproves it. The ERR_TUNNEL_CONNECTION_FAILED reported there is a 403 policy denial — a different failure from the reset described here, which is why the two look alike and aren't.

The practical cost: browser-based verification looks impossible in the sandbox when in fact WebKit works today and Chromium works with two flags.

What Should Happen?

Chromium should tunnel through the egress proxy the way WebKit, curl and Node all do — completing a TLS 1.3 handshake and returning the page.

Instead the handshake is reset mid-exchange, and a connection is only possible by capping the browser at TLS 1.2.

Error Messages/Logs

net::ERR_CONNECTION_RESET   https://registry.npmjs.org/   (chromium, default)
net::ERR_CONNECTION_RESET   https://api.anthropic.com/    (chromium, default)

Proxy-side log for the same attempt:
{ "kind": "ws_closed_mid_exchange",
  "detail": "tunnel closed (code 1006, Connection ended) after 6s; 1806 B sent, 39 B received, client reading" }

~1800 bytes out (a Chromium ClientHello), 39 bytes back, then the tunnel dies.

For contrast, a genuinely denied host fails DIFFERENTLY — this is a 403 to
CONNECT, not a reset:
net::ERR_TUNNEL_CONNECTION_FAILED   https://cdnjs.cloudflare.com/

Steps to Reproduce

  1. Open a Claude Code cloud (web) session with Playwright available.
  1. Save this as tls-probe.js:
const pw = require('playwright')
const PROXY = { server: process.env.HTTPS_PROXY, bypass: 'localhost,127.0.0.1' }

;(async () => {
  for (const [name, launch] of [
    ['headless shell (default)', () => pw.chromium.launch({ proxy: PROXY })],
    ['headless shell + tls1.2',  () => pw.chromium.launch({ proxy: PROXY, args: ['--ssl-version-max=tls1.2'] })],
    ['full chrome',              () => pw.chromium.launch({ channel: 'chromium', proxy: PROXY })],
    ['full chrome + tls1.2',     () => pw.chromium.launch({ channel: 'chromium', proxy: PROXY, args: ['--ssl-version-max=tls1.2'] })],
    ['webkit',                   () => pw.webkit.launch({ proxy: PROXY })],
  ]) {
    const b = await launch()
    const p = await (await b.newContext()).newPage()
    try { console.log(name, 'OK', (await p.goto('https://registry.npmjs.org/')).status()) }
    catch (e) { console.log(name, String(e).split('\n')[0]) }
    await b.close()
  }
})()
  1. Run node tls-probe.js.
  1. Compare against curl https://registry.npmjs.org/ through the same proxy — it returns 200.

Observed:

| binary | flag | result |
|---|---|---|
| chromium_headless_shell | (none) | ERR_CONNECTION_RESET |
| chromium_headless_shell | --ssl-version-max=tls1.2 | ERR_CONNECTION_RESET |
| full chrome | (none) | ERR_CONNECTION_RESET |
| full chrome | --ssl-version-max=tls1.2 | OK 200 |
| webkit | (none) | OK 200 |

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.251 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

WHAT ISOLATES IT

Capping at TLS 1.2 fixes it, so the failure is in the TLS 1.3 handshake specifically. Ruled out explicitly, so nobody re-treads them:

  • Not CA trust — identical reset with ignoreHTTPSErrors: true AND with --ignore-certificate-errors. It dies below certificate validation.
  • Not the allowlist — the same hosts return real HTTP to curl through the same proxy, and denied hosts fail differently (ERR_TUNNEL_CONNECTION_FAILED = 403 to CONNECT).
  • Not post-quantum key agreement — disabling PostQuantumKyber, X25519MLKEM768 and TLS13PostQuantumKeyAgreement changed nothing.
  • Not ECH — disabling EncryptedClientHello changed nothing.

Node's and curl's TLS 1.3 handshakes to the same hosts succeed, so it appears specific to how Chromium composes its ClientHello — plausibly fragmentation across TLS records, or an extension the gateway mishandles. This is a known class of middlebox bug (Chrome disabled TLS 1.3 in Chrome 56 over it), but this instance doesn't appear to be reported.

WHY THE KNOWN TLS-1.2 WORKAROUND APPEARS NOT TO WORK

chromium_headless_shell silently ignores --ssl-version-max. Playwright 1.49+ resolves a bare chromium.launch() to the headless shell, so anyone applying the documented mitigation gets an unchanged reset and concludes the mitigation is broken. It needs channel: 'chromium'. Rows 2 and 4 in the table above are the same flag against different binaries.

ENVIRONMENT NOTE

This is Claude Code on the web — a managed cloud container, not a local
terminal, which is why Terminal/Shell is "Other". Ubuntu 24.04. The CLI version
is whatever the platform provisions rather than something I control. The finding
is in the sandbox egress proxy, not the CLI, and is independent of CLI version.

Playwright 1.62.1, PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers.

ASKS

  1. Fix the gateway's TLS 1.3 negotiation with Chromium.
  2. Until then, document the WebKit path and the channel: 'chromium' + --ssl-version-max=tls1.2 fallback.
  3. Correct or supersede #11791.

View original on GitHub ↗