Fresh project sessions rapidly accumulate hidden tool-result context and then fail with ECONNRESET

Status Closed — not planned
Reported on v2.1.177
Maintainer reply None cached
Activity 7 comments · opened Jun 14, 2026 · closed Aug 30, 2026

Preflight Checklist

  • [x] I have searched existing issues for similar reports.
  • [x] This is a single bug report.
  • [x] I am using the latest version of Claude Code available to me.
  • [x] This report does not contain private source code or secrets.

What's Wrong?

Claude Code project sessions can become unstable even when started as a fresh session. In my case, a fresh project session quickly accumulated a large hidden context/tool-result footprint and then began failing with ECONNRESET, while a minimal non-persistent Claude Code request from the same terminal/account/network succeeded immediately.

This made the failure look like a VPN/proxy/network problem, but the evidence points to Claude Code session/context handling.

Observed environment:

  • Claude Code version: 2.1.177 (Claude Code)
  • Model: Sonnet 4.6
  • OS: macOS
  • Shell/terminal: zsh / Terminal.app
  • Project type: frontend project with large CSS/Astro files

Minimal request succeeds from the same machine/account/proxy:

claude -p 'Reply OK' --model sonnet --effort low --no-session-persistence --output-format json

Result:

success, result: OK
duration_api_ms: ~3008
ttft_ms: ~2677

But fresh interactive project sessions fail after normal coding/tool use.

Examples from recent fresh session JSONL files:

~/.claude/projects/.../a10d6770-*.jsonl
size: 1.9MB
entries: hundreds
largest persisted user/tool-result line: ~700KB
cache_read_input_tokens later in the session: ~48k-50k

~/.claude/projects/.../ecbe50f6-*.jsonl
size: 268KB
largest persisted user/tool-result line: ~68KB
cache_read_input_tokens later in the session: ~56k-57k

~/.claude/projects/.../e0a8ebf2-*.jsonl
size: 307KB
largest persisted user/tool-result line: ~107KB
cache_read_input_tokens later in the session: ~65k-66k

Tool activity that preceded the failures included normal project operations such as:

Read src/styles/global.css
Read src/pages/index.astro
Read src/pages/theater.astro
Bash git diff --stat
Bash grep/rg style searches

The project has a large stylesheet:

src/styles/global.css: ~6,465 lines, ~149KB
src/pages/index.astro: ~1,040 lines, ~69KB

Even when I explicitly started a new project session, used /clear, and instructed Claude not to rely on old context, the session still became large quickly and hit the same failure mode.

The visible UI made this confusing:

  • The user only sent a small number of prompts.
  • The context/token indicator did not make it obvious that large hidden tool results were being retained.
  • /compact also got stuck at 95% for ~20 minutes in a related session.
  • A fresh minimal non-persistent CLI request kept succeeding, so the base API/network path was healthy.

What Should Happen?

Fresh project sessions should not become unstable after ordinary tool use on moderately large source files. Claude Code should make the hidden context/tool-result footprint visible and should protect the user before sending oversized or fragile requests.

Expected behavior:

  1. A fresh session should not carry hidden state from prior sessions unless explicitly resumed.
  2. /clear should either remove enough hidden context/tool results to make the next turn safe, or clearly say what is still retained.
  3. Large Read/tool results should be summarized, capped, or excluded from future context by default unless the user explicitly opts in.
  4. The context indicator should account for persisted tool results and cache-read footprint, not just the visible conversation.
  5. If requests repeatedly fail with ECONNRESET after context growth, Claude Code should surface a context/session warning instead of retrying the same oversized request.
  6. /compact should not hang indefinitely near completion; if compaction cannot complete, it should fail with a clear recovery path.

Steps to Reproduce

This is not perfectly deterministic, but the pattern is reproducible on a frontend project with large CSS/Astro files:

  1. Start Claude Code in a project with a large stylesheet and page file:

``bash
cd /path/to/frontend-project
claude --model sonnet --effort medium --no-chrome
``

  1. Ask Claude to inspect current changes and continue UI work.
  2. Claude runs normal tools such as git status, git diff --stat, Read src/styles/global.css, and Read src/pages/index.astro.
  3. After several tool calls, the session JSONL grows quickly, including large persisted tool-result lines.
  4. Follow-up turns begin hanging/retrying or fail with:

``text
API Error: Unable to connect to API (ECONNRESET)
The socket connection was closed unexpectedly. For more information, pass
verbose: true in the second argument to fetch()
``

  1. Run a minimal non-persistent request from the same terminal/account/network:

``bash
claude -p 'Reply OK' --model sonnet --effort low --no-session-persistence --output-format json
``

  1. The minimal request succeeds quickly, suggesting the failure is tied to the project session/context rather than a general network outage.

Related Issues

Related but not identical:

  • #68231: large Read tool result injected into context, causing repeated ECONNRESET
  • #5674: persistent ECONNRESET reports
  • #45224: SSE keep-alive/heartbeat during long tool execution
  • #58554: JSONL/session-chain issues around ECONNRESET

This report is broader than a single oversized Read: even fresh project sessions can quickly accumulate large hidden context/tool-result state, making normal follow-up turns fragile while minimal non-persistent requests continue to work.

Error Messages/Logs

API Error: Unable to connect to API (ECONNRESET)
The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()

Related compact behavior:

Compacting conversation... stuck around 95% for ~20 minutes

Additional Information

From a user perspective, this is especially costly because the symptom looks like a network/VPN/proxy issue. In this case I spent significant time debugging network settings before isolating that:

  • minimal --no-session-persistence requests work;
  • failures correlate with project session context growth;
  • large tool results are persisted in JSONL;
  • the visible message count is much smaller than the actual hidden session payload.

Please consider adding guardrails around large tool results, clearer context accounting, and a recovery hint when ECONNRESET correlates with oversized session context.

View original on GitHub ↗

6 Comments

github-actions[bot] · 2 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/68231

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

yurukusa · 2 months ago

Your diagnosis is unusually thorough — pinpointing the ~700KB persisted tool_result line is the key, and I think it points somewhere slightly different from a context-window problem. Sharing a few things from poking at my own session JSONLs, plus a way to rescue the stuck session.
cache_read_input_tokens staying at ~48–66k means the token footprint is actually small — that's nowhere near a context limit. But the request body is large: Claude Code rebuilds the whole conversation and re-sends it on every turn, so once a 700KB tool_result is persisted, every subsequent request carries that 700KB+ in its body. ECONNRESET is the socket being closed mid-request, which is much more consistent with an oversized/slow upload than with a token limit. Your own control proves it: the --no-session-persistence minimal request (tiny body) succeeds on the same machine/network every time.
Falsifiable check: start fresh and deliberately don't let it Read the big files (see below). If ECONNRESET disappears, it's body-size, not network and not the context window.

  • /clear resets the conversation, but if the next turn re-Reads global.css (~149KB) and index.astro (~69KB), the large tool_result lines come right back within the same session. The growth you saw isn't leaked state from prior sessions — fresh sessions get their own <session-id>.jsonl and don't inherit old turns unless you --resume. It's re-reading the big files inside the new session.
  • /compact has to send the entire conversation to the model to summarize it. If the conversation already contains the oversized request that ECONNRESETs, compaction's own call hits the same wall — which lines up with it hanging at 95%.

The bloat is whole-file Reads of large source files. Until tool results are capped by default, you can avoid it:

  • Prefer Grep/rg to locate the relevant region, then Read with offset/limit instead of reading the whole 149KB stylesheet.
  • Ask it not to re-read files it already saw; one full read of global.css is ~37k tokens of body weight that then rides along on every later turn.

The session is on disk at ~/.claude/projects/<project>/<session-id>.jsonl, one JSON record per line, and you already know the bloated lines. You can shrink the giant tool_result content in place and resume. The one trap: don't delete the whole line — a tool_result is a user record carrying a tool_use_id that pairs with the preceding assistant tool_use; remove it and the next request becomes invalid (tool_use with no result → 400). Keep the record and its tool_use_id, just replace the huge content with a stub.
This script does that (back up the file first):

// node shrink.mjs path/to/<session-id>.jsonl   -> writes <...>.fixed.jsonl
import fs from "fs";
const f = process.argv[2];
const LIMIT = 50 * 1024;            // shrink any line bigger than 50KB
const stub = "[large tool output truncated to recover session]";
const out = fs.readFileSync(f, "utf8").split("\n").filter(Boolean).map(line => {
  if (Buffer.byteLength(line) <= LIMIT) return line;
  let o; try { o = JSON.parse(line); } catch { return line; }
  const c = o.message && o.message.content;
  if (Array.isArray(c)) for (const b of c) {
    if (b.type === "tool_result") {                 // keep tool_use_id, shrink content
      b.content = Array.isArray(b.content) ? [{ type: "text", text: stub }] : stub;
    }
  }
  if (o.toolUseResult) o.toolUseResult = { truncated: true }; // CC-internal, not sent to API
  return JSON.stringify(o);
});
fs.writeFileSync(f.replace(/\.jsonl$/, ".fixed.jsonl"), out.join("\n") + "\n");

Then move the .fixed.jsonl over the original (keep your backup) and claude --resume <session-id>. I tested the transform on synthetic records with both content shapes (array and string): every output line stays valid JSON and both tool_use_ids are preserved, 2.1MB → 0.6KB. The history reads as "tool output was truncated" but the session continues.
This is a workaround, not a fix — the real fixes are the ones you listed (cap/summarize large tool_results by default, and make the persisted footprint visible in the context indicator), which only Anthropic can do.

junaidtitan · 2 months ago

Your diagnosis is right: the session instability is driven by those persisted tool-result payloads in the JSONL, not conversation length. When CC does Read global.css (149KB), that full content lands in the JSONL and stays there — every subsequent API call re-sends it as context. With a few of those reads in a session the JSONL balloons fast, and the repeat ECONNRESET is the API choking on an oversized request body, not a network problem.

The /compact hang-at-95% is a related symptom: compaction runs via the API too, so if the context is already borderline on size, the compaction request itself times out or fails.

A tool called cozempic addresses this directly. cozempic current --diagnose will show you a breakdown of what's inflating the JSONL (tool-result payloads are usually the culprit) and how much of the context window they're occupying. cozempic treat current then prunes the oversized tool-result content from those sessions using a tool-output-trim strategy — keeping structure intact but removing the multi-hundred-KB payloads that are re-sent on every turn. Running it on your 1.9MB file should bring it to something the API can actually handle.

For prevention: the guard daemon (cozempic guard) monitors active sessions and auto-prunes before they hit the threshold that triggers the ECONNRESET loop. It terminates Claude first (so it's never a second writer to the file), prunes, then resumes — the runaway growth stays bounded without manual intervention.

What cozempic doesn't fix: CC reading and persisting large files in the first place. That's an Anthropic-side decision about what belongs in context. But the tool-result contents that are already in your JSONL can be reclaimed today, before the next session.

If you run --diagnose and the breakdown looks unexpected, worth sharing here — useful data for the Anthropic report.

yurukusa · 2 months ago

Exactly — and your /compact-hangs-at-95% point nails the second-order effect: compaction is itself an API round-trip that has to send the oversized body first, so the very thing you're trying to shrink is what makes the shrink request time out. That's why it stalls right at the end rather than failing fast.
cozempic is a good post-hoc fix (prune the JSONL after it's already bloated). The complementary move is preventive — stop the 149KB Read from landing in the transcript in the first place. A PreToolUse guard on Read that blocks a wholesale read of a large file (unless it's already scoped with offset/limit) keeps the giant tool_result out of the JSONL entirely:

INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[[ -z "$FILE" || ! -f "$FILE" ]] && exit 0
HASLIMIT=$(echo "$INPUT" | jq -r '(.tool_input.limit // .tool_input.offset) // empty')
[[ -n "$HASLIMIT" ]] && exit 0          # already scoped — allow
MAX=${CC_MAX_READ_BYTES:-262144}        # 256KB
SIZE=$(stat -c %s "$FILE" 2>/dev/null || stat -f %z "$FILE" 2>/dev/null || echo 0)
if (( SIZE > MAX )); then
  echo "BLOCKED: $FILE is $((SIZE/1024))KB. A wholesale Read persists in the JSONL and is re-sent every turn (#68339). Use Read with offset/limit, or grep for the part you need." >&2
  exit 2
fi
exit 0

I smoke-tested it: blocks a 300KB wholesale Read, but allows the same file when called with offset/limit, and allows small files untouched — so it nudges the agent toward scoped reads without getting in the way. Pair it with cozempic and you get both halves: prevention + cleanup.
The real fix is provider-side (cap how much of a single tool_result is re-serialized into each request, or summarize large reads after first use), but until then keeping big reads out of the transcript is the cheapest way to stop the body from growing toward ECONNRESET.

junaidtitan · 2 months ago

@yurukusa that PreToolUse hook is the most practical prevention I've seen for this specific problem. The 256KB ceiling with scoped-read pass-through is the right balance — nudges the agent toward chunked reads without blocking legitimate whole-file operations.

The prevention + cleanup framing is exactly right. Guard handles what slips through: files the agent decides are necessary regardless, pre-existing JSONL bloat before you add the hook, or any escape path the hook can't intercept. Running them together you get both halves — hook blocks new accumulation, guard keeps the existing file lean.

The real provider-side fix (cap how much of a single tool_result gets re-serialized into each request, or summarize large reads after first use) is the right long-term target. Until then this combination gets you most of the way there.

Dave-London · 1 month ago

The hidden-accumulation framing is spot on — it's the persisted tool-result footprint, not the visible message count, that grows. For the command side of that (the git status, git diff --stat, rg calls here), one lever is returning structured/stripped output instead of raw terminal text: I wrap those as MCP servers in Pare that emit compact schema-validated JSON (~85% fewer tokens), so each git/grep result adds far less to the footprint that piles up over a session. Doesn't help the large Read results — but it shrinks the command-output half.

Showing cached comments. Read the full discussion on GitHub ↗