claude update fails with "getaddrinfo EREFUSED" on split-DNS VPN — updater resolves via c-ares, not the system resolver
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?
On Linux with systemd-resolved and a split-tunnel VPN, claude update consistently fails to fetch the version manifest, even though downloads.claude.ai is fully reachable.
In the same Node runtime, fetch() and dns.lookup() resolve the host fine — only the updater fails. The reason: the updater resolves DNS via c-ares (dns.resolve*), which reads /etc/resolv.conf directly and bypasses NSS / /etc/hosts / systemd-resolved. EREFUSED is a c-ares-specific code (glibc getaddrinfo returns EAI_* codes like ENOTFOUND), which is the tell that this is the c-ares path, not the OS resolver.
My VPN pushes internal authoritative resolvers into /etc/resolv.conf that refuse recursion for public names, and lists them before the systemd-resolved stub (127.0.0.53). c-ares queries the first server, gets REFUSED, and stops — it never reaches the stub, which resolves correctly. So the updater resolves differently, and more fragilely, than the rest of the app.
What Should Happen?
The updater's manifest fetch should resolve downloads.claude.ai the same way the rest of Claude Code does — via the OS resolver (getaddrinfo / dns.lookup), honoring NSS, /etc/hosts, and systemd-resolved split-DNS. It already works for fetch() in the same process, so the updater should succeed in any environment where fetch/curl succeed.
If c-ares is intentional (e.g. to avoid the libuv threadpool), it should at least fall back to dns.lookup on EREFUSED/ESERVFAIL rather than treating the first server's REFUSED as terminal.
Error Messages/Logs
Error: Failed to install native update
Error: Failed to fetch version from https://downloads.claude.ai/claude-code-releases/latest after 3 attempt(s): getaddrinfo EREFUSED downloads.claude.ai
strace -f -e trace=connect on `claude update`, filtered to DNS endpoints:
11 connect(.., {AF_INET, sin_port=htons(53), sin_addr="10.x.x.53"}) = 0 <- internal resolver, refuses public names
1 connect(.., {AF_UNIX, sun_path="/run/systemd/resolve/io.systemd.Resolve"}) = 0
The 11 connects are c-ares retrying across the 3 update attempts. The internal server answers REFUSED; c-ares does not fall through to the 127.0.0.53 stub listed later in resolv.conf.
Steps to Reproduce
On a host where the first nameserver in /etc/resolv.conf returns REFUSED for public names (e.g. an internal authoritative DNS server pushed by a VPN), while systemd-resolved/the stub resolves correctly:
# c-ares path (what the updater uses) — FAILS
node -e "require('dns').resolve4('downloads.claude.ai',(e,r)=>console.log(e?e.code:r))" # -> EREFUSED
# getaddrinfo path (NSS -> systemd-resolved) — WORKS
node -e "require('dns').lookup('downloads.claude.ai',(e,r)=>console.log(e?e.code:r))" # -> 35.x.x.x
# global fetch to the exact updater URL — WORKS
node -e "fetch('https://downloads.claude.ai/claude-code-releases/latest').then(r=>console.log(r.status))" # -> 200
# c-ares forced at the systemd-resolved stub — WORKS (stub does split-DNS correctly)
node -e "const d=require('dns');d.setServers(['127.0.0.53']);d.resolve4('downloads.claude.ai',(e,r)=>console.log(e?e.code:r))" # -> 35.x.x.x
# the updater itself — FAILS
claude update # -> getaddrinfo EREFUSED downloads.claude.ai
### Claude Model
None
### Is this a regression?
I don't know
### Last Working Version
N/A
### Claude Code Version
2.1.193
### Platform
Anthropic API
### Operating System
Ubuntu/Debian Linux
### Terminal/Shell
Xterm
### Additional Information
[claude-update-cares-bugreport.md](https://github.com/user-attachments/files/29397285/claude-update-cares-bugreport.md)
**Root cause:** the native updater's version-manifest fetch uses c-ares (`dns.resolve*`, or an HTTP client wired with a c-ares `lookup`) instead of the system resolver. c-ares reads `/etc/resolv.conf` directly, ignores NSS/`systemd-resolved`, queries servers in file order, and treats the first `REFUSED` as terminal. Because the VPN writes a public-refusing authoritative server as the first nameserver, c-ares fails where `getaddrinfo` (and `fetch`) succeed.
**Environment:** Ubuntu-family Linux, `systemd-resolved` active (NSS `nss-resolve` enabled), split-tunnel VPN (Cisco Secure Client, `cscotun0`), Node v24, shell `bash`. `/etc/resolv.conf` lists internal resolvers before `127.0.0.53`.
**Workarounds that work for me:**
- Update via the signed Linux package repo (`apt`/`dnf`/`apk` — these resolve through `getaddrinfo`).
- Or the curl installer: `curl -fsSL https://claude.ai/install.sh | bash` (curl uses `getaddrinfo`).
- `"DISABLE_AUTOUPDATER": "1"` in `settings.json` silences the failing background checks (doesn't fix resolution).
_Note: the "suggested fix" is inferred from observed behavior, not from the source._
3 Comments
Your root-cause analysis is exactly right, and the
EREFUSED-is-a-c-ares-code tell is the cleanest way to prove it — c-ares (dns.resolve*) reads/etc/resolv.confdirectly, queries the first listed server, getsREFUSEDfrom your VPN's internal authoritative resolver, and stops without ever falling through to thesystemd-resolvedstub thatgetaddrinfo/dns.lookup(and thereforefetch) use. Nothing to add to the diagnosis. Two practical notes for anyone hitting this who needs to update now:Don't reach for
/etc/hosts— it won't help here. Because the failing path is c-ares, and c-ares bypasses NSS, pinningdownloads.claude.aiin/etc/hostsis silently ignored by the very code that's failing (it only works for thegetaddrinfopath, which already succeeds). Easy hour to lose.Workarounds that actually hit the working resolver:
curl(OS resolver, honorssystemd-resolved), not the in-app c-ares updater:``
sh
`curl -fsSL https://claude.ai/install.sh | bash
claude install(or
— it goes throughfetch`, the same path you confirmed already resolves the host correctly).``
sh
`# back up, then put the systemd-resolved stub first
sudo sed -i '1i nameserver 127.0.0.53' /etc/resolv.conf
claude update
systemdCaveat: the VPN/
will likely rewriteresolv.conf` on the next link change, so this is a one-shot, not a fix — but it lets the manifest fetch through immediately.---
For the maintainers: this is a clean, correct bug. The updater's manifest fetch should use
dns.lookup/getaddrinfo(the OS resolver) like the rest of the process, notdns.resolve*/c-ares — it already works viafetch()in the same runtime, so the invariant should simply be "iffetch/curlcan reachdownloads.claude.ai, the updater can too." If c-ares is deliberate (e.g. to dodge the libuv threadpool), it should at minimum (a) fall back todns.lookuponREFUSED/SERVFAILrather than stopping at the first server, and (b) surface which host failed to resolve in the error —getaddrinfo EREFUSEDwith no hostname is what makes this a multi-hour diagnosis instead of a one-liner. Split-DNS / split-tunnel VPN is common enough in corp environments that this will keep recurring.Is there any update on this bug report?
Update: this is not updater-specific, and the root cause is upstream in Bun
Follow-up with measured results. Two things changed since I filed this: the scope is wider than
claude update, and the fix I suggested in the original report was wrong — see below.WebFetchfails the same wayClaude Code 2.1.227, on the same split-DNS VPN host, no updater involved:
Same error signature, same host, while
WebSearch(server-side) works andcurlon the samemachine returns
HTTP 200.Root cause: Bun's Linux DNS default, not anything specific to the updater
Claude Code ships as a Bun binary, and on Linux Bun's default DNS backend is c-ares, which reads
/etc/resolv.confdirectly and has no NSS hook — sosystemd-resolvedandnsswitch.confarebypassed. That is why the error reads
getaddrinfo EREFUSED: thegetaddrinfolabel with a c-aresrcode. glibc
getaddrinfo()only ever returnsEAI_*codes.Filed upstream, with reproductions:
REFUSEDfrom the first nameserver instead of tryingthe remaining
resolv.confnameservers (glibc falls through)Verified against stock Bun, both the released
1.3.14+0d9b296afand1.4.0-canary.1+23d233b20(the line this build embeds) — identical results (internal resolver addresses masked):
Node.js v24.14.1 on the same host, same
/etc/resolv.conf, seconds apart, resolves it fine viadns.lookup— so this is a Bun-vs-Node divergence, not a broken host.Correction to my original "Suggested fix"
The original report said to have the updater "use the default
getaddrinfo/dns.lookuppath."That would not fix it. In Bun,
dns.lookupis the c-ares path — on Linux it is the defaultbackend. Please disregard that recommendation.
The framing was also slightly off: nothing had to opt into c-ares. Any
node:dns.lookupcall inBun on Linux gets it, which is why the updater and
WebFetchfail identically.You can fix this without waiting for Bun
Bun's own
fetch()is unaffected — fresh process, no DNS cache involved:So a Bun app only hits this if it resolves a hostname through
node:dnsbefore fetching. Fromstrings in the binary it looks like Claude Code does exactly that — resolving the host, screening the
addresses against link-local/metadata ranges, then fetching by IP. I can't see your source, so treat
that as an inference from behavior rather than a claim about the code; the behavioral evidence is
that bare
fetchsucceeds whereWebFetchfails on the same host in the same second.If that's right, the one-line fix is to keep the guard and change only the resolver:
Please don't fix it by dropping the pre-resolution and letting
fetchtake the hostname — thatwould make the symptom vanish while removing the DNS-rebinding protection the pre-resolve provides.
A fallback (retry on
EREFUSED/ESERVFAILvialibc) also works, but for a guard lookup there's nobenefit to c-ares' async behavior in the first place.
Why this is worth more than a failed lookup
getaddrinfo EREFUSED docs.aws.amazon.comreads as "that site is unreachable from this machine."In an agent session it did real damage: the model concluded the docs host was unresolvable and
recorded that as a durable fact about the environment, then worked around a problem that didn't
exist. The site was reachable the whole time by every tool on the host that uses the system resolver.
A resolver-level failure that presents as a network-level one is especially costly in an agentic
tool, so an error message distinguishing the two — or a
libcfallback — would help even before theupstream default changes.
Workarounds for anyone else landing here
WebFetchon a split-DNS VPN: retry off-VPN, or fetch throughBash+curl(glibcgetaddrinfo, works on VPN).WebSearchis unaffected — it runs server-side.claude update: use the signed apt/rpm/apk repo or thecurlinstaller; both resolve via glibc."DISABLE_AUTOUPDATER": "1"silences the failing background checks but does not fix resolution.