# Idle compaction trades your context for a cache write

> "Compacted while idle, before the prompt cache expired" means Claude Code summarized a 200k+ session you walked away from. Set "idleCompaction": false (v2.1.290+) or take control with the API's compaction parameter.

URL: https://bartlomiejkrupa.dev/articles/claude-code-idle-compaction/
Published: 2026-10-08
Tags: claude-code, compaction, context-window, prompt-caching, hooks, api, cost
Summary: "Compacted while idle, before the prompt cache expired" means Claude Code summarized a 200k+ session you walked away from. Set "idleCompaction": false (v2.1.290+) or take control with the API's compaction parameter.

Since v2.1.286 (released 2026-09-30), Claude Code compacts idle sessions of 200k+ tokens just before their prompt cache (the server-side copy of your context that makes repeat turns cheap) expires. That means your working context gets swapped for a summary, with no warning beyond the line "Compacted while idle, before the prompt cache expired". You get a cheaper resume, but the session keeps only a summary: your exact instructions and earlier wording are gone. As of v2.1.290 you can opt out with one setting. If you build on the Claude API, the new on-demand `compaction` parameter offers the same trade with your code deciding when it happens and what survives.

## What changed, version by version

| Version | Released | Change |
| --- | --- | --- |
| 2.1.286 | 2026-09-30 | Idle sessions compact before the cache expires. No setting, no warning. The transcript mislabels it `compactMetadata.trigger: "manual"`, as if you had run `/compact` |
| 2.1.290 | 2026-10-05 | New `idleCompaction` setting; the trigger is now labeled `auto` |
| 2.1.291 | 2026-10-06 | Reporter confirms `"idleCompaction": false` fixes it; issue closed |

None of this is in the Claude Code changelog. The details come from [issue #98747](https://github.com/anthropics/claude-code/issues/98747) and an Anthropic maintainer's replies in it. The reporter runs about 20 long-lived sessions in parallel. Overnight, 7 of them were compacted, none of them near the context limit. Their exact instructions, the reasons behind decisions, and the methods agreed on were gone from what the model sees. The maintainer said the feature fires only on contexts of 200k+ tokens.

## Why it exists: a cold resume costs 25-40x a warm one

A **prompt cache** stores a request prefix so later requests can read it cheaply. An entry lives 5 minutes or 1 hour (one commenter on Max says Claude Code uses the 1-hour lifetime, or TTL). After it expires, the next turn has to write the whole context to the cache again.

Here is one turn's cost to re-send a 200k-token context, computed from Anthropic's list prices (MTok = million tokens):

| Model | Warm (cache read) | Cold, 5-min write (1.25x) | Cold, 1-hour write (2x) |
| --- | --- | --- | --- |
| Opus 5.5 ($4/MTok input) | $0.04 | $1.00 | $1.60 |
| Sonnet 5.5 ($2/MTok input) | $0.02 | $0.50 | $0.80 |

Cache reads on both models cost 0.05x the input price, so a cold turn costs 25x a warm one (5-minute write) or 40x (1-hour write). Compact before expiry and the next turn re-writes a short summary instead of 200k tokens. The summarization call still has to read the old context once, so the saving is the avoided cold write, not the whole turn. These are API list prices; on a subscription you don't see them directly. Nobody in the issue disputes the cost logic. The complaint is the default: "the context is the valuable part, and a cache re-read is a cost I'd gladly pay to keep it."

For how context fills up in the first place, see [How Claude Code's agentic loop fills your context window](/articles/claude-code-agentic-loop-context). For the cache rules behind the table, see [Don't break the prompt cache](/articles/dont-break-the-prompt-cache).

## Fix: turn off idle compaction, keep threshold compaction

**Symptom:** you come back to a session and see "Compacted while idle, before the prompt cache expired", and the session no longer follows instructions it had followed before.

**Cause:** the idle timer compacted a context of 200k+ tokens before its cache expired.

**Solution:** update to v2.1.290 or later and add the key to any settings.json: user (`~/.claude/settings.json`), project, local, or managed.

```json
{
  "idleCompaction": false
}
```

Threshold auto-compaction (the kind that fires when the context nears its limit) keeps working. Before v2.1.290 the only off switch was disabling auto-compaction entirely. Several people in the issue did that, which means hitting the context wall mid-task.

Two more details come from a commenter who read the 2.1.290 binary; the docs don't list them. Setting `"idleCompaction": true` adds nothing, because a server-side gate still decides whether the feature runs. Turning auto-compaction off still turns idle compaction off too.

## Keep it on, but steer what the summary keeps

If the cheaper resume is worth it to you, a `PreCompact` hook (a command Claude Code runs just before it compacts) can shape the summary. A commenter tested on 2.1.287 that idle compaction fires `PreCompact` with `custom_instructions: null`. Since 2.1.290 the trigger is `auto`, so a hook with the `auto` matcher catches both idle and threshold compaction:

```json
{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "auto",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Preserve verbatim: every standing instruction the user gave, each decision with its reason, and the task in progress.'"
          }
        ]
      }
    ]
  }
}
```

Whatever the hook prints to stdout is appended to the summarizer's instructions. The binary's own hook description says so; the hooks reference page does not. Verify it the way the commenter did: print a canary token ("Begin the summary with PINEAPPLE-7731"), run `/compact`, and check the summary for it.

Three limits:

- **Instructions are guidance, not retention.** Nothing survives compaction word for word. The commenter found that after compaction Claude Code re-reads at most five recently touched files.
- **No subagent filter.** The `PreCompact` input has `session_id`, `transcript_path`, `cwd`, `trigger`, and `custom_instructions`, but no `agent_id`. Your instructions also reach subagents that auto-compact.
- **The transcript stays on disk.** The pre-compaction history is still in the session's JSONL file, which is how the reporter rebuilt their sessions by hand.

## Same trade-off, your code decides: API on-demand compaction

The Claude API added on-demand compaction on 2026-09-14, in beta behind the `compact-2026-09-04` header. You send the conversation with `"compaction": {"type": "summarize"}`. The API returns a single signed `compaction` block with `stop_reason: "compaction"` and no reply. From then on, that block goes first in `messages`, in place of the turns it summarized.

```python
import anthropic

client = anthropic.Anthropic()
SYSTEM = "You are a coding assistant."  # your conversation's system prompt

summary = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=4096,  # caps thinking + summary, so leave room
    system=SYSTEM,    # same system prompt as the conversation (pass tools too if you use them)
    betas=["compact-2026-09-04"],
    messages=history,
    compaction={
        "type": "summarize",
        "instructions": "Preserve every standing instruction, decision and reason, "
        "and the open task. Do not call tools; reply with the summary only.",
    },
)
if summary.stop_reason == "compaction":
    history = [{"role": "assistant", "content": summary.content}]
```

Here, unlike idle compaction, you choose:

- **When:** you decide. Add `input_tokens`, `cache_read_input_tokens`, and `cache_creation_input_tokens` from the last response and compact below your budget.
- **What it keeps:** `instructions` (up to 16,384 characters) replaces the default summarization prompt entirely.
- **Recent turns, word for word:** send only the older turns to the compaction request, then put the block in front of the turns you kept. Cut where no tool call is left without its result.
- **No pause:** the request can run in the background while the conversation continues, and you swap the block in when it arrives.
- **Cost:** the summarization call is billed like any request and reported under `usage.iterations`. Resending the block later costs nothing extra.

The older threshold mode (`compact-2026-01-12`, edit type `compact_20260112`) lets the API compact automatically once input reaches a trigger, 150,000 tokens by default and at least 50,000. To keep recent turns, set `pause_after_compaction: true`: the API stops with `stop_reason: "compaction"`, you append the block, re-insert the turns, and resend. On the current models (Fable 5.1, Mythos 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5), threshold mode doesn't carry the model's reasoning (thinking blocks) forward, so strip `thinking` and `redacted_thinking` from turns you re-insert or the request returns a 400. On-demand compaction keeps that reasoning valid if the compaction request uses the conversation's exact `system` and `tools` and the cut falls between a reply and the next user message.

### Mistakes the API won't catch, and the ones it will

| What you did | Result |
| --- | --- |
| Left summarized messages after the block | No error; Claude reads them twice |
| Dropped the block on a later request | No error; Claude gets no summary |
| Left summarized messages in front of the block | 400 `compaction_block_misplaced` |
| Edited the block's `content` or `signature` | 400 `compaction_signature_invalid` or `compaction_content_mismatch` |
| Omitted the beta header on a later request | 400 saying `compaction` isn't an expected content block type |
| Sent `compaction` and `context_management` together | 400; use one kind of compaction per request |

Images, documents, and fetched URLs inside the summarized range are gone once the block replaces them. Re-upload anything a later turn still needs.

## Which compaction fits

| | Claude Code idle | API on demand | API threshold |
| --- | --- | --- | --- |
| Who decides when | Idle timer, 200k+ tokens | Your code | API, at an input-token trigger |
| Steer the summary | `PreCompact` stdout (undocumented) | `instructions` | `instructions` |
| Recent turns word for word | No | Yes | Yes, with `pause_after_compaction` |
| Kept thinking on 5.5 models | n/a | Yes, if `system`/`tools` match | No, strip it |
| Off switch | `"idleCompaction": false` | Don't send it | Don't set it |

Interactive sessions where you go away and come back: turn idle compaction off unless the cost of a cold resume actually hurts. Agents you build: compact on demand, keep the last few turns, and write your own `instructions`. For the client-side alternative, clearing old tool output instead of summarizing it, see [Prune the log, not the window](/articles/llm-context-management) and [observation masking](/notes/observation-masking).

Next step: add `"idleCompaction": false` to `~/.claude/settings.json` now, then decide per project whether a `PreCompact` hook earns its place.

Sources:
- 2.1.286 idle compaction silently discards working context (anthropics/claude-code#98747): https://github.com/anthropics/claude-code/issues/98747
- Compaction overview: https://platform.claude.com/docs/en/build-with-claude/compaction
- Compaction on demand: https://platform.claude.com/docs/en/build-with-claude/compaction-on-demand
- Compaction that keeps recent turns: https://platform.claude.com/docs/en/build-with-claude/compaction-keep-recent-turns
- Compaction at a token threshold: https://platform.claude.com/docs/en/build-with-claude/compaction-threshold
- Pricing: https://platform.claude.com/docs/en/about-claude/pricing
- Claude API release notes: https://platform.claude.com/docs/en/release-notes/api
