Background Summaries Reportedly Cost 16%. I Measured Mine and Got Zero#

2026-11-10

At a wooden desk under a lamp, a person holds an analog gauge in both hands with its needle resting at the start of the scale, while beyond it three small robots quietly go about their work, carrying a letter, reading a sheet of paper, and sorting cards

While putting together the October 31 AI news brief, one item made me stop. Claude Code was sending calls the user never asked for, just to show the status of background agents on screen, and at one company those calls made up 16% of estimated spend. I run subagents in the background quite often, so I wrote in the brief that I planned to measure it the same way. If tokens were leaking, I wanted to plug the leak.

Here is the conclusion up front. A simpler off switch than the one the original post described already existed, and four days of measuring showed that the request never happened once in my environment. It was a bit of a letdown, but I was also relieved to confirm that nothing was leaking. This essay records how I checked, and how I found out why the number was zero. The Claude Code version at the time of checking was 2.1.319.

The 16% in the brief#

When you run a subagent (a separate agent that the main conversation hands work to) in the background in Claude Code, the agent panel shows a short sentence about what each agent is doing. According to Flamerix AI’s analysis, that sentence is not free. For each background agent, roughly every 30 seconds, Claude Code sends the agent’s entire transcript to the model and asks it to “describe your most recent action in 3–5 words.” The model is that agent’s own model, which is usually Opus. This has been on by default in interactive sessions since version 2.1.232 in August.

Background agentStill workingFull transcriptRe-sent every 30 sModel callUsually Opus3–5 word statusShown in agent panelAgain 30 s later, until the agent finishes
Every status line means rereading the whole conversation, so the longer a background agent runs, the more the cost piles up.

The original author’s company grouped a week of requests by source and found that these summary requests accounted for 16.4% of estimated spend and 19.7% of requests. That was more than the main conversation (15.7%). There was pushback too. One commenter recalculated with cache discounts and got about 8%, and another pointed out that the share varies a lot with how you work. Either way, the point stood: calls the user never asked for still land on the bill or count against subscription limits.

What I weighed before turning it off: answer quality#

Before looking for an off switch, I first checked what I would lose by turning it off. The status line on screen doesn’t matter to me. What matters is whether the quality of the final answer drops.

There is one place where the status line goes back to the model. When a conversation gets long and the earlier part is replaced with a summary (compaction), any background agent that is still running has its latest status line attached to the main model as a single Progress: line. A commenter on the original post pointed this out, and the author agreed. Turning summaries off removes that line.

Even so, the main model still knows the agent is running, and it receives the agent’s full result when the agent finishes. The final answer is built from that full result; the 3–5 word status line plays no part in it. The only case where it could matter is a rare one: compaction happens while the agent is running, and the main model has to make a decision based on the agent’s progress instead of waiting for its result. I concluded that turning it off costs nothing.

The off switch had already changed#

The original post offered a preload script: about 70 lines that Claude Code loads at startup and that pick out the summary timer and make sure it never fires. The author noted that it might conflict with the terms of service. It also has other drawbacks: if an update changes the code it hooks into, it silently stops working, and if the script file breaks, Claude Code won’t start at all.

A simpler route turned up in the comments. Since 2.1.318, Claude Code has an environment variable, CLAUDE_CODE_DISABLE_AGENT_PROGRESS_SUMMARIES=1, that turns the timer off. The author replied that they had confirmed it in the binary. I searched the 2.1.319 binary installed on my machine myself, and the variable was in the list of environment variables Claude Code recognizes. It does not appear in the changelog or the official environment variable docs yet. It exists in the program, but there is no official guidance for it.

There was no longer any reason to adopt the workaround script. One part of the original post had not aged, though: the way it measured, by splitting requests by source. The way to turn it off was outdated, but the way to measure it still worked.

Requests that never reach the session logs#

In an earlier essay, I aggregated my local session logs to diagnose where my tokens went. I was about to measure the same way when a sentence in the original post caught my eye. According to the author, these summary requests are not written to the session logs on disk, so tools that count cost from log files can’t see them. My earlier method had a blind spot.

So, like the original author, I decided to use Claude Code’s records. This feature is off by default. Once enabled, Claude Code emits an event for every API request with the model, token counts, estimated cost, and the part of Claude Code that sent it (query_source). The main conversation, compaction, and each subagent show up under different values, and background summaries show up as agent_summary.

These events are sent to a receiver over . Normally they go to a monitoring service, but I didn’t want to depend on an outside service, so I ran a 20-line Python receiver on my own machine and only wrote to files. Prompts, response text, and executed commands are not recorded with the default settings.

I put the settings in my user settings file (~/.claude/settings.json). According to the official docs, OpenTelemetry export settings are ignored in a repository’s project settings. In exchange, putting them in user settings once means sessions in every repository on that machine are measured together. The exact settings are in the appendix.

Four days of measuring came back zero#

From November 7 to 10, I worked as usual. Moving between blog work and a work repository, I logged about 7,000 API requests across 17 sessions. There was not a single agent_summary. In case it showed up under a different name, I searched the raw records for every value containing “summary.” Nothing.

My heart sank a little. I had set up a measurement pipeline to find leaking tokens, and there was no leak. Still, I didn’t take it at face value. I first checked whether the measurement itself could be wrong.

  • Maybe there was never a situation that would trigger a summary? The session records in my work repository showed that on November 9 alone, I explicitly launched subagents in the background about 100 times across three sessions. There were plenty of chances.
  • Did turning on telemetry change the result? The 2.1.318 changelog has a line saying progress summary behavior changed “with telemetry off,” so I checked it. That change was about how long to wait for a response on a cold cache, and had nothing to do with whether summaries are on or off.
  • Was the off switch already set? I didn’t set it during the measurement period. I added it only after measuring.

The zero was not a measurement error.

Why zero: the editor extension doesn’t turn summaries on#

The clue was in the same records. The request source for the main conversation was not repl_main_thread, the example in the official docs, but sdk across the board. I use Claude Code not in a terminal but through the Cursor editor extension. The extension starts Claude Code in and draws the UI itself.

The original post’s code analysis made the same distinction. Summaries for regular background subagents are on by default only in the terminal’s interactive mode; in SDK mode, the program that launches Claude Code has to turn on the agentProgressSummaries option. What I found matched. The description of this SDK option inside Claude Code says progress summaries are generated “only when the agentProgressSummaries option is on,” and the code of the Cursor extension I have installed passes that option empty.

Run from terminalInteractive modeBackground subagentSummary timer onSummary requestsFull transcript every 30 sEditor extensionSDK modeBackground subagentSummary option emptyNo summary requestsZero in four days
Even with the same Claude Code, the editor extension launches it without the summary option, so running subagents in the background produces no summary requests.

There are exceptions. According to the original post, fork subagents, which inherit the whole conversation, and skills that run as forks are summarized even in SDK mode. I didn’t use either during the measurement period. If you launch Claude Code directly in a terminal and run background agents, the original 16% may apply to you as is. The extension could also start turning this option on in the future, so this result reflects 2.1.319 and the current extension code.

I turned the switch on anyway#

After measuring, I turned telemetry off and deleted the raw records. I left just one line in my user settings: the off switch. It does nothing for me right now, but it doesn’t cost anything either. It’s insurance for the day I run background agents from a terminal, use forks, or the extension’s default changes.

That said, since my environment was at zero to begin with, I couldn’t verify for myself that this variable actually turns the timer off. It’s also undocumented, so I’m keeping in mind that it could disappear or change in a future version.

Closing thoughts#

When I read “it’s spending 16%,” my first thought was how to cut it. What I actually needed was to check whether that 16% happened in the way I run Claude Code. The same Claude Code made these calls or didn’t, depending on whether I used it in a terminal or through an editor extension.

What four days of measuring left me with was not a lower bill but two confirmations. My tokens were not leaking through this feature. And the session-log-only method from last time can’t see requests like these. The next time I run into a number like this, I plan to look for a way to measure it before looking for a way to turn it off.

Appendix: Example settings#

Telemetry for measuring#

Put this in the env of your user settings file (~/.claude/settings.json). It applies to sessions opened after the change; remove it when you’re done measuring.

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:4318"
  }
}

Local receiver#

It just appends each request body as one line to a dated file. Keep it running for the measurement period.

# receiver.py
import json
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length") or 0))
        with open(datetime.now().strftime("otel-%Y%m%d.jsonl"), "a") as f:
            f.write(json.dumps(json.loads(body)) + "\n")
        self.send_response(200)
        self.end_headers()

    def log_message(self, *args):
        pass


ThreadingHTTPServer(("127.0.0.1", 4318), Handler).serve_forever()

Aggregating by source#

Groups api_request events by query_source and prints request counts and estimated cost shares.

import collections, glob, json

count, cost = collections.Counter(), collections.Counter()
for path in glob.glob("otel-*.jsonl"):
    for line in open(path):
        for rl in json.loads(line).get("resourceLogs", []):
            for sl in rl.get("scopeLogs", []):
                for rec in sl.get("logRecords", []):
                    a = {x["key"]: list(x["value"].values())[0] for x in rec.get("attributes", [])}
                    if a.get("event.name") == "api_request":
                        count[a.get("query_source")] += 1
                        cost[a.get("query_source")] += float(a.get("cost_usd") or 0)

total = sum(cost.values()) or 1
for source, n in count.most_common():
    print(f"{source:32} requests {n:6}  cost {100 * cost[source] / total:5.1f}%")

Turning off background summaries#

A variable recognized by 2.1.318 and later. It is not in the official docs yet.

{
  "env": {
    "CLAUDE_CODE_DISABLE_AGENT_PROGRESS_SUMMARIES": "1"
  }
}

References#