curl works but claude won't connect — the 5 distinct TLS/DNS causes behind Claude Code CLI failures in corporate/VPN environments
In locked-down environments, claude /login (or claude update) fails with CERT_HAS_EXPIRED, getaddrinfo EREFUSED, a 503, or EPERM — while curl to the same host returns a clean 200. The generic error collapses five distinct causes into one line and usually points you at "your proxy," which is wrong most of the time. Here's how to tell which one you actually have.
These mechanisms are relayed from incidents reported on the tracker in late June 2026 (numbers at the end); I helped triage each. I'm stating the mechanism, not claiming I reproduced every environment.
The native CLI runs on Bun, which validates TLS against its own bundled CA/trust store (BoringSSL) and, on some paths, resolves DNS through its own path rather than the OS getaddrinfo. So a clean curl -vI only proves the OS path is healthy — it says nothing about the path Bun builds. "curl works but the CLI doesn't" in a corp/VPN environment is almost always this mismatch, and the one-line error doesn't distinguish the causes below.
1. DNS — split-DNS VPN, internal resolver refuses public names. Symptom: getaddrinfo EREFUSED. EREFUSED is a c-ares code — the tell that the CLI (or its updater) read /etc/resolv.conf directly, hit the first listed (internal authoritative) server, got REFUSED, and stopped before reaching the systemd-resolved stub. Fix: update through the OS resolver path (curl -fsSL https://claude.ai/install.sh | bash, or temporarily front-load nameserver 127.0.0.53). /etc/hosts won't help — c-ares bypasses NSS, so the failing code ignores it. (#71699)
2. TLS — stale bundled CA store, or an expired cross-signed root in the chain. Symptom: CERT_HAS_EXPIRED with no proxy and a correct clock. EXPIRED (not UNABLE_TO_GET_ISSUER) means a cert in the built chain has a notAfter in the past — classically an expired cross-signed root that OpenSSL/curl routes around but the stricter bundled store stops at. Confirm: openssl s_client -connect platform.claude.com:443 -servername platform.claude.com -showcerts </dev/null | openssl x509 -noout -dates. Fix: update the native build (newer Bun → newer CA store), or pass a fresh cacert.pem via an absolute NODE_EXTRA_CA_CERTS. (#71708)
3. NODE_EXTRA_CA_CERTS set to a relative path. A value like cert.pem is resolved from the CWD, so launching from ~/.claude silently reads nothing and falls back to the default store. Fix: absolute path, and export/set it in the shell before launch (settings.json env can apply too late for the first handshake). (#71663)
4. Parent-process env injects a proxy, changing only the transport. Symptom: fails from one terminal (e.g. VS Code's integrated terminal) with a 503/hang, but the same binary works from the native terminal. The integrated terminal injects env (proxy vars, NODE_OPTIONS) that reroutes the CLI's HTTP path even though curl in the same terminal is fine. Confirm: diff env | sort between the two terminals; the delta is the suspect. Fix: unset HTTP(S)_PROXY/ALL_PROXY/NODE_OPTIONS and retry. (#71683)
5. Post-update EPERM (won't even start). Symptom: An internal error occurred (EPERM) on every run after an update, even though the directories are writable. Usually a root-owned file inside ~/.claude left by a past sudo (a writable dir can still hold root-owned files), or a dual npm-global + native install with a PATH conflict. Confirm: claude --debug "test" 2>&1 | tail -60 — Node/Bun EPERM carries the offending path:. (#71655)
The five share one trait: the CLI uses a different stack than curl/the OS, and the one-line error doesn't say which. So don't act on the message's "it's your proxy" guess — run claude --debug first and read which host and which of name-resolution / cert-chain / transport / permissions actually failed. EREFUSED → DNS; CERT_HAS_EXPIRED → chain/bundled store; 503/terminal-specific → injected env; EPERM → permissions.
If you run agents in a locked-down environment, the same operator discipline that untangles "won't connect" also prevents the running failures — data loss, runaway cost, settings lockout. Free MIT pre-execution guards are in cc-safe-setup; the monthly Agent Safety Brief (free by email; $5/mo done-for-you) covers each month's new failure modes for people running this for real.
Sources (reported incidents I helped triage; mechanisms relayed, not reproduced by me in every environment): DNS #71699, bundled CA / expired chain #71708, relative CA path #71663, injected-env transport #71683, post-update EPERM #71655. That the native CLI runs on Bun with its own bundled TLS stack is standard.