WebFetch fails with HTTPS proxy (https:// scheme): certificate validation uses wrong servername
What's Wrong?
When using Claude Code SDK TypeScript (@anthropic-ai/claude-agent-sdk) with an HTTPS proxy that requires TLS encryption (https://proxy.example.com), the WebFetch tool fails with a certificate validation error. Claude Code appears to validate the proxy's certificate against the target hostname instead of the proxy hostname.
Our architecture requires https:// proxy scheme:
┌─────────────────┐ HTTPS ┌──────────────────┐ CONNECT ┌─────────────┐
│ E2B Sandbox │ ───────────────────────> │ HTTPS Proxy │ ──────tunnel───────> │ Target │
│ (external) │ TLS to proxy │ (GCP) │ │ httpbin.org │
└─────────────────┘ (public internet) └──────────────────┘ └─────────────┘
Our sandboxes run in E2B (external environment) and connect to our proxy in GCP over the public internet. TLS encryption to the proxy is required - using http:// is not acceptable.
Expected TLS validation:
| Step | Connection | Certificate | Should Validate Against |
|------|-----------|-------------|------------------------|
| 1 | Client → Proxy | Proxy's cert | Proxy hostname ✅ |
| 2 | Client → Target (tunnel) | Target's cert | Target hostname ✅ |
Actual behavior:
Claude Code validates the proxy's certificate against the target hostname, causing a mismatch.
Documentation ambiguity:
Per Claude Code Network Configuration, HTTPS proxy is "recommended", but the example uses http:// scheme:
export HTTPS_PROXY=http://username:password@proxy.example.com:8080
| Configuration | Connection to Proxy | Status |
|--------------|---------------------|--------|
| HTTPS_PROXY=http://proxy:8080 | Unencrypted | ✅ Works |
| HTTPS_PROXY=https://proxy:443 | TLS encrypted | ❌ Broken (this issue) |
What Should Happen?
When using an https:// proxy URL, Claude Code should correctly handle the two separate TLS connections:
- TLS connection TO the proxy:
- Validate proxy's certificate against proxy hostname
- Use
proxyTlsoptions
- TLS connection TO the target (through CONNECT tunnel):
- Validate target's certificate against target hostname
- Use
requestTlsoptions (withservernameset to target hostname or auto-detected)
Expected behavior:
- WebFetch successfully connects through the HTTPS proxy
- Domain safety check to
claude.aisucceeds - Target URL is fetched and content is returned
Currently: Claude Code appears to mix up the hostnames, validating certificates against the wrong hostname, causing TLS handshake failures.
Error Messages/Logs
Default Error (Misleading)
By default, Claude Code shows a generic error that hides the underlying TLS issue:
Unable to verify if domain httpbin.org is safe to fetch. This may be due to network
restrictions or enterprise security policies blocking claude.ai.
This appears because the domain safety check (https://claude.ai/api/web/domain_info?domain=httpbin.org) also fails with the same TLS error.
Underlying Error (Actual Cause)
When domain safety checking is bypassed (see below), the actual TLS error is revealed:
Hostname/IP does not match certificate's altnames:
Host: httpbin.org. is not in the cert's altnames: DNS:proxy.example.com
How to Reveal the Underlying Error
Option 1: Set skipWebFetchPreflight in Claude Code config:
{
"skipWebFetchPreflight": true
}
Option 2: Add claude.ai to NO_PROXY environment variable:
export NO_PROXY="claude.ai"
Steps to Reproduce
1. Configure Claude Code SDK with HTTPS proxy
export HTTPS_PROXY="https://user:pass@proxy.example.com:443"
2. Bypass domain safety check to reveal the actual error
By default, the error is masked by a misleading "Unable to verify domain" message. To see the actual TLS error, use one of:
export NO_PROXY="claude.ai"
Or set in Claude Code config:
{
"skipWebFetchPreflight": true
}
3. Run Claude Code SDK and use WebFetch tool
The WebFetch tool will fail with the certificate error when trying to fetch any HTTPS URL.
3. Standalone script illustrating the issue
The following script demonstrates the underlying TLS misconfiguration. It shows that setting servername to the proxy hostname (instead of target hostname) causes the exact same certificate error that Claude Code produces.
Save as claude-code-proxy-bug-repro.js and run with:
HTTPS_PROXY="https://user:pass@proxy.example.com:443" node claude-code-proxy-bug-repro.js
#!/usr/bin/env node
/**
* Reproduction script for Claude Code HTTPS proxy certificate validation bug
*/
import { ProxyAgent } from 'undici';
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy;
if (!proxyUrl) {
console.error('Error: HTTPS_PROXY environment variable not set');
console.error('Usage: HTTPS_PROXY="https://user:pass@proxy:port" node claude-code-proxy-bug-repro.js');
process.exit(1);
}
const proxyHostname = new URL(proxyUrl).hostname;
const targetUrl = process.argv[2] || 'https://httpbin.org/get';
const targetHostname = new URL(targetUrl).hostname;
console.log('='.repeat(70));
console.log('Claude Code HTTPS Proxy Bug Reproduction');
console.log('='.repeat(70));
console.log('Proxy:', proxyUrl.replace(/:[^:@]+@/, ':****@'));
console.log('Proxy hostname:', proxyHostname);
console.log('Target URL:', targetUrl);
console.log('Target hostname:', targetHostname);
console.log('Node version:', process.version);
console.log('='.repeat(70));
// Test 1: Default behavior - should work
async function test1_defaultBehavior() {
console.log('\n--- Test 1: Default behavior (no explicit servername) ---');
const agent = new ProxyAgent({ uri: proxyUrl });
try {
const response = await fetch(targetUrl, { dispatcher: agent });
console.log('Status:', response.status);
console.log('✅ SUCCESS');
return true;
} catch (error) {
console.error('❌ FAILED:', error.cause?.message || error.message);
return false;
}
}
// Test 2: Correct servername - should work
async function test2_correctServername() {
console.log('\n--- Test 2: Explicit CORRECT servername (target hostname) ---');
console.log('Setting servername to:', targetHostname);
const agent = new ProxyAgent({
uri: proxyUrl,
requestTls: { servername: targetHostname },
});
try {
const response = await fetch(targetUrl, { dispatcher: agent });
console.log('Status:', response.status);
console.log('✅ SUCCESS');
return true;
} catch (error) {
console.error('❌ FAILED:', error.cause?.message || error.message);
return false;
}
}
// Test 3: WRONG servername - REPRODUCES THE BUG
async function test3_wrongServername() {
console.log('\n--- Test 3: WRONG servername - REPRODUCES THE BUG ---');
console.log('Setting servername to PROXY hostname:', proxyHostname);
console.log('(This is WRONG - should be target hostname)');
const agent = new ProxyAgent({
uri: proxyUrl,
requestTls: { servername: proxyHostname },
});
try {
const response = await fetch(targetUrl, { dispatcher: agent });
console.log('Status:', response.status);
console.log('⚠️ UNEXPECTED SUCCESS');
return true;
} catch (error) {
console.error('❌ EXPECTED ERROR:', error.cause?.message || error.message);
return false;
}
}
// Run tests
const results = {
test1: await test1_defaultBehavior(),
test2: await test2_correctServername(),
test3: await test3_wrongServername(),
};
console.log('\n' + '='.repeat(70));
console.log('SUMMARY');
console.log('='.repeat(70));
console.log('Test 1 (default):', results.test1 ? '✅ PASS' : '❌ FAIL');
console.log('Test 2 (correct servername):', results.test2 ? '✅ PASS' : '❌ FAIL');
console.log('Test 3 (wrong servername - bug):', results.test3 ? '⚠️ UNEXPECTED' : '❌ EXPECTED FAIL');
if (!results.test3) {
console.log('\n🐛 Test 3 reproduces the bug: servername set to proxy hostname');
console.log(' causes TLS validation to fail against target certificate.');
}
3. Expected Output
======================================================================
Claude Code HTTPS Proxy Bug Reproduction
======================================================================
Proxy: https://user:****@proxy.example.com:443
Proxy hostname: proxy.example.com
Target URL: https://httpbin.org/get
Target hostname: httpbin.org
Node version: v24.2.0
======================================================================
--- Test 1: Default behavior (no explicit servername) ---
Status: 200
✅ SUCCESS
--- Test 2: Explicit CORRECT servername (target hostname) ---
Setting servername to: httpbin.org
Status: 200
✅ SUCCESS
--- Test 3: WRONG servername - REPRODUCES THE BUG ---
Setting servername to PROXY hostname: proxy.example.com
(This is WRONG - should be target hostname)
❌ EXPECTED ERROR: Hostname/IP does not match certificate's altnames: Host: proxy.example.com. is not in the cert's altnames: DNS:httpbin.org, DNS:*.httpbin.org
======================================================================
SUMMARY
======================================================================
Test 1 (default): ✅ PASS
Test 2 (correct servername): ✅ PASS
Test 3 (wrong servername - bug): ❌ EXPECTED FAIL
🐛 Test 3 reproduces the bug: servername set to proxy hostname
causes TLS validation to fail against target certificate.
Test 3 demonstrates the exact error seen in Claude Code's WebFetch tool.
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
anthropic-ai/claude-agent-sdk 0.2.1
Platform
Anthropic API
Operating System
Other Linux
Terminal/Shell
Other
Additional Information
Suggested Fix
When configuring the proxy agent for https:// proxy URLs, ensure that:
proxyTls.servername= proxy hostname (for TLS connection TO the proxy)requestTls.servername= target hostname (for TLS connection TO the target through the tunnel), or left undefined to auto-detect
Workarounds
None available for the core TLS issue with HTTPS proxies.
To reveal the actual error (for debugging), bypass domain safety checking:
- Set
"skipWebFetchPreflight": truein config, OR - Set
NO_PROXY="claude.ai"in environment
Not acceptable workarounds:
- Use
http://proxy URL - Not secure for public internet - Use TLS-terminating proxy - Changes security model, requires custom CA
Proxy Configuration
Our Squid proxy uses ssl_bump splice mode (CONNECT tunnel, no TLS interception):
acl step1 at_step SslBump1
ssl_bump peek step1
ssl_bump splice all
Environment
- Claude Code SDK:
@anthropic-ai/claude-agent-sdk": "0.2.1"(TypeScript) - Node.js version: v22
- OS: Linux (E2B sandbox)
- Proxy: Squid with HTTPS frontend, CONNECT tunnel mode
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗