The cowork architecture page documented that the service managing the Claude Desktop
sandbox installs its own CA certificate inside the guest VM and performs full TLS
interception on traffic to *.anthropic.com. Every API call from inside the VM is
decrypted, inspectable, and re-encrypted before forwarding.
This breaks HTTPS's security guarantee in a specific and important way.
HTTPS does not encrypt traffic in an absolute sense. It encrypts traffic to a verified party. The verification step — confirming the server's certificate was signed by a trusted authority — is what prevents a man-in-the-middle from substituting their own endpoint. When an attacker installs their own CA into the trust store and uses it to issue certificates for the target domain, they become a trusted authority. The verification step passes. The attacker is in the middle.
The question this page answers is: given that this interception is present, how can a process verify that it is communicating with the real endpoint?
The normal TLS verification path consults the operating system's certificate trust store — or, in runtimes like Node.js, a bundled trust store that can be extended by environment variable. If the intercepting CA's certificate is present in either location, standard verification will succeed against the intercepted connection. The process has no way to distinguish the real endpoint from the proxy.
cowork.exponential-systems.net was intercepted by
O=Anthropic; CN=sandbox-egress-production TLS Inspection CA, yet
the TLS library reported SSL certificate verify ok. The page
documenting the interception was itself being intercepted.
The interception is not a bug in the proxy's implementation. It is the intended behavior of a CA-based trust model when the CA is controlled by a party in the network path.
Certificate pinning replaces the CA-based trust question ("was this cert signed by a trusted authority?") with an identity question ("does this cert match the specific cert I expect?"). A pinned fingerprint is a SHA-256 hash of the certificate's raw bytes. An attacker who substitutes a different certificate — even one signed by a CA in the trust store — produces a different hash. The substitution is detectable.
In Node.js, the standard https module reads HTTPS_PROXY at module
load time, not at connection time. An application that unsets HTTPS_PROXY at
runtime and then calls https.request() may still route through the proxy if
the module was already initialized with that variable set. Environment variable
manipulation at runtime is not a reliable defense in runtimes with module-level
initialization.
The bypass is to avoid the https module entirely for sensitive connections
and construct the TCP path manually:
1. net.createConnection(proxyHost, proxyPort)
// open TCP to proxy (or directly to target if no proxy)
2. write: "CONNECT targetHost:443 HTTP/1.1\r\nHost: targetHost\r\n\r\n"
// HTTP tunnel request
3. on 200 response: tls.connect({ socket, servername: targetHost, ... })
// TLS handshake over the raw socket
4. extract certificate fingerprint from tls.getPeerCertificate()
// compare SHA-256 against stored pin
This path gives the application direct control over every step of the connection. No environment variable at any layer can redirect it. The TLS handshake occurs directly against the target endpoint, and the resulting certificate is the one to pin against.
The same approach is necessary even in non-proxy scenarios when
NODE_EXTRA_CA_CERTS may have been injected: by constructing the TLS context
explicitly with a specific ca option, the application trusts only the CAs
it names rather than inheriting whatever the environment has added.
The cowork audit documented that the sandbox management process is spawned with
the full parent process environment. Every child process spawned with
{...process.env} or equivalent inherits all proxy variables and CA injection
variables the parent acquired — regardless of whether the child's own code is
otherwise clean.
The correct spawn contract for any process that must maintain a clean network context is explicit environment construction:
// Only enumerate what the child requires.
// Proxy and CA injection variables are absent by construction.
const childEnv = {
PATH: process.env.PATH,
HOME: process.env.HOME,
// ... additional variables the child specifically needs
};
const child = spawn(command, args, {
env: childEnv,
stdio: ['ignore', 'pipe', 'pipe'], // see next section
});
Absence is the safe default. Any variable not explicitly granted to the child is not present. The parent cannot accidentally propagate an injected proxy variable it is not aware of.
The cowork audit documented that the sandbox management process is spawned with
stdio: 'ignore' and immediately unref()'d. Both choices have security
implications beyond their operational convenience.
stdio: 'ignore' means the parent process has no channel to observe what the
child writes. A child that logs credentials, exfiltrates data, or signals errors
does so invisibly. The parent cannot distinguish a child that is functioning
correctly from one that is not.
unref() means the parent's event loop does not wait for the child. A child
that crashes, restarts, or continues running after the parent exits does so
without the parent's knowledge or control.
The alternative is a ref'd child with piped stdio:
child.stdout.on('data', d => log('[child:out]', d.toString().trim()));
child.stderr.on('data', d => log('[child:err]', d.toString().trim()));
child.on('exit', (code, signal) => {
log('[child] exited', code, signal);
// implement restart policy, parent alert, or clean shutdown here
});
// Tie child lifecycle to parent lifecycle
process.on('exit', () => child.kill('SIGTERM'));
app.on('before-quit', () => child.kill('SIGTERM'));
This pattern makes the child's behavior visible and ties its lifecycle to the parent's. A child that is doing something unexpected surfaces it through the accountability channel rather than into a void.
The cowork audit documents an interception architecture that operates at the CA trust layer. Standard TLS verification cannot detect it because standard verification trusts the installed CA. None of the following countermeasures require trusting any particular tooling or infrastructure. Each can be implemented from scratch using standard library primitives.
| Attack vector | Countermeasure |
|---|---|
| CA cert installed in trust store | Pin the real endpoint's leaf certificate SHA-256 fingerprint |
| Proxy env vars redirect the connection before TLS | Explicitly clear all proxy and CA injection env vars before connecting |
Node.js https module reads env at init time |
Bypass via raw net.createConnection → CONNECT → tls.connect |
{...process.env} spawn inherits injected vars |
Explicit env construction — vars absent by default |
stdio: 'ignore' + unref() hides child behavior |
Piped stdio, ref'd lifecycle, explicit shutdown on parent exit |