Repeated context injection via JSONL attachments causes linear token bloat across long sessions
[Claude Code] Repeated context injection via JSONL attachments causes linear token bloat across long sessions
Where to submit: https://github.com/anthropics/claude-code/issues Label suggestion:bug,performance,context-management
---
Summary
Claude Code persists every hook result, built-in task_reminder, IDE events, skill-list changes, tool-call side-events, subagent progress, and last-prompt as separate attachment / last-prompt / progress entries in the session JSONL under <USER_HOME>/.claude/projects/<PROJECT_ID>/<SESSION_ID>.jsonl. On every subsequent API turn, history reconstruction re-includes these entries in the prompt payload, so the same content is re-transmitted many times within one session, producing linear token bloat that users perceive as "the same context being injected on every turn."
In a 150-minute live monitoring window across five concurrent sessions, this report documents:
- Linear, unbounded growth of per-turn API payload (~1,100 tokens/min) in a continuously-used session, reaching 661,706 tokens = 66% of a 1 M-token context window in 148 minutes.
- Auto-compaction is observable but does not help: three spikes of
cache_creation_input_tokensat 455K~558K each, immediately followed bycache_readreturning to its pre-compaction 500K level. /clearis a temporary reset, not a fix: 319 reset events were logged across four sessions; every reset was followed by rapid re-accumulation.- Per-tool-use fan-out: one Bash call produces ~3
attachment/hook_successentries. - Subagent silos:
Agent/Taskinvocations create separate JSONLs of up to 6.4 MB each, on top of the main session. - Global CLI history (
<USER_HOME>/.claude/history.jsonl) retains every input across all sessions/projects forever with no documented purge command.
All findings are reproducible across concurrent sessions on the same machine and persist after upgrading to the latest Claude Code v2.1.113.
---
Environment
| Field | Value |
|---|---|
| Claude Code versions observed | 2.1.110 → 2.1.112 → 2.1.113 (auto-update within same sessions) |
| Platform | Windows 11 Pro 10.0.26100 |
| Shell | bash (Git Bash on Windows) |
| Node.js | v24.11.1 |
| Model | Opus 4.7 (1M context) |
| OS locale | Korean (language: "korean") |
Same sessionId is reused across CLI restarts and version upgrades, so accumulated attachment / last-prompt entries carry over — a restart does not reset the re-injection problem.
---
Reproduction
Any long-running session reproduces this. Below are five concurrent sessions on the same machine; all show the same structural pattern.
session size attachment task_reminder last-prompt
------- ---- ---------- ------------- -----------
<PROJECT_A> 2.2 MB 532 16 54
<PROJECT_B> 2.5 MB 429 14 62
<PROJECT_C> 7.2 MB 1,413 32 168
<PROJECT_D> 3.4 MB 992 11 81
<PROJECT_E> 870 KB 154 9 20
Minimal steps to reproduce:
- Start a Claude Code session in any project.
- Use tools (Bash/Edit/Write/Read) and invoke skills/agents for 30+ turns.
- Inspect
<USER_HOME>/.claude/projects/<PROJECT_ID>/<SESSION_ID>.jsonl. - Observe the entry-type distribution and repeated content.
---
Evidence
1. Entry-type distribution in a 7.2 MB session (<PROJECT_C>)
entry type count bytes %
attachment 1,413 1.66 MB 22%
assistant 860 1.79 MB 25%
user 684 2.71 MB 37%
permission-mode 176 20 KB 0%
last-prompt 168 26 KB 0%
file-history-snapshot 97 297 KB 4%
custom-title 84 9 KB 0%
agent-name 84 9 KB 0%
system 66 52 KB 1%
queue-operation 11 2 KB 0%
attachment alone is 22% of the JSONL. Combined with last-prompt and file-history-snapshot, Claude-Code-generated metadata exceeds 26% of persisted session bytes.
2. Repeated attachment content — hash-based duplication analysis
hash repeats size/copy cumulative waste
<HASH_1> 16 × 56 B 896 B task_reminder itemCount=0 (empty list)
<HASH_2> 9 × 51 B 459 B
<HASH_3> 5 × 8,730 B 43,650 B (large repeated payload)
<HASH_4> 5 × 2,931 B 14,655 B
<HASH_5> 5 × 456 B 2,280 B specific task object persisted 5×
<HASH_6> 3 × 1,396 B 4,188 B specific task object persisted 3×
- Total
attachmentbytes: ~1.72 MB - After content-hash deduplication: ~1.56 MB
- Bytes wasted on duplicates in storage alone: ~154 KB (~9%)
That 9% is storage only. The amplification is in the API payload: each past attachment is re-included on every subsequent API turn, so one duplicate attachment in storage becomes duplicate re-transmission on every future turn.
3. attachment.type catalog — not just hook output
Live observation on v2.1.113 shows Claude Code persists at least nine distinct attachment types, covering almost every in-session state change:
attachment.type role
------------------------ -----------------------------------------------------
hook_success user + plugin hook results (dominant, 89 / 300 entries in sample)
task_reminder built-in TodoWrite nag
opened_file_in_ide IDE file-open tracking
deferred_tools_delta tool list updates
skill_listing skill list snapshot
command_permissions permission mode changes
date_change system clock date rollover
hook_non_blocking_error hook execution failures
hook_additional_context hook-injected additionalContext
hook_system_message hook-emitted system messages
Every one of these is persisted as a separate JSONL entry and is a candidate for re-injection on subsequent turns.
4. task_reminder is emitted by Claude Code itself (not user hooks)
itemCount distribution (<PROJECT_C> session):
0: 16 ← "task tools haven't been used recently" reminder
1: 5
3: 3
4: 5
7: 1
10: 1
13: 1
The itemCount: 0 variant is the built-in <system-reminder>The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate ...</system-reminder> nag. It is stored as a separate attachment entry sixteen times in one session. A built-in Claude Code reminder is a primary contributor — user plugins/hooks are not the cause.
5. last-prompt entries duplicate user input
total last-prompt entries: 168
unique content hashes: 22
mean copies per unique: 7.6 ×
The same user prompt string is persisted up to 16 times per session. A 2 KB user message therefore occupies up to 32 KB of JSONL through this mechanism alone — before considering its inclusion in subsequent API payloads.
6. <system-reminder> tags are not stored in the user-message body
When parsing message.content[].text for type: user entries specifically, zero <system-reminder> tags are found in the <PROJECT_C> session, even though those tags appear in the live prompt. This proves:
<system-reminder>blocks are runtime-injected during API payload construction, not persisted.- The persistence that causes bloat is not the reminders themselves — it is the
attachment/last-promptentries that Claude Code reads back in and re-attaches on every turn.
7. Cross-session confirmation rules out user-side causes
The 5 sessions above are in 5 different projects with different CLAUDE.md, different plugin activity, and different workflows. All show the same attachment / task_reminder / last-prompt accumulation pattern. This is a Claude Code binary behavior, not a user misconfiguration.
8. Direct API-payload evidence — usage fields returned by Anthropic API
The message.usage object in each assistant entry is server-returned and non-forgeable. It reports the exact token counts Anthropic's API billed for the preceding request. Plotting these across a session shows the payload growth directly.
session turns first-turn 25% 50% 75% last-turn
------- ----- ---------- ------- ------- ------- ---------
<PROJECT_C> 860 52,572 80,656 152,242 285,677 498,255
<PROJECT_B> 390 86,804 189,077 349,696 422,434 497,299
<PROJECT_D> 530 89,619 185,527 251,836 343,565 431,287
<PROJECT_E> 105 92,183 134,735 168,584 198,054 242,501
Last-turn breakdown:
session input cache_read cache_creation output TOTAL
------- ----- ---------- -------------- ------ --------
<PROJECT_C> 1 497,533 721 1,662 498,255
<PROJECT_B> 1 496,604 694 1,123 497,299
<PROJECT_D> 1 430,331 955 1,274 431,287
<PROJECT_E> 6 231,371 11,124 3,200 242,501
Key observations:
input_tokensis 1~6 across every sampled last turn — essentially no "genuinely new" content is sent raw.cache_read_input_tokenscarries 231K~497K — the entire conversation (including every historical attachment, tool_result, last-prompt, assistant output) is re-included in every request via prompt cache.cache_creation_input_tokens= 721~11,124 per turn — this is the increment appended to cache each turn, which then compounds into the next turn'scache_read.- Growth is approximately linear in turn count:
<PROJECT_C>sampled at turns 0 / 215 / 430 / 645 / 859 yields 52K / 80K / 152K / 285K / 498K, averaging ~540 tokens of permanent context growth per turn. <PROJECT_C>'s last turn occupies 49.8% of the 1M-token context window before any new user input. Earlier turns in that same session fit comfortably — the growth is structural, not driven by user content.
9. Mapping cache growth back to attachment accumulation
From the attachment analysis above, <PROJECT_C> contains ~1.72 MB of attachment bytes across 1,413 attachment entries, averaging ~1.2 KB per attachment ≈ ~300 tokens. The per-turn cache_creation_input_tokens range of 721~11,124 observed in §8 corresponds to roughly 2~36 attachment-equivalents per turn being appended to the running context.
This is the arithmetic bridge between §2~§5 (what gets persisted in JSONL) and §8 (what actually gets billed in the API request). It confirms the originally-hypothesized mechanism: Claude Code stores per-turn events as attachments in JSONL, then re-sends them on every subsequent API call via the prompt cache, and the cache size — and therefore per-turn billable tokens — grows linearly with the number of turns.
10. Idle Claude Code still accumulates JSONL — /clear with zero user input
Starting Claude Code v2.1.113 and immediately pressing /clear twice, with zero user text input, produces a micro-session whose JSONL already contains seven entries totalling ~3.9 KB:
entry 0 file-history-snapshot (auto-generated by Claude Code, no timestamp)
entry 1 attachment/hook_success hookName = SessionStart:clear
entry 2 attachment/hook_system_message hookName = SessionStart:clear content = "bkit v2.1.7"
entry 3 attachment/hook_success hookName = SessionStart:clear
entry 4 user <local-command-caveat>…</local-command-caveat>
entry 5 user <command-name>/clear</command-name>…
entry 6 system/local_command (empty stdout)
Observations:
/clearre-fires SessionStart hooks — matcher name observed is literallySessionStart:clear. Every SessionStart-bound hook (user and plugin) emits a freshattachmententry, even though the user did not start a new session deliberately.- User-typed bytes: 0. Persisted JSONL bytes: 3,941. Every byte of the JSONL is eligible for re-inclusion in the next API request.
- A user who never types anything, only presses
/clear, still pays the cache-creation tax for these attachments.
This rules out "the user is accumulating context" as a possible cause. The accumulation happens purely from Claude Code's own lifecycle events.
11. v2.1.113 upgrade does not fix the issue
Same sessions analyzed before and after auto-upgrade, splitting entries by the version field they were written under:
session version entries attach tr:itemCount=0 last-p(unique)
-------- ------- ------- ------ -------------- --------------
<PROJECT_C> 2.1.110 1,937 945 16 —
<PROJECT_C> 2.1.112 522 189 0 —
<PROJECT_C> 2.1.113 533 279 0 168 (22 uniq, 7.6×)
<PROJECT_E> 2.1.112 288 136 5 —
<PROJECT_E> 2.1.113 23 9 0 20 ( 8 uniq, 2.5×)
<PROJECT_D> 2.1.112 1,546 793 4 —
<PROJECT_D> 2.1.113 369 199 7 81 (11 uniq, 7.4×)
<PROJECT_B> 2.1.112 714 249 1 —
<PROJECT_B> 2.1.113 395 187 1 63 ( 8 uniq, 7.9×)
Readings:
attachment / entriesratio on v2.1.113 is 40~54% — equal to or worse than older versions.task_reminder itemCount=0emission on v2.1.113: decreases in some sessions, increases in others — the patch is not a coalescing fix; it is situational.last-promptcontinues to persist ~7× copies of each unique prompt on v2.1.113 (no improvement).- CLI restart keeps the same
sessionIdand writes to the same JSONL — prior accumulation carries over and is re-included in future API payloads.
v2.1.113 does not address the root cause.
12. Tool-use amplification — one Bash call produces ~3 attachment entries
Inspecting the last 12 entries written during an active turn of this reporting session shows a repeating motif. One Bash invocation expands into one tool_use / tool_result pair plus three attachment/hook_success entries (one PreToolUse + another PreToolUse variant + one PostToolUse). A typical slice:
[user-input]
file-history-snapshot (auto-generated by Claude Code)
assistant: thinking
assistant: tool_use ← Bash call #1
attachment/hook_success ← PreToolUse:Bash hook
attachment/hook_success ← PreToolUse:Bash hook (second)
user: tool_result
attachment/hook_success ← PostToolUse:Bash hook
assistant: thinking
assistant: text
assistant: tool_use ← Bash call #2
...
Implication: every tool use, regardless of content, appends three attachment entries to the JSONL. Ten Bash invocations → 30 new attachments. Given §9's conversion of ~300 tokens per attachment, this is ~9K tokens per 10 tool calls purely from hook-success persistence, all of which re-enter subsequent turns' prompt caches.
13. Subagent JSONL silos — parallel accumulation under subagents/
Agent / Task tool invocations create a separate JSONL per subagent under <PROJECT_ROOT>/subagents/agent-*.jsonl. These exhibit the same attachment accumulation pattern as the parent session, compounding the problem.
total subagent JSONL files on machine : 1,091
largest single subagent JSONL : 6,378,381 B (6.4 MB)
largest subagent internal composition : progress 2,615 assistant 163 user 97 system 9
The progress entry type appears only in subagent JSONLs — one per step of the subagent's execution — and each carries its own persisted state. A single 6.4 MB subagent JSONL is comparable in size to a large main session, and every parent-session turn that invoked a subagent pays both costs.
14. Global CLI history file never rotates
<USER_HOME>/.claude/history.jsonl stores every command-line input typed into Claude Code across all sessions and all projects on the machine, forever.
<USER_HOME>/.claude/history.jsonl 846,561 B
lines 2,674
schema {display, pastedContents, project, sessionId, timestamp}
sample display values "/exit", "/clear", <free-form user prompts>
Unlike session JSONLs, this file is not re-injected into API requests, so it does not contribute to per-turn token bloat. However:
- It accumulates without bound;
/cleardoes not touch it. - It preserves every prompt a user typed — which for security-sensitive users can include paths, API keys, database URLs that were pasted on the CLI.
- There is no documented mechanism to rotate or purge it.
This is a separate issue in spirit (privacy / data retention rather than per-turn cost), but it shares the root cause pattern: Claude Code persists everything and provides no user-level trimming controls.
15. 150-minute continuous live monitoring — linear growth, auto-compaction inefficacy, /clear is temporary
A persistent monitor snapshotted five active sessions every 30 seconds from 09:22 to 12:00 local time (316 log lines, 314 snapshots). Full per-session findings:
15.1 Longest continuously-used session: <PROJECT_C> (148 min, 230 snapshots, zero /clear)
elapsed size total_tokens cache_r cache_c input
------- ------------ ------------ ---------- --------- -----
0m 7,273,633 498,255 497,533 721 1
10m 7,300,616 500,013 44,488 455,519 6 ← auto-compaction
20m 7,524,612 524,962 522,689 2,272 1
60m 8,092,572 582,403 44,488 537,909 6 ← auto-compaction
70m 8,293,253 602,914 44,488 558,420 6 ← auto-compaction
120m 8,606,324 638,367 637,545 821 1
140m 8,850,869 661,706 661,594 111 1
148m 8,850,869 661,706 661,594 111 1 (final)
148 min Δ: size +1.58 MB / tokens +163,451 (≈ 1,100 tokens/min, linear)
peak: 661,706 tokens = 66% of a 1 M-token context window
The user performed normal work in this session; no /clear or /compact commands were issued. Per-turn input_tokens stayed at 1~6 throughout, confirming that growth is driven by Claude-Code-persisted state, not by user text.
15.2 Auto-compaction is observed — and does not help
At the 10 min, 60 min, and 70 min marks, cache_read_input_tokens collapses from ~500 K to 44,488 while cache_creation_input_tokens spikes to 455 K~558 K in the same turn. This is the observable signature of prompt-cache reconstruction / auto-compaction.
time cache_r before cache_r after cache_c (that turn) billable amplification
----- -------------- ------------- ------------------- -------------------------
10m 497,533 44,488 455,519 cache_c billed at 125% of input
60m ~538,000 44,488 537,909 equivalent to ~672 K input tokens
70m ~558,000 44,488 558,420 equivalent to ~698 K input tokens
Yet the very next snapshot after each compaction event shows cache_r back in the 500 K range. Auto-compaction does not reduce the context that subsequent turns have to carry — it rebuilds the cache from that same context and immediately resumes accumulation. In effect the user pays a cache-creation surcharge and the new cache starts filling back up at the same rate.
15.3 /clear is reset, not a fix — five-session reset tally
session snapshots reset events peak tokens final tokens
--------------- --------- ------------ ----------- ------------
<PROJECT_C> 230 0 661,706 661,706 (monotonic)
<PROJECT_A> 411 103 266,927 266,927 (final re-accumulation to peak)
<PROJECT_B> 502 211 562,491 0 (ended in /clear state)
<PROJECT_D> 213 1 558,418 0 (single late reset)
<PROJECT_E> (this session) 173 4 290,986 89,945 (repeated reset / accumulate)
A "reset event" is detected when last_total_tokens falls to less than 50 % of the previous snapshot's value with the previous value > 50 K.
Readings:
- The only session that avoided resets is the one where the user never ran
/clear. It is also the one that reached 66 % of the 1 M window. <PROJECT_B>and<PROJECT_A>users/clear-ed many times (211 and 103 resets). After each reset, the session re-accumulated within minutes.<PROJECT_A>eventually reached 266 K tokens at the 110-min mark despite heavy/clearuse.- Resetting is a perpetual cost — users are effectively paying for context they throw away because they cannot selectively prune persisted attachments.
15.4 Aggregate over 150 min
total snapshots across 5 sessions : 1,529
combined JSONL size change in 150 min : +0.65 MB net (with four sessions resetting)
longest uninterrupted accumulation : 148 min → +163 K tokens linear
largest peak observed : 661,706 tokens (66 % of 1 M window)
auto-compaction events observed in one session: 3
total /clear-induced resets observed : 319 across 4 sessions
---
Expected vs Actual
Expected
- History reconstruction sends past turns without re-attaching transient metadata that is already summarized in the current turn.
- Built-in nags (
task_reminderwithitemCount: 0) emit at most once per threshold condition, not per tool call. last-promptis a pointer, not a full-content copy on every write.- Restarting the CLI, auto-updating, or idling resets ephemeral attachment accumulation.
- Auto-compaction meaningfully reduces the persisted context a subsequent turn must carry.
- Subagents (
Agent/Task) run in an isolated context that does not compound main-session cost. - One tool call produces roughly one persisted event.
Actual
- Each past attachment is re-included on every API turn → payload grows linearly with turn count (§8, §15.1).
task_reminder itemCount:0is appended on nearly every tool use where TodoWrite is not active → users pay for the same nag 16+ times per session (§4).last-promptduplicates the prompt string ~7-8× on average (§5).- Restart keeps the same
sessionIdand all accumulated attachments; idle sessions still accumulate when/clearis pressed (§7, §10). - Auto-compaction was observed three times in one 148-min session (§15.2). Each event spikes
cache_creationby 455K~558K tokens (billed at 125% of input), and the immediately following turn'scache_readreturns to the same pre-compaction level — compaction is billed but does not shrink ongoing per-turn payload. - Subagent
Taskcalls create independent JSONLs undersubagents/agent-*.jsonlthat reach 6.4 MB each and repeat the same attachment accumulation pattern (§13). - One Bash tool call produces roughly three
attachment/hook_successentries (§12).
---
Impact
- Cost: API token consumption grows faster than the information content of the conversation. 5-minute prompt-cache TTL does not absorb this because session JSONL entries change on every turn, invalidating cache boundaries downstream of the first modification. In a 148-minute continuously-used session, the per-turn API payload grew linearly by ~1,100 tokens/min and reached 661,706 tokens — 66% of a 1M-token context window (§15.1).
- Latency: Larger payloads → slower first-token time and slower auto-compaction triggers. Auto-compaction itself introduces 455K~558K-token spikes (§15.2).
- Context window: Usable context shrinks faster than the visible conversation warrants, forcing premature
/clearor/compact. Heavy/clearusers were observed resetting 211 times in a single session (<PROJECT_B>, §15.3) without ever escaping re-accumulation; the user who never/clear-ed hit 66% of the 1M window. - User experience: Users perceive "the same context injected repeatedly" without a clear mitigation path because the cause is internal and survives restarts, upgrades,
/clear, and/compact. - Privacy (secondary):
<USER_HOME>/.claude/history.jsonlretains every CLI input across all projects forever, with no documented purge command (§14).
---
Requested
One or more of:
- Deduplicate
attachmententries by content hash before including them in API payload history reconstruction. - Coalesce
task_reminder— emit at most once per N turns or once per distinctitemCountchange, and suppress theitemCount: 0variant beyond the first occurrence. - Trim
last-promptstorage — store a reference/hash once rather than a full copy on each submit. - Collapse
skill_listing/deferred_tools_delta/command_permissions— only persist deltas that actually change state. - Reduce per-tool-use attachment fan-out — one
hook_successper tool call is enough; the current ~3 (PreToolUse × 2 + PostToolUse) multiplies cost for every Bash/Edit/Write. - Stop re-injecting subagent
progressentries during main-session history reconstruction; subagent JSONLs already contain the full trace. - Make auto-compaction actually lossy — after a
cache_creationspike of the entire conversation, discard or summarize the next turn's persisted history rather than immediately lettingcache_readreturn to the same pre-compaction level. - Expose a user-level setting (e.g.
settings.json → experimental.historyCompaction: true) so power users can opt into aggressive attachment compression. - Offer a
/clear-attachmentscommand that strips non-essentialattachmenttypes from the current JSONL in place, without ending the session. - Rotate or cap
<USER_HOME>/.claude/history.jsonl— right now it stores every CLI input across all projects forever, including pasted secrets. - Document the current injection semantics in the Hooks / Context Management section so users can audit their own sessions with confidence.
---
Workarounds the user tried (limited effect)
- Removing dormant
UserPromptSubmithooks (prd-detector,skill-router, disabled-plugin leftovers) slightly reducesattachmentcreation rate. - Using
TodoWriteeagerly prevents theitemCount: 0task_reminderfrom firing. - Running
/clearresets JSONL entry count but loses context. In the 150-min monitoring window, 319/clearresets were observed across 4 sessions, each followed by rapid re-accumulation — never a durable fix. - Running
/compactor waiting for auto-compaction: observed three times in one 148-min session; each event spikescache_creationby 455 K~558 K tokens (billed at 125% of input), and the very next turn'scache_readis back in the 500 K range — compaction is billed but does not meaningfully shrink the ongoing per-turn payload. - Uninstalling unused plugins reduces hook fan-out.
- Avoiding
Agent/Tasktool use avoids creating subagent JSONL silos, but at the cost of losing parallelism. - Upgrading to v2.1.113 — no material change.
None address the underlying re-injection on every turn.
---
Reproduction artifacts
JSONL files referenced above (paths redacted):
<USER_HOME>/.claude/projects/<PROJECT_A>/<SESSION_ID_A>.jsonl (2.2 MB)
<USER_HOME>/.claude/projects/<PROJECT_B>/<SESSION_ID_B>.jsonl (2.5 MB)
<USER_HOME>/.claude/projects/<PROJECT_C>/<SESSION_ID_C>.jsonl (7.2~8.9 MB, grew during monitoring)
<USER_HOME>/.claude/projects/<PROJECT_D>/<SESSION_ID_D>.jsonl (3.4~4.6 MB)
<USER_HOME>/.claude/projects/<PROJECT_E>/<SESSION_ID_E>.jsonl (0.1~1.2 MB, reset during monitoring)
<USER_HOME>/.claude/projects/<PROJECT_*>/subagents/agent-*.jsonl (1,091 files machine-wide, largest 6.4 MB)
<USER_HOME>/.claude/history.jsonl (846,561 B, 2,674 entries, machine-wide)
Live-monitoring artifact:
<USER_HOME>/.claude/plugins/…/monitor.log (JSON Lines, 314 snapshots at 30 s intervals,
09:22–12:00 local time, 321 KB)
Schema of one monitor.log snapshot:
{
"ts": "2026-04-20T11:48:32",
"sessions": [
{
"session": "<PROJECT_C>",
"size": 8850869,
"mtime_ago_s": 4,
"last_total_tokens": 661706,
"last_input": 1,
"last_cache_r": 661594,
"last_cache_c": 111,
"last_out": 1662,
"last_version": "2.1.113"
}
]
}
The numeric tables in §Evidence can be regenerated by (a) counting "type":"attachment", "task_reminder", "type":"last-prompt" occurrences in the JSONLs, (b) hashing attachment payloads for dedup counts, or (c) replaying the monitor.log to reconstruct the per-session time series.
---
Checklist before submission
- [ ] Run one of the above JSONL files through the Python snippet in §Reference below and paste the output into the GitHub issue.
- [ ] Attach (or link) a sanitized JSONL excerpt of a repeated-attachment block.
- [ ] Confirm Claude Code version with
claude --versionand paste the exact string. - [ ] If possible, include
/costoutput from a session that hit the problem. - [ ] Optionally run the live-monitor script (§Monitor Script) for 30+ min in an active session and attach its
monitor.log. - [ ] Redact any proprietary project names from the JSONL and from the paths before attaching.
- [ ] Redact any personally-identifying strings from
<USER_HOME>/.claude/history.jsonlif referenced.
---
Reference snippet for reproducing the numbers
# Usage: python this_script.py <USER_HOME>/.claude/projects/<PROJECT_ID>/<SESSION_ID>.jsonl
import json, hashlib, collections, sys
path = sys.argv[1]
counts = collections.Counter()
hashes = collections.Counter()
task_reminder_items = collections.Counter()
attachment_types = collections.Counter()
version_split = collections.defaultdict(lambda: {'entries':0, 'attach':0, 'tr0':0, 'lp':0})
last_prompt_hashes = set()
last_prompts = 0
with open(path, 'r', encoding='utf-8') as f:
for line in f:
try:
d = json.loads(line)
except Exception:
continue
t = d.get('type', '')
v = d.get('version', 'unknown')
counts[t] += 1
version_split[v]['entries'] += 1
if t == 'attachment':
version_split[v]['attach'] += 1
att = d.get('attachment', {})
attachment_types[att.get('type', '?')] += 1
h = hashlib.md5(json.dumps(att, sort_keys=True).encode()).hexdigest()[:12]
hashes[h] += 1
if att.get('type') == 'task_reminder':
ic = att.get('itemCount', -1)
task_reminder_items[ic] += 1
if ic == 0:
version_split[v]['tr0'] += 1
elif t == 'last-prompt':
last_prompts += 1
version_split[v]['lp'] += 1
last_prompt_hashes.add(hashlib.md5(d.get('lastPrompt', '').encode()).hexdigest()[:12])
print('entry type counts :', dict(counts))
print('attachment.type catalog :', dict(attachment_types))
print('top repeated attachment :', hashes.most_common(10))
print('task_reminder itemCount dist:', dict(task_reminder_items))
print(f'last-prompt: {last_prompts} entries, {len(last_prompt_hashes)} unique')
print('by version :', dict(version_split))
---
Monitor Script — capture live per-turn growth
Save as monitor.py and run in the background (python monitor.py &). Writes one JSON line per snapshot every 30 seconds.
"""
Snapshot every 30 s until today's 12:00 local. Logs size + last assistant usage
(input / cache_read / cache_creation / output) for every session JSONL modified
within the last 10 minutes. One JSON line per snapshot appended to monitor.log.
"""
import json, os, glob, time, datetime
HERE = os.path.dirname(os.path.abspath(__file__))
LOG = os.path.join(HERE, 'monitor.log')
PROJECTS_ROOT = os.path.expanduser('~/.claude/projects')
def last_assistant_usage(path):
try:
last = None
with open(path, 'r', encoding='utf-8') as f:
for line in f:
try:
d = json.loads(line)
except Exception:
continue
if d.get('type') == 'assistant' and d.get('message', {}).get('usage'):
last = d
if not last:
return None
u = last['message']['usage']
return {
'v': last.get('version', ''),
'in': u.get('input_tokens', 0),
'cache_r': u.get('cache_read_input_tokens', 0),
'cache_c': u.get('cache_creation_input_tokens', 0),
'out': u.get('output_tokens', 0),
}
except Exception:
return None
def snapshot():
now = time.time()
files = glob.glob(os.path.join(PROJECTS_ROOT, '**', '*.jsonl'), recursive=True)
active = []
for f in files:
try:
mt = os.path.getmtime(f)
sz = os.path.getsize(f)
except OSError:
continue
if now - mt < 600:
active.append((f, mt, sz))
active.sort(key=lambda x: -x[1])
sessions = []
for f, mt, sz in active[:8]:
u = last_assistant_usage(f) or {}
total = u.get('in', 0) + u.get('cache_r', 0) + u.get('cache_c', 0)
rel = f[len(PROJECTS_ROOT):]
for ch in ('/', os.sep):
rel = rel.lstrip(ch)
label = rel
for ch in ('/', os.sep):
if ch in label:
label = label.split(ch, 1)[0]
break
sessions.append({
'session': label, 'size': sz, 'mtime_ago_s': int(now - mt),
'last_total_tokens': total,
'last_input': u.get('in', 0),
'last_cache_r': u.get('cache_r', 0),
'last_cache_c': u.get('cache_c', 0),
'last_out': u.get('out', 0),
'last_version': u.get('v', ''),
})
return {
'ts': datetime.datetime.now().isoformat(timespec='seconds'),
'sessions': sessions,
}
def main():
now = datetime.datetime.now()
end = datetime.datetime.combine(now.date(), datetime.time(12, 0, 0))
if end <= now:
end = end + datetime.timedelta(days=1)
with open(LOG, 'a', encoding='utf-8') as fp:
fp.write(json.dumps({'ts': now.isoformat(timespec='seconds'), 'event': 'monitor_start',
'end_target': end.isoformat(timespec='seconds')}) + '\n')
fp.flush()
while datetime.datetime.now() < end:
fp.write(json.dumps(snapshot(), ensure_ascii=False) + '\n')
fp.flush()
time.sleep(30)
fp.write(json.dumps({'ts': datetime.datetime.now().isoformat(timespec='seconds'),
'event': 'monitor_end'}) + '\n')
if __name__ == '__main__':
main()
Post-processing (time-series and reset detection):
import json, time
p = 'monitor.log'
snaps = [json.loads(l) for l in open(p, encoding='utf-8') if l.strip() and 'sessions' in l]
def e(ts): return time.mktime(time.strptime(ts, '%Y-%m-%dT%H:%M:%S'))
series = {}
for s in snaps:
for ses in s['sessions']:
series.setdefault(ses['session'], []).append((e(s['ts']), ses['size'],
ses['last_total_tokens'],
ses['last_cache_r'],
ses['last_cache_c']))
for name, rec in series.items():
resets = sum(1 for i in range(1, len(rec))
if rec[i-1][2] > 50000 and rec[i][2] < rec[i-1][2] * 0.5)
peak = max(rec, key=lambda r: r[2])
print(f'{name}: snaps={len(rec)} resets={resets} peak_tokens={peak[2]}')This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗