Follow-up to support incident d2142d0e-0119-4c35-a18d-761171637be4 (filed earlier,
without this evidence). A condensed version of this has also been submitted through the
in-app reporter — see the note in §7 about why that channel is unreliable for exactly this
class of bug.
Environment: Claude Code bundle 2.1.219 running inside Claude Desktop (not the terminal
CLI). Versions cited are bundle versions from [CCD] Initialized with version /
Using Claude Code binary at …; the Desktop app's own version does not appear in these
logs. macOS, Darwin 25.3.0, Apple Silicon.
Evidence: entirely local — ~/Library/Logs/Claude/main*.log (five rotations,
2026-04-18 → 2026-07-29), ~/Library/Application Support/Claude/Network Persistent State,
and ~/.claude/projects/*/*.jsonl. No server-side visibility. All figures are reproducible
with the commands in the appendix. Log lines are de-duplicated throughout — the app writes
every line twice.
TL;DR. On an identical, repeated commute workload the rate of turns that never receive
any response has gone from 0% (May) to 33% (now). It is not signal quality: turns fail
less in a known reception dead zone than in good coverage. The trigger is the source IP
changing — a mobile handover, or walking to an adjacent meeting room on corporate wi-fi.
The app reports net::ERR_INTERNET_DISCONNECTED and blames the user's connection while the
host is demonstrably online. The cheapest fix already exists in the codebase, applied to
exactly one call site (§4.2).
1. Executive summary
Since mid-May the reporter has run a naturally controlled experiment: commuting most
Tuesdays and Thursdays, working in Claude Code on the same two projects, in two fixed
windows — 06:45–07:25 and 16:25–17:10. Same route, same laptop, same hotspot, same kind of
work. Measured against the app's own hadFirstResponse telemetry, the rate of turns that
never received any response at all has risen steadily:
| Period |
Days |
Cycles |
No response |
Rate |
| May (05-05, 05-07) |
2 |
19 |
0 |
0% |
| June |
5 |
70 |
6 |
9% |
| July, pre-vacation (07-07, 07-09) |
2 |
18 |
5 |
28% |
| July, post-vacation (07-28) |
1 |
12 |
4 |
33% |
Identical workload, identical route, one third of turns now fail. Across all sessions
(not just commutes) the same metric moves 2% → 5% → 6% → 9% from April to July over ~3,600
cycles.
The user's link is not the problem, and this is demonstrable twice over. First, during
the 2026-07-28 incident the reporter was concurrently reading mail, browsing the web and
streaming YouTube on the same laptop and the same interface, uninterrupted — only Claude
Desktop failed. Second, the route crosses a known reception dead zone, and turns fail
less often there (7%) than in the rest of the commute (10%); in May, 13 turns crossed it
with zero failures (§2.1). Signal quality does not predict failure. Yet the app reports
net::ERR_INTERNET_DISCONNECTED and tells the user to "Check your internet connection and
try again."
The variable that does predict failure is the source IP changing — which is why weak-
but-stable coverage is survivable and well-covered territory, where the network constantly
re-steers the device between cells and bands, is not.
There are two independent failure modes (§3):
- Mode A — the request never leaves.
hadFirstResponse=false; attempts wait 9–32
seconds, receive nothing, and are abandoned; retried every one to two minutes. This is
what the table above measures and what has degraded.
- Mode B — a live stream is killed mid-flight.
hadFirstResponse=true then
ECONNRESET. Measured instance: 6m39s of generated output destroyed. Occurs even when
the app's network state is entirely correct.
Mode A wastes the session; Mode B destroys work already produced. They need different
fixes, and no client-side workaround exists for either (§6).
2. The controlled series in detail
Tue/Thu commute windows, 06:45–07:25 (AM) and 16:25–17:10 (PM). De-duplicated — the app
writes every log line twice. — means the app was running but no Claude Code cycles
occurred in that window. No log coverage exists for 2026-05-11 → 2026-05-28 (rotation gap),
and the reporter was on leave 2026-07-13 → 2026-07-26.
| Date |
Day |
W |
Cycles |
No-resp |
Unhealthy |
Max |
Mean |
Disc.min |
| 05-05 |
Tue |
AM |
3 |
0 |
0 |
539s |
267s |
0 |
| 05-05 |
Tue |
PM |
5 |
0 |
1 |
464s |
242s |
1 |
| 05-07 |
Thu |
AM |
5 |
0 |
0 |
247s |
112s |
0 |
| 05-07 |
Thu |
PM |
6 |
0 |
0 |
575s |
216s |
0 |
| 06-02 |
Tue |
AM |
4 |
2 |
0 |
732s |
375s |
0 |
| 06-02 |
Tue |
PM |
12 |
1 |
0 |
10495s* |
960s |
7 |
| 06-09 |
Tue |
AM |
3 |
0 |
0 |
100s |
60s |
0 |
| 06-09 |
Tue |
PM |
4 |
0 |
0 |
462s |
191s |
2 |
| 06-11 |
Thu |
PM |
12 |
0 |
0 |
334s |
97s |
0 |
| 06-23 |
Tue |
AM |
11 |
0 |
0 |
258s |
121s |
1 |
| 06-23 |
Tue |
PM |
19 |
0 |
3 |
161s |
34s |
4 |
| 06-30 |
Tue |
PM |
5 |
3 |
0 |
472s |
126s |
2 |
| 07-07 |
Tue |
AM |
7 |
0 |
0 |
419s |
212s |
0 |
| 07-07 |
Tue |
PM |
6 |
3 |
0 |
302s |
121s |
2 |
| 07-09 |
Thu |
AM |
5 |
2 |
0 |
228s |
92s |
0 |
| 07-28 |
Tue |
AM |
12 |
4 |
1 |
490s |
116s |
0 |
* a 10,495s (2.9h) cycle on 06-02 PM is almost certainly an idle session left open, not a
real turn; it inflates that row's mean and is excluded from no consideration elsewhere.
Two observations that matter for interpreting this:
The degradation predates the vacation. July pre-vacation (28%) and post-vacation (33%)
are effectively the same. Nothing changed while the reporter was away; the decline built
through June and had reached roughly its current level by 07-07/07-09. The subjective
sense that this is "new" is explained by two weeks away removing gradual habituation — not
by a recent deploy.
Single windows are unreliable; only the series is evidence. 07-07 AM was 0 of 7 while
07-07 PM was 3 of 6. An earlier draft of this report used 07-07 AM as a "clean baseline"
against 07-28 AM and was wrong to. Window-level variance is large, which is exactly why the
repeated fixed-scenario design above is the right instrument.
2.1 The failures do not happen where the signal is worst
The route contains a known dead zone — open country with exceptionally poor reception,
crossed at roughly 07:15 (±5 min) outbound and 16:40 (±5 min) inbound. Classifying each
turn by whether its interval (log timestamp minus reported duration) overlapped that
stretch:
| Period |
Dead zone |
Rest of window |
| May |
0 / 13 (0%) |
0 / 41 (0%) |
| June |
1 / 22 (5%) |
5 / 48 (10%) |
| July pre-vacation |
1 / 5 (20%) |
4 / 12 (33%) |
| July post-vacation |
1 / 3 (33%) |
2 / 8 (25%) |
| All periods |
3 / 43 = 7% |
11 / 109 = 10% |
Turns do not fail more where reception is worst — marginally less, though that
difference is within noise at these sample sizes. In May, 13 turns crossed the worst
reception on the entire route with zero failures.
The app is nonetheless fully aware of the bad reception. Normalised per minute of window,
inside the dead zone versus outside:
| Event |
Dead zone |
Rest |
Per-minute density |
ERR_INTERNET_DISCONNECTED |
37 |
73 |
1.6× |
fetch failed |
17 |
34 |
1.6× |
ECONNRESET |
2 |
0 |
— |
ERR_NETWORK_CHANGED |
0 |
0 |
— |
So background syncs fail 1.6× more densely where the signal is poor, exactly as expected —
while turn failures do not follow that gradient at all.
Breaking the commute into segments by where each turn was sent (log timestamp minus
duration) shows failures distributed across the entire route, including the opening minutes
in good reception. June onwards, i.e. the degraded era:
| Segment |
Failures |
Rate |
| 06:45–06:55 departure, good reception |
1/8 |
12% |
| 06:55–07:10 en route |
2/16 |
12% |
| 07:10–07:20 dead zone |
2/10 |
20% |
| 07:20–07:25 arrival |
3/9 |
33% |
| 16:25–16:35 departure, good reception |
3/14 |
21% |
| 16:35–16:45 dead zone |
1/14 |
7% |
| 16:45–17:00 en route |
3/17 |
18% |
| 17:00–17:10 arrival |
0/11 |
0% |
The evening commute fails three times more often in its first ten minutes, with good
reception, than in the dead zone. No segment is failure-free. Per-segment counts are small
(8–17 turns), so no individual pair is significant on its own — but the distribution is
flat with respect to reception quality, and the reporter independently confirms perceiving
errors from the very start of the commute in well-covered territory.
This dissociation is the core of the report. Weak-but-stable signal is survivable: TCP
retransmits absorb latency and packet loss, and a single weak cell with nothing to hand
over to keeps the connection's 4-tuple intact for minutes. What destroys a turn is the
source IP changing — and handovers are if anything more frequent where coverage is
good, because the network continuously re-steers the device between cells, bands and
carriers. The same reasoning explains why the reporter hits this while stationary,
simply walking from a desk to an adjacent meeting room: intra-ESSID AP roaming changes the
BSSID and usually the DHCP lease. Motion is not required, and neither is a bad signal —
only a re-homing network.
It follows that "poor connectivity" is the wrong frame for this bug, and that
"Check your internet connection and try again" (§4.1) is not merely unhelpful but points
at the one variable that demonstrably does not predict failure.
3. Two independent failure modes
3.1 Mode A — the request never leaves the machine
hadFirstResponse=false. From [CCD CycleHealth], 2026-07-28 morning, six consecutive
failures:
07:17:36 (25s, hadFirstResponse=false)
07:22:44 (29s, hadFirstResponse=false)
07:23:19 (30s, hadFirstResponse=false)
07:25:25 (32s, hadFirstResponse=false)
07:26:50 (20s, hadFirstResponse=false)
07:27:35 (20s, hadFirstResponse=false)
Each attempt waits 20–32s, receives nothing, is abandoned; attempts are spaced one to two
minutes apart. These timestamps match the duplicated prompt entries in the session
transcript exactly, confirming each retry is a full re-send of the turn. In the same hour
the transcript holds 30 user entries and 0 assistant entries. On 07-28 evening one
attempt gave up after just 9 seconds.
A 9–32 second wait that returns nothing is the signature of writing into a socket that is
already dead — no RST comes back when the old path simply vanished.
3.2 Mode B — a live stream destroyed mid-flight
2026-07-27 at 16:45:43:
16:45:43 [warn] [CCD CycleHealth] local_… api_error (success):
API Error: Unable to connect to API (ECONNRESET)
16:45:43 [info] [CCD CycleHealth] unhealthy cycle for local_…
(399s, hadFirstResponse=true, reason=api_error)
hadFirstResponse=true — the response was streaming normally and was destroyed 6 minutes
39 seconds in. There were zero ERR_INTERNET_DISCONNECTED events in that window, and
other turns in the same window completed at 820s and 679s. The app's network state was
correct throughout. This is raw architectural exposure: one TCP connection held for the
whole of a long generation, cut by a path change.
Genuine disruption did occur that evening (ERR_NAME_NOT_RESOLVED at 16:51:41, a real DNS
failure). The app absorbed most of it and lost exactly one long turn — which is the correct
behaviour for an unresumable stream, and why Mode B is a design limit rather than a bug.
Prioritisation consequence. The client fixes in §5.0–§5.1 resolve Mode A and would have
saved the 07-28 morning entirely. They would not have saved the 07-27 evening turn. Only
a resumable stream (§5.3) fixes Mode B — and Mode B is the mode that discards work that
already exists.
4. Mechanism
4.1 The app reports no internet while the host is online
main1.log, 2026-07-28 06:37:07–06:37:14 — one event, seven seconds, every subsystem:
06:37:07 [error] [EventLogging] POST threw: net::ERR_INTERNET_DISCONNECTED
06:37:07 [warn] [plan-usage] fetch failed
06:37:07 [error] [growthbook] network error: net::ERR_INTERNET_DISCONNECTED
06:37:07 [warn] [my-access] fetch failed
06:37:07 [error] [RemotePluginManager] Sync failed: net::ERR_INTERNET_DISCONNECTED
06:37:13 [warn] [oauth] network error on fetch, flushing connection pool and retrying
06:37:13 [error] OAuth token refresh network error: net::ERR_INTERNET_DISCONNECTED
06:37:13 [error] Cannot get base query config: oauth failed (network_error)
06:37:13 [error] [CCD] Failed to warm session local_8bed95f4…:
Unable to start session. Check your internet connection and try again.
06:37:14 [error] [SkillsPlugin] Sync failed: net::ERR_INTERNET_DISCONNECTED
Host connectivity was verifiably intact (mail, web, YouTube). ERR_INTERNET_DISCONNECTED
is what Chromium returns before attempting a socket, when NetworkChangeNotifier
reports connection type NONE. Nothing re-validates that belief against reality.
Note also that auth sits in the turn critical path: path change → OAuth refresh fails →
Cannot get base query config → Failed to warm session. A momentary gap does not merely
interrupt a response, it blocks the session from starting. Reproduced 2026-07-29 08:40:01.
4.2 The stale-connection fix exists — for OAuth only
All 18 flushing connection pool and retrying lines in four months of logs are [oauth].
Not one belongs to the API or streaming path:
2026-04-29 17:05:15 [oauth] … net::ERR_NETWORK_CHANGED
2026-06-02 06:37:26 [oauth] … net::ERR_INTERNET_DISCONNECTED (×3 that morning)
2026-07-06 06:50:17 [oauth] … net::ERR_INTERNET_DISCONNECTED (×2)
2026-07-10 08:24:03 [oauth] … net::ERR_INTERNET_DISCONNECTED (×3)
2026-07-28 06:37:13 [oauth] … net::ERR_INTERNET_DISCONNECTED
2026-07-29 08:40:01 [oauth] … net::ERR_INTERNET_DISCONNECTED
Flush the socket pool, then retry, is the correct remedy for reusing a dead pooled
connection after a path change. Somebody already diagnosed this and implemented it for
token refresh and nowhere else. This is the cheapest actionable item in the report.
4.3 Structural exposure
Each turn re-uploads the full conversation context — prompt caching saves server compute,
not bytes on the wire — then holds a single HTTP/2 connection open for the entire
streamed response, seconds to minutes. Failure probability scales with time-on-wire, so
long sessions are structurally more fragile, and every retry re-uploads the whole payload.
From Network Persistent State: 84 of 88 known servers negotiate HTTP/2; 22 advertise h3
via alt-svc, none of them Anthropic API hosts. So the API path is HTTP/2 over TCP — no
connection migration, and pooled sockets that outlive the path beneath them.
Weak supporting signal, possibly a coincidence: within a single day the PM window is worse
than the AM window on 07-07 (0% → 50%), consistent with accumulated context enlarging the
upload. 07-28 does not follow the pattern.
4.4 A secondary observation that does not explain the above
ERR_NETWORK_CHANGED — the error that should invalidate pooled sockets, and what the
[oauth] flush fires on — has declined as a share of all disconnect events:
| Month |
ERR_NETWORK_CHANGED |
ERR_INTERNET_DISCONNECTED |
Ratio |
| 2026-04 |
52 |
1194 |
4.4% |
| 2026-05 |
33 |
1248 |
2.6% |
| 2026-06 |
21 |
2085 |
1.0% |
| 2026-07 |
5 |
625 |
0.8% |
Last occurrence anywhere: 2026-07-08, coinciding with bundle 2.1.197 → 2.1.202.
However, this cannot be the cause of the commute failures. Across the entire Tue/Thu
series in §2, ERR_NETWORK_CHANGED appears essentially never — including in May, when the
failure rate was 0%. Its absence therefore does not distinguish working periods from
failing ones. Recorded here as an anomaly worth checking, not as an explanation. Bundle
timeline for reference: 2.1.187 (06-27→07-01), 2.1.197 (07-01→07-08), 2.1.202 (07-08→07-10),
2.1.205 (07-10→07-18), 2.1.209 (07-18→07-20), 2.1.215 (07-20→07-27), 2.1.219 (07-27→now).
5. Requested changes, in priority order
- Never fail a user-visible request on cached connectivity state. Treat
CONNECTION_NONE as a hint, not a verdict: attempt the request anyway, or probe
reachability first. ERR_INTERNET_DISCONNECTED outnumbers ERR_NETWORK_CHANGED 45:1
(5157 vs 114) across four months, and §4.1 shows the belief can be flatly wrong.
- Flush the connection pool and retry on the API and streaming paths, exactly as
[oauth] already does (§4.2). Small, well-precedented, and targets Mode A directly.
- Bisect the client between 2026-05-07 and 2026-07-09. §2 brackets the regression on a
controlled workload: 0% in early May, 9% through June, 28% by 07-07. Bundle versions in
that range are 2.1.121 → 2.1.202. This is a gradual erosion, so look for accumulated
changes to timeouts, connection reuse, or retry policy rather than one bad commit.
- Resumable response streams. An event cursor so a reconnect resumes mid-generation
instead of regenerating; HTTP standardises this as SSE Last-Event-ID. The only fix
for Mode B, and the mode that destroys already-generated work.
- Delta upload against server-side conversation state. Let a turn reference prior
context by ID and ship only new content, collapsing the upload window to near-constant
regardless of session age (§4.3).
- Retry policy. Jittered backoff across the timescale handovers actually take
(~5–60s), re-validating connectivity between attempts instead of repeating a doomed
request. Note each retry currently re-uploads the entire context.
- Take auth out of the turn critical path (§4.1) — refresh ahead of expiry with
generous skew so a momentary gap cannot produce Failed to warm session.
- Consider HTTP/3 for the API path (§4.3). QUIC connection IDs survive an IP change by
design. Lower priority than 0–2, which are bugs rather than architecture.
- Fix the user-facing message. "Check your internet connection and try again" is false
here and cost the reporter a morning debugging the wrong layer.
6. No client-side workaround exists — tested
On 2026-07-28 between 17:25 and 18:15 the reporter switched between several VPNs
specifically to test mitigation. Result: 2 of 6 cycles still received no response
(17:41:53 at 90s, 17:44:14 after 9s).
More telling is what is absent: bringing a VPN up or down necessarily rebuilds the default
route and adds or removes a utun interface, yet the window contains zero
ERR_NETWORK_CHANGED events. The app registered none of those deliberate, repeated path
changes.
A stable tunnel (WireGuard/Tailscale) can only help Mode B, by holding the inner IP
constant beneath a long stream. It cannot help Mode A, which is the app failing on its
own bookkeeping however stable the path is. Switching between tunnels is strictly worse,
since each switch is another path change. And no setting reaches this: ~/.claude/settings.json
has no network keys, and in Desktop the app owns the transport.
7. Secondary defects
- The in-app bug reporter fails during the outage it exists to report. Twelve
submissions across two incidents, all feedback_id=null, failure=network_error
(2026-07-28 07:12:29–07:17:35, seven attempts; 17:45:02–17:52:24, three attempts — the
first with descLen=1315, i.e. a detailed report, then two shorter retries). The
reporter was trying to file this very incident. Consequence: this bug class is
structurally under-reported to Anthropic, because affected users are blocked precisely
when they try to report. Feedback must be persisted to disk and retried later.
api_error (success) — a transport failure logged with a success qualifier.
healthy cycle … hadFirstResponse=false — cycles that produced no response at all
are recorded as healthy (all six in §3.1). If aggregate telemetry counts these as
healthy, the entire degradation in §1 is invisible on Anthropic's own dashboards.
- 34-minute diagnostic blackout. Between 06:37:15 and 07:11:16 on 2026-07-28 the log
contains nothing but periodic memory lines, while the user retried repeatedly.
- Every log line is written twice.
8. Reproduction
- macOS, Claude Code bundle 2.1.219 in Claude Desktop, Wi-Fi plus an iPhone hotspot.
- Open a session and accumulate real context (an hour of work).
- Issue a turn whose response streams for more than ~30 seconds.
- Mid-stream, change the path: toggle Wi-Fi off/on, or carry the laptop between two APs on
one corporate ESSID. Walking to an adjacent meeting room is sufficient.
- In parallel, keep a YouTube stream or a
ping running to prove the host link is
healthy.
- Observed: Mode A — the turn dies, retries wait 9–32s and receive nothing, log shows
ERR_INTERNET_DISCONNECTED fan-out across all subsystems and possibly
Failed to warm session; or Mode B — the stream dies mid-generation with ECONNRESET.
Host connectivity is fine throughout.
- Expected: the request is attempted regardless of cached network state, the pool is
flushed on failure, and the stream resumes from its last delivered event.
9. Limitations
hadFirstResponse=false may conflate transport failures with user-initiated
interrupts. The app does not appear to distinguish them, so absolute rates are an upper
bound. Since the metric definition is constant across the whole series, the trend is
unaffected. Separating the two causes is itself request §7.
- Sample sizes per period in §1 are modest (19 / 70 / 18 / 12 cycles). The trend is
monotonic and the workload is genuinely repeated, but no single period is decisive.
July post-vacation is one window.
- No log coverage 2026-05-11 → 2026-05-28 (rotation gap), so the May baseline rests on two
days that slightly predate the stated start of the commute pattern.
- Individual windows vary widely (07-07 AM 0/7 vs 07-07 PM 3/6); earlier drafts of this
report over-read single windows and were corrected.
- In §2.1, a turn is assigned to the dead zone if its interval touches it, so long turns
that mostly ran elsewhere are counted as dead-zone. This biases the dead-zone group
toward long turns — i.e. toward higher expected failure — which strengthens rather than
weakens the finding that it is not worse. The 7% vs 10% gap is not itself significant
(3/43 vs 11/109); the claim being made is only that failures show no dead-zone
concentration, plus the May 0/13 result.
- Dead-zone boundaries are the reporter's recollection (±5 min around 07:15 and 16:40), not
GPS-derived.
- The 2026-07-08 / bundle-2.1.202 correlation in §4.4 rests on 5 July occurrences and is
explicitly contradicted as a cause by the May data. Starting point only.
ERR_* counts are unique log lines, not distinct network events — one event fans out
across subsystems (§4.1). Ratios compare like with like.
- The
NetworkChangeNotifier attribution is inference from Chromium's documented semantics
for the error code plus observed host connectivity. Confirming it needs a
--log-net-log capture or Anthropic's own instrumentation.
- Purely local evidence. No server-side view; no claim about intent or any specific deploy.
10. Appendix — regenerate
# Controlled Tue/Thu commute series (§1, §2)
cd ~/Library/Logs/Claude && python3 - <<'PY'
import re,datetime,collections
WINS={'AM':('06:45','07:25'),'PM':('16:25','17:10')}
seen=set(); rec=collections.defaultdict(lambda: collections.defaultdict(list))
for f in ('main4.log','main3.log','main2.log','main1.log','main.log'):
for line in open(f,errors='ignore'):
if not line.startswith('2026-'): continue
d,hm=line[:10],line[11:16]
w=next((n for n,(a,b) in WINS.items() if a<=hm<=b),None)
if not w or line in seen: continue
seen.add(line)
m=re.search(r'CycleHealth\].*?\((\d+)s, hadFirstResponse=(true|false)',line)
if m: rec[d][w].append(m.group(2)=='true')
for ds in sorted(rec):
if datetime.date.fromisoformat(ds).weekday() not in (1,3): continue
for w,c in sorted(rec[ds].items()):
print(ds,w,f"{sum(1 for x in c if not x)}/{len(c)}")
PY
# Error-type distribution (§5.0)
cd ~/Library/Logs/Claude && for p in ERR_INTERNET_DISCONNECTED ERR_NETWORK_CHANGED \
ERR_CONNECTION_CLOSED ERR_CONNECTION_RESET ERR_TIMED_OUT; do \
printf '%-30s %s\n' "$p" "$(grep -h $p main*.log | sort -u | wc -l)"; done
# Pool-flush call sites (§4.2) — all 18 are [oauth]
cd ~/Library/Logs/Claude && grep -h 'connection pool' main*.log | sort -u
# Negotiated protocols (§4.3)
python3 -c "import json,os;d=json.load(open(os.path.expanduser( \
'~/Library/Application Support/Claude/Network Persistent State'))); \
s=d['net']['http_server_properties']['servers']; \
print(len(s),'servers',sum('supports_spdy' in x for x in s),'h2', \
sum('alternative_service' in x for x in s),'h3')"
Follow-up to support incident
d2142d0e-0119-4c35-a18d-761171637be4(filed earlier,without this evidence). A condensed version of this has also been submitted through the
in-app reporter — see the note in §7 about why that channel is unreliable for exactly this
class of bug.
Environment: Claude Code bundle 2.1.219 running inside Claude Desktop (not the terminal
CLI). Versions cited are bundle versions from
[CCD] Initialized with version/Using Claude Code binary at …; the Desktop app's own version does not appear in theselogs. macOS, Darwin 25.3.0, Apple Silicon.
Evidence: entirely local —
~/Library/Logs/Claude/main*.log(five rotations,2026-04-18 → 2026-07-29),
~/Library/Application Support/Claude/Network Persistent State,and
~/.claude/projects/*/*.jsonl. No server-side visibility. All figures are reproduciblewith the commands in the appendix. Log lines are de-duplicated throughout — the app writes
every line twice.
TL;DR. On an identical, repeated commute workload the rate of turns that never receive
any response has gone from 0% (May) to 33% (now). It is not signal quality: turns fail
less in a known reception dead zone than in good coverage. The trigger is the source IP
changing — a mobile handover, or walking to an adjacent meeting room on corporate wi-fi.
The app reports
net::ERR_INTERNET_DISCONNECTEDand blames the user's connection while thehost is demonstrably online. The cheapest fix already exists in the codebase, applied to
exactly one call site (§4.2).
1. Executive summary
Since mid-May the reporter has run a naturally controlled experiment: commuting most
Tuesdays and Thursdays, working in Claude Code on the same two projects, in two fixed
windows — 06:45–07:25 and 16:25–17:10. Same route, same laptop, same hotspot, same kind of
work. Measured against the app's own
hadFirstResponsetelemetry, the rate of turns thatnever received any response at all has risen steadily:
Identical workload, identical route, one third of turns now fail. Across all sessions
(not just commutes) the same metric moves 2% → 5% → 6% → 9% from April to July over ~3,600
cycles.
The user's link is not the problem, and this is demonstrable twice over. First, during
the 2026-07-28 incident the reporter was concurrently reading mail, browsing the web and
streaming YouTube on the same laptop and the same interface, uninterrupted — only Claude
Desktop failed. Second, the route crosses a known reception dead zone, and turns fail
less often there (7%) than in the rest of the commute (10%); in May, 13 turns crossed it
with zero failures (§2.1). Signal quality does not predict failure. Yet the app reports
net::ERR_INTERNET_DISCONNECTEDand tells the user to "Check your internet connection andtry again."
The variable that does predict failure is the source IP changing — which is why weak-
but-stable coverage is survivable and well-covered territory, where the network constantly
re-steers the device between cells and bands, is not.
There are two independent failure modes (§3):
hadFirstResponse=false; attempts wait 9–32seconds, receive nothing, and are abandoned; retried every one to two minutes. This is
what the table above measures and what has degraded.
hadFirstResponse=truethenECONNRESET. Measured instance: 6m39s of generated output destroyed. Occurs even whenthe app's network state is entirely correct.
Mode A wastes the session; Mode B destroys work already produced. They need different
fixes, and no client-side workaround exists for either (§6).
2. The controlled series in detail
Tue/Thu commute windows, 06:45–07:25 (AM) and 16:25–17:10 (PM). De-duplicated — the app
writes every log line twice.
—means the app was running but no Claude Code cyclesoccurred in that window. No log coverage exists for 2026-05-11 → 2026-05-28 (rotation gap),
and the reporter was on leave 2026-07-13 → 2026-07-26.
* a 10,495s (2.9h) cycle on 06-02 PM is almost certainly an idle session left open, not a
real turn; it inflates that row's mean and is excluded from no consideration elsewhere.
Two observations that matter for interpreting this:
The degradation predates the vacation. July pre-vacation (28%) and post-vacation (33%)
are effectively the same. Nothing changed while the reporter was away; the decline built
through June and had reached roughly its current level by 07-07/07-09. The subjective
sense that this is "new" is explained by two weeks away removing gradual habituation — not
by a recent deploy.
Single windows are unreliable; only the series is evidence. 07-07 AM was 0 of 7 while
07-07 PM was 3 of 6. An earlier draft of this report used 07-07 AM as a "clean baseline"
against 07-28 AM and was wrong to. Window-level variance is large, which is exactly why the
repeated fixed-scenario design above is the right instrument.
2.1 The failures do not happen where the signal is worst
The route contains a known dead zone — open country with exceptionally poor reception,
crossed at roughly 07:15 (±5 min) outbound and 16:40 (±5 min) inbound. Classifying each
turn by whether its interval (log timestamp minus reported duration) overlapped that
stretch:
Turns do not fail more where reception is worst — marginally less, though that
difference is within noise at these sample sizes. In May, 13 turns crossed the worst
reception on the entire route with zero failures.
The app is nonetheless fully aware of the bad reception. Normalised per minute of window,
inside the dead zone versus outside:
ERR_INTERNET_DISCONNECTEDfetch failedECONNRESETERR_NETWORK_CHANGEDSo background syncs fail 1.6× more densely where the signal is poor, exactly as expected —
while turn failures do not follow that gradient at all.
Breaking the commute into segments by where each turn was sent (log timestamp minus
duration) shows failures distributed across the entire route, including the opening minutes
in good reception. June onwards, i.e. the degraded era:
The evening commute fails three times more often in its first ten minutes, with good
reception, than in the dead zone. No segment is failure-free. Per-segment counts are small
(8–17 turns), so no individual pair is significant on its own — but the distribution is
flat with respect to reception quality, and the reporter independently confirms perceiving
errors from the very start of the commute in well-covered territory.
This dissociation is the core of the report. Weak-but-stable signal is survivable: TCP
retransmits absorb latency and packet loss, and a single weak cell with nothing to hand
over to keeps the connection's 4-tuple intact for minutes. What destroys a turn is the
source IP changing — and handovers are if anything more frequent where coverage is
good, because the network continuously re-steers the device between cells, bands and
carriers. The same reasoning explains why the reporter hits this while stationary,
simply walking from a desk to an adjacent meeting room: intra-ESSID AP roaming changes the
BSSID and usually the DHCP lease. Motion is not required, and neither is a bad signal —
only a re-homing network.
It follows that "poor connectivity" is the wrong frame for this bug, and that
"Check your internet connection and try again" (§4.1) is not merely unhelpful but points
at the one variable that demonstrably does not predict failure.
3. Two independent failure modes
3.1 Mode A — the request never leaves the machine
hadFirstResponse=false. From[CCD CycleHealth], 2026-07-28 morning, six consecutivefailures:
Each attempt waits 20–32s, receives nothing, is abandoned; attempts are spaced one to two
minutes apart. These timestamps match the duplicated prompt entries in the session
transcript exactly, confirming each retry is a full re-send of the turn. In the same hour
the transcript holds 30
userentries and 0assistantentries. On 07-28 evening oneattempt gave up after just 9 seconds.
A 9–32 second wait that returns nothing is the signature of writing into a socket that is
already dead — no
RSTcomes back when the old path simply vanished.3.2 Mode B — a live stream destroyed mid-flight
2026-07-27 at 16:45:43:
hadFirstResponse=true— the response was streaming normally and was destroyed 6 minutes39 seconds in. There were zero
ERR_INTERNET_DISCONNECTEDevents in that window, andother turns in the same window completed at 820s and 679s. The app's network state was
correct throughout. This is raw architectural exposure: one TCP connection held for the
whole of a long generation, cut by a path change.
Genuine disruption did occur that evening (
ERR_NAME_NOT_RESOLVEDat 16:51:41, a real DNSfailure). The app absorbed most of it and lost exactly one long turn — which is the correct
behaviour for an unresumable stream, and why Mode B is a design limit rather than a bug.
Prioritisation consequence. The client fixes in §5.0–§5.1 resolve Mode A and would have
saved the 07-28 morning entirely. They would not have saved the 07-27 evening turn. Only
a resumable stream (§5.3) fixes Mode B — and Mode B is the mode that discards work that
already exists.
4. Mechanism
4.1 The app reports no internet while the host is online
main1.log, 2026-07-28 06:37:07–06:37:14 — one event, seven seconds, every subsystem:Host connectivity was verifiably intact (mail, web, YouTube).
ERR_INTERNET_DISCONNECTEDis what Chromium returns before attempting a socket, when
NetworkChangeNotifierreports connection type
NONE. Nothing re-validates that belief against reality.Note also that auth sits in the turn critical path: path change → OAuth refresh fails →
Cannot get base query config→Failed to warm session. A momentary gap does not merelyinterrupt a response, it blocks the session from starting. Reproduced 2026-07-29 08:40:01.
4.2 The stale-connection fix exists — for OAuth only
All 18
flushing connection pool and retryinglines in four months of logs are[oauth].Not one belongs to the API or streaming path:
Flush the socket pool, then retry, is the correct remedy for reusing a dead pooled
connection after a path change. Somebody already diagnosed this and implemented it for
token refresh and nowhere else. This is the cheapest actionable item in the report.
4.3 Structural exposure
Each turn re-uploads the full conversation context — prompt caching saves server compute,
not bytes on the wire — then holds a single HTTP/2 connection open for the entire
streamed response, seconds to minutes. Failure probability scales with time-on-wire, so
long sessions are structurally more fragile, and every retry re-uploads the whole payload.
From
Network Persistent State: 84 of 88 known servers negotiate HTTP/2; 22 advertiseh3via alt-svc, none of them Anthropic API hosts. So the API path is HTTP/2 over TCP — no
connection migration, and pooled sockets that outlive the path beneath them.
Weak supporting signal, possibly a coincidence: within a single day the PM window is worse
than the AM window on 07-07 (0% → 50%), consistent with accumulated context enlarging the
upload. 07-28 does not follow the pattern.
4.4 A secondary observation that does not explain the above
ERR_NETWORK_CHANGED— the error that should invalidate pooled sockets, and what the[oauth]flush fires on — has declined as a share of all disconnect events:ERR_NETWORK_CHANGEDERR_INTERNET_DISCONNECTEDLast occurrence anywhere: 2026-07-08, coinciding with bundle 2.1.197 → 2.1.202.
However, this cannot be the cause of the commute failures. Across the entire Tue/Thu
series in §2,
ERR_NETWORK_CHANGEDappears essentially never — including in May, when thefailure rate was 0%. Its absence therefore does not distinguish working periods from
failing ones. Recorded here as an anomaly worth checking, not as an explanation. Bundle
timeline for reference: 2.1.187 (06-27→07-01), 2.1.197 (07-01→07-08), 2.1.202 (07-08→07-10),
2.1.205 (07-10→07-18), 2.1.209 (07-18→07-20), 2.1.215 (07-20→07-27), 2.1.219 (07-27→now).
5. Requested changes, in priority order
CONNECTION_NONEas a hint, not a verdict: attempt the request anyway, or probereachability first.
ERR_INTERNET_DISCONNECTEDoutnumbersERR_NETWORK_CHANGED45:1(5157 vs 114) across four months, and §4.1 shows the belief can be flatly wrong.
[oauth]already does (§4.2). Small, well-precedented, and targets Mode A directly.controlled workload: 0% in early May, 9% through June, 28% by 07-07. Bundle versions in
that range are 2.1.121 → 2.1.202. This is a gradual erosion, so look for accumulated
changes to timeouts, connection reuse, or retry policy rather than one bad commit.
instead of regenerating; HTTP standardises this as SSE
Last-Event-ID. The only fixfor Mode B, and the mode that destroys already-generated work.
context by ID and ship only new content, collapsing the upload window to near-constant
regardless of session age (§4.3).
(~5–60s), re-validating connectivity between attempts instead of repeating a doomed
request. Note each retry currently re-uploads the entire context.
generous skew so a momentary gap cannot produce
Failed to warm session.design. Lower priority than 0–2, which are bugs rather than architecture.
here and cost the reporter a morning debugging the wrong layer.
6. No client-side workaround exists — tested
On 2026-07-28 between 17:25 and 18:15 the reporter switched between several VPNs
specifically to test mitigation. Result: 2 of 6 cycles still received no response
(17:41:53 at 90s, 17:44:14 after 9s).
More telling is what is absent: bringing a VPN up or down necessarily rebuilds the default
route and adds or removes a
utuninterface, yet the window contains zeroERR_NETWORK_CHANGEDevents. The app registered none of those deliberate, repeated pathchanges.
A stable tunnel (WireGuard/Tailscale) can only help Mode B, by holding the inner IP
constant beneath a long stream. It cannot help Mode A, which is the app failing on its
own bookkeeping however stable the path is. Switching between tunnels is strictly worse,
since each switch is another path change. And no setting reaches this:
~/.claude/settings.jsonhas no network keys, and in Desktop the app owns the transport.
7. Secondary defects
submissions across two incidents, all
feedback_id=null, failure=network_error(2026-07-28 07:12:29–07:17:35, seven attempts; 17:45:02–17:52:24, three attempts — the
first with
descLen=1315, i.e. a detailed report, then two shorter retries). Thereporter was trying to file this very incident. Consequence: this bug class is
structurally under-reported to Anthropic, because affected users are blocked precisely
when they try to report. Feedback must be persisted to disk and retried later.
api_error (success)— a transport failure logged with a success qualifier.healthy cycle … hadFirstResponse=false— cycles that produced no response at allare recorded as healthy (all six in §3.1). If aggregate telemetry counts these as
healthy, the entire degradation in §1 is invisible on Anthropic's own dashboards.
contains nothing but periodic memory lines, while the user retried repeatedly.
8. Reproduction
one corporate ESSID. Walking to an adjacent meeting room is sufficient.
pingrunning to prove the host link ishealthy.
ERR_INTERNET_DISCONNECTEDfan-out across all subsystems and possiblyFailed to warm session; or Mode B — the stream dies mid-generation withECONNRESET.Host connectivity is fine throughout.
flushed on failure, and the stream resumes from its last delivered event.
9. Limitations
hadFirstResponse=falsemay conflate transport failures with user-initiatedinterrupts. The app does not appear to distinguish them, so absolute rates are an upper
bound. Since the metric definition is constant across the whole series, the trend is
unaffected. Separating the two causes is itself request §7.
monotonic and the workload is genuinely repeated, but no single period is decisive.
July post-vacation is one window.
days that slightly predate the stated start of the commute pattern.
report over-read single windows and were corrected.
that mostly ran elsewhere are counted as dead-zone. This biases the dead-zone group
toward long turns — i.e. toward higher expected failure — which strengthens rather than
weakens the finding that it is not worse. The 7% vs 10% gap is not itself significant
(3/43 vs 11/109); the claim being made is only that failures show no dead-zone
concentration, plus the May 0/13 result.
GPS-derived.
explicitly contradicted as a cause by the May data. Starting point only.
ERR_*counts are unique log lines, not distinct network events — one event fans outacross subsystems (§4.1). Ratios compare like with like.
NetworkChangeNotifierattribution is inference from Chromium's documented semanticsfor the error code plus observed host connectivity. Confirming it needs a
--log-net-logcapture or Anthropic's own instrumentation.10. Appendix — regenerate