Ramon SosaRamon Sosa·10 min read

Claude Code now compacts idle sessions: what mine cost me

Two recent Claude Code changes that hit what costs me most: long sessions I leave open and hooks that fail silently. What changed and what I'll do.

In this article
  1. My numbers
  2. Why coming back costs so much
  3. 1. It compacts while you're away
  4. What I measured on October 19
  5. 2. Hooks that block when they fail
  6. The first thing I did
  7. What I still need
  8. Check yours

Since 2.1.299 (Oct 14), Claude Code compacts on its own, in the terminal, a conversation over 200,000 tokens that has sat idle for about 54 minutes. And since 2.1.295 (Oct 8), a hook can stop the action when it fails.

I leave Claude Code sessions open all day. I step out for a call, come back an hour later and pick up where I was. What I didn't see was what that return costs. One of these two changes goes straight at that. The other touches something I took for granted: that my hooks always run.

My numbers

A few weeks ago I counted my own sessions from the logs Claude Code keeps. In 14 days of September there were 264 sessions and 9,249 requests. This is how the cost split:

These are two different counts. This one measures cost and I ran it on September 30 over the logs from those 14 days. The 195 sessions in what is Claude Code count only sessions I opened myself, no subagents, with the logs I had on October 12.

Re-reading the cache
69%
Writing the cache and replies
31%
My sessions, 14 days of September 2026, counted from Claude Code's logs on September 30. The weight per token type is mine (cache write 1.25, re-read 0.1, reply 5), not the bill.

More than two thirds of the cost is re-reading what was already in the conversation, not what the AI writes. And six sessions took 57% of everything. They were the long ones: hundreds of requests on the same conversation.

Here is how I counted. Claude Code saves each session as a file on your Mac, and every reply carries its token count: what it read from the cache, what it wrote into it and what it produced. I added those three amounts per session and gave each one its weight. The weights are close to the bill's, but they are not the bill.

If you want numbers for your own setup before you change anything, the agent cost calculator uses today's prices.

Why coming back costs so much

An example with round numbers. You have a 200,000-token conversation, you step out for an hour and when you come back you type “keep going”.

  1. With the cache alive, Claude Code re-reads the 200,000 tokens. They weigh 0.1 each: 20,000 units.
  2. With the cache expired, it has to write them again. They weigh 1.25 each: 250,000 units.
  3. Same message, same result, and 12.5 times the weight (250,000 divided by 20,000).

Those are my units, not money. The ratio is what matters: the message you type when you return from an hour-long call pays for the whole conversation as if it were new. At the published one-hour write rate (2 instead of 1.25) it would be 20 times.

  1. Minute 0You step out for a call. The conversation already weighs more than 200,000 tokens.
  2. Minute 54With 2.1.299, Claude Code compacts it in the terminal.
  3. Minute 60The one-hour cache expires. Without compaction, the next message rebuilds everything.
  4. You returnWith compaction you continue from the summary. Without it, you pay for the full rebuild.
The 54 minutes come from the 2.1.299 changelog; the one-hour cache is the one I explain in the next section. Your return time is what decides.

1. It compacts while you're away

Claude Code keeps your conversation in a cache that lasts one hour. While it is alive, each message is cheap. If you are gone for more than an hour, the cache expires and your next message has to process the whole conversation again.

Idle compaction is exactly that: Claude Code summarizes a long conversation while you are away, before the cache runs out. It shipped in version 2.1.299 on October 14: if a conversation over 200,000 tokens sits idle for about 54 minutes in the terminal, it gets compacted. When you come back, it continues from the summary, not from scratch.

Sources: official Claude Code changelog, 2.1.299; Classmethod, Oct 14, 2026

What you lose: compacting is summarizing, and a summary leaves details out. If you are in the middle of something where every detail counts, turn it off with "idleCompaction": false in your settings.json. The 200,000 threshold changes with CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS.

I am leaving it on. The comparison:

OnOff
If you leave for more than 54 minutes with over 200,000 tokensIt summarizes itself and you continue from the summary when you returnNothing happens: after the hour, the return message processes everything
What you loseDetails the summary leaves outNothing from the conversation, but you pay for the rebuild
Where I would use itLong working sessions with pauses, like calls or lunchA debugging session where every line of the error counts
The first two rows come from the changelog and how the cache works. The last one is my judgment.

What I measured on October 19

On October 16 I wrote here that I had not measured how many of my six expensive sessions went past 200,000 tokens. Now I measured all my saved sessions, not just those six.

The count is simple. For each session I sort the replies by time. If 54 minutes or more pass between two replies and the conversation already weighed 200,000 tokens or more, I count one return. Then I look at that reply: if it read less than half of what was already in the conversation from the cache, I count the cache as rebuilt.

What I countedResult
Sessions with 3 or more replies698
Went past 200,000 tokens120
With at least one pause of 54 min or more above 200,00063
Returns after that pause470
Between 54 min and 3 h190
Between 3 h and 12 h102
Over 12 h178
With the cache almost fully rebuilt452 of 470
In the terminal (cli)9 sessions, 10 returns
In the desktop app54 sessions, 460 returns
My sessions saved on this Mac, June 4 to October 19, 2026. Main conversation only, no subagents. The logs keep growing: the script may show a session or two more.

I see three things. First, 63 of my 120 big sessions, more than half, had at least one of those pauses. Second, in 452 of 470 returns the cache was already gone. Third, almost all of it happens in the desktop app: 54 of the 63 sessions and 460 of the 470 returns. The 2.1.299 changelog says idle compaction arrived “in the terminal”, so I don't know whether the app applies it. In the terminal I counted only 10 returns.

The 190 returns between 54 minutes and 3 hours, a call or lunch, are the case the feature describes. What I don't know: what it does with the 178 over 12 hours, which are nights and weekends. I also didn't measure how many would have changed with 2.1.299. My terminal claude is on 2.1.294, but the desktop app already runs 2.1.299 in 28 of my 60 most recent sessions, so this count mixes before and after. And a pause between two replies can also be a slow tool; I did not separate that.

If you want your own number, this is the script I used. It only reads and it only prints totals:

# count-returns.py: read-only. Prints totals, never the content of your conversations.
import glob, json, os
from datetime import datetime

PAUSE = 54 * 60      # seconds without a reply
BIG = 200_000        # context tokens

def context(u):
    return sum(u.get(k) or 0 for k in ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"))

sessions = big = with_pause = 0
returns = []
cli_sessions = 0
for path in glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")):
    replies = {}
    cli = False
    for line in open(path, errors="ignore"):
        try:
            d = json.loads(line)
        except ValueError:
            continue
        cli = cli or d.get("entrypoint") == "cli"
        u = (d.get("message") or {}).get("usage")
        if d.get("type") != "assistant" or d.get("isSidechain") or not u or not d.get("timestamp"):
            continue
        t = datetime.fromisoformat(d["timestamp"].replace("Z", "+00:00")).timestamp()
        key = (d["message"].get("id"), d.get("requestId"))
        if key not in replies or context(u) >= replies[key][1]:
            replies[key] = (t, context(u), u.get("cache_read_input_tokens") or 0)
    seq = sorted(replies.values())
    if len(seq) < 3:
        continue
    sessions += 1
    big += max(s[1] for s in seq) >= BIG
    found = False
    for before, after in zip(seq, seq[1:]):
        if after[0] - before[0] >= PAUSE and before[1] >= BIG:
            found = True
            returns.append(((after[0] - before[0]) / 60, after[2] < before[1] * 0.5, cli))
    with_pause += found
    cli_sessions += found and cli

print(f"sessions with 3 or more replies: {sessions}")
print(f"  went past 200,000 tokens: {big}")
print(f"  with a pause of 54 min or more above 200,000: {with_pause}")
print(f"returns after that pause: {len(returns)}")
print(f"  between 54 min and 3 h: {sum(54 <= m < 180 for m, *_ in returns)}")
print(f"  between 3 h and 12 h: {sum(180 <= m < 720 for m, *_ in returns)}")
print(f"  over 12 h: {sum(m >= 720 for m, *_ in returns)}")
print(f"  with the cache almost fully rebuilt: {sum(v[1] for v in returns)}")
print(f"  in the terminal (cli): {cli_sessions} sessions, {sum(v[2] for v in returns)} returns")
print(f"  in the desktop app: {with_pause - cli_sessions} sessions, {sum(not v[2] for v in returns)} returns")

2. Hooks that block when they fail

A hook is a command that runs on its own at a fixed moment. Until now, if that command didn't start, timed out or exited with an odd error, Claude Code carried on as if nothing happened. For a hook that notifies you, that is fine. For one that protects you, it is a hole.

Since 2.1.295, from October 8, a command or HTTP hook can carry "onFailure": "block": if it fails, the action stops instead of going through.

Sources: official Claude Code changelog, 2.1.295; Classmethod, Oct 9, 2026

.claude/settings.json (example)

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "python3 .claude/hooks/no-delete.py",
            "onFailure": "block" }
        ] }
    ]
  }
}
A guard that checks every command before it runs. If the script crashes, the command doesn't go through.

One detail: onFailureonly looks at whether the hook started, timed out or exited with a code it didn't expect. If your script fails on the inside and still ends with 0, the action goes through anyway. The guard has to fail properly.

To decide whether a hook gets block, I ask myself three questions:

  1. Does it run before the action? A guard that runs afterwards can no longer stop anything.
  2. If it fails and nobody notices, what happens? If the answer is that something gets deleted or uploaded that shouldn't, it is a guard.
  3. If it fails, do I prefer to stop or to keep going? If I prefer to keep going, I leave it without onFailure.

The first thing I did

Check what I have. This is what came out on my Mac:

~: zsh

claude(--version)

2.1.294 (Claude Code)

python3(count-hooks.py ~/.claude/settings.json (my script))

hooks 7 onFailure 0
Real output on my Mac, Oct 16, 2026. I counted the hooks with a Python script over ~/.claude/settings.json.

The claude in my terminal was on 2.1.294: it had neither change. First I have to update.

Then, the 7 hooks. This is what each one does:

HookWhen it runsWhat it doesGuard?
al-grano-activateWhen a session opensLoads my rules so replies stay shortNo
al-grano-trackerOn each message of mineReminds me of those rulesNo
session-size-hookOn each message of mineWarns when the conversation passes 150 requestsNo, it warns
session_bridgeWhen each reply finishesSaves session notes into my vaultNo
pane-status-hook (3)On finish, on notification and on a permission requestMarks the session's status on my panelNo
The 7 hooks in my ~/.claude/settings.json, checked on Oct 19, 2026. My projects have hooks of their own that this count does not include.

None of those 7 is a guard. If one breaks, I would rather keep working, so they stay without onFailure. The day I add one that protects a folder or stops a delete, that one gets block.

The one closest to this post is session-size-hook. At 150 requests it tells me to close and open another session, because every request re-reads the whole conversation. It has been my rule since October 9 and it is a warning, not a block: I would rather decide myself.

There is one question for each hook: if this one fails, do I want the work to keep going or to stop?

What I still need

  1. Update the claude in my terminal, which is still on 2.1.294.
  2. Find out whether the desktop app applies idle compaction. That is where 460 of the 470 returns happen.
  3. Repeat the count with the script, on the new version, and compare.

Check yours

Paste this into Claude Code. It only reads, it changes nothing:

Tell me which Claude Code version I have, whether my settings set idleCompaction, and which of my hooks have no onFailure. For each hook, tell me whether it is a guard (it should block if it fails) or a logger (it can fail without stopping the work). Don't change anything.

In the answer, look at four things:

  1. The version. Before 2.1.295 there is no onFailure, and before 2.1.299 there is no idle compaction.
  2. idleCompaction. If it isn't in your settings, you didn't configure it: the feature behaves however your version defaults.
  3. The hooks without onFailure. For a logger, that is normal.
  4. The ones Claude flags as guards that have no block. That is the first one you fix.