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%
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”.
- With the cache alive, Claude Code re-reads the 200,000 tokens. They weigh 0.1 each: 20,000 units.
- With the cache expired, it has to write them again. They weigh 1.25 each: 250,000 units.
- 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.
- Minute 0You step out for a call. The conversation already weighs more than 200,000 tokens.
- Minute 54With 2.1.299, Claude Code compacts it in the terminal.
- Minute 60The one-hour cache expires. Without compaction, the next message rebuilds everything.
- You returnWith compaction you continue from the summary. Without it, you pay for the full rebuild.
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:
| On | Off | |
|---|---|---|
| If you leave for more than 54 minutes with over 200,000 tokens | It summarizes itself and you continue from the summary when you return | Nothing happens: after the hour, the return message processes everything |
| What you lose | Details the summary leaves out | Nothing from the conversation, but you pay for the rebuild |
| Where I would use it | Long working sessions with pauses, like calls or lunch | A debugging session where every line of the error counts |
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 counted | Result |
|---|---|
| Sessions with 3 or more replies | 698 |
| Went past 200,000 tokens | 120 |
| With at least one pause of 54 min or more above 200,000 | 63 |
| Returns after that pause | 470 |
| Between 54 min and 3 h | 190 |
| Between 3 h and 12 h | 102 |
| Over 12 h | 178 |
| With the cache almost fully rebuilt | 452 of 470 |
| In the terminal (cli) | 9 sessions, 10 returns |
| In the desktop app | 54 sessions, 460 returns |
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" }
] }
]
}
}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:
- Does it run before the action? A guard that runs afterwards can no longer stop anything.
- 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.
- 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:
claude(--version)
2.1.294 (Claude Code)
python3(count-hooks.py ~/.claude/settings.json (my script))
hooks 7 onFailure 0
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:
| Hook | When it runs | What it does | Guard? |
|---|---|---|---|
| al-grano-activate | When a session opens | Loads my rules so replies stay short | No |
| al-grano-tracker | On each message of mine | Reminds me of those rules | No |
| session-size-hook | On each message of mine | Warns when the conversation passes 150 requests | No, it warns |
| session_bridge | When each reply finishes | Saves session notes into my vault | No |
| pane-status-hook (3) | On finish, on notification and on a permission request | Marks the session's status on my panel | No |
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
- Update the claude in my terminal, which is still on 2.1.294.
- Find out whether the desktop app applies idle compaction. That is where 460 of the 470 returns happen.
- 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:
- The version. Before 2.1.295 there is no onFailure, and before 2.1.299 there is no idle compaction.
- idleCompaction. If it isn't in your settings, you didn't configure it: the feature behaves however your version defaults.
- The hooks without onFailure. For a logger, that is normal.
- The ones Claude flags as guards that have no block. That is the first one you fix.


