- Book: LLM Observability Pocket Guide
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
You start three background agents on a refactor and go make coffee. While each one works, about every 30 seconds, Claude Code sends that agent's transcript back to the agent's own model with one request: "describe your most recent action in 3-5 words." The answer becomes the little label next to the agent in the agents panel.
That is the finding in a gist from the flamerix-ai account that reached 584 points and 236 comments on Hacker News at the time of writing. In the author's OpenTelemetry data for one week, the agent_summary calls were 16.4% of estimated spend and 19.7% of API requests. The main conversation was 15.7%. A Claude Code engineer replied in the thread that the basic mechanism is right, and that the typical share is far lower. Both can be true. Which one is your bill depends on how you work.
How the agent_summary timer works
Per the gist, the feature has been on by default since Claude Code 2.1.232, in August 2026. Each actively working background agent gets a timer. When it fires, Claude Code forks a request that carries the agent's transcript to the agent's own model, usually Opus, and asks for the 3-5 word description. The gist puts the input-to-output ratio at about 9,000 tokens in for every token out.
Three details matter:
- The interval comes from the server. The gist says it is a per-model-family server flag, clamped to 30-240 seconds, and the default is already the minimum. No value you can set turns it off.
- It uses the agent's model. A GitHub issue on 2.1.312 measured a Haiku subagent and saw the calls go to Haiku, so an Opus subagent gets summarized by Opus.
-
It skips the transcript. The original feature request found the calls run with
skipTranscript: true, so cost tools that read session transcripts never see them.
The Claude Code reply on Hacker News narrows the scope: it "only applies to background agents that are actively working." Idle agents don't trigger it. Workflow agents don't get summary timers either, according to a commenter on #86879.
This is not the status line you configure yourself. The status line docs say that one "runs locally and does not consume API tokens." The agent_summary label lives in the agents panel, and it is a model call.

The trade in one picture: the whole transcript in, a few words out.
There is no supported off switch. CLAUDE_CODE_AGENT_PROGRESS_SUMMARIES and the SDK's agentProgressSummaries can only turn summaries on, per the gist. CLAUDE_CODE_FORK_SUBAGENT=0 has mixed reports on #86879 and #105981: some people saw zero calls, others still saw summary calls. One commenter suggested the difference comes from a settings env block overriding the shell export. It also disables fork mode, so setting it loses you more than the summaries.
What the Bun preload does, and why it breaks
The gist ships an MIT-licensed no-agent-summary.js. It relies on Claude Code's native build running on Bun, and on Bun's BUN_OPTIONS variable, which "prepends command-line arguments to any Bun execution." So BUN_OPTIONS=--preload=<path> loads a script before Claude Code's own code runs. The gist says to use the = form, because a space breaks bun subcommands, and to run claude daemon stop --any so background processes pick up the new environment.
The script wraps the global setTimeout. When a callback's source text contains one of two anchors, [AgentSummary] Timer fired or agent_summary, it returns an inert timer instead of scheduling the real one. It also checks that it is running inside Claude Code before doing anything. The technique, in its smallest form, looks like this:
// Illustration of the technique. Not the gist's script.
const realSetTimeout = globalThis.setTimeout;
const ANCHOR = "[AgentSummary] Timer fired";
globalThis.setTimeout = (fn, ms, ...args) => {
const src = typeof fn === "function" ? String(fn) : "";
if (src.includes(ANCHOR)) {
const t = realSetTimeout(() => {}, 2 ** 31 - 1);
t.unref?.();
return t;
}
return realSetTimeout(fn, ms, ...args);
};
Don't run that sketch. It has none of the gist's gating or verification, and it shows exactly why this approach is fragile. It matches source text inside a minified binary. Rename one log line, or move the timer off the global setTimeout, and summaries come back with no error. The gist says this itself and asks you to verify after every update. A commenter on #86879 also warned against installing a preload by curl-ing the latest version, since whatever it downloads then runs inside every Claude Code process.
Two more caveats from the gist's own pages. Its README says the summary never reaches the model or your conversation, but a commenter on Claude Code 2.1.314 found accepted summary text stored in task.progress.summary in later context, and called that claim too strong. And the license section says plainly: "Changing a client's runtime behaviour may conflict with the terms of the service you use it with; checking that is on you." I haven't run the hook myself.
Measure your own agent_summary share
You don't need the hook to find out whether this matters to you. Claude Code's monitoring docs log a claude_code.api_request event for each request, with cost_usd and a query_source attribute, the "subsystem that issued the request." The gist grouped those events by query_source, where summaries show up as agent_summary.
Use the events, not the metrics. On the claude_code.cost.usage metric, query_source collapses to main, subagent or auxiliary, which hides the summaries inside a bucket. Turn on telemetry and the logs exporter:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# your collector's OTLP gRPC address, usually port 4317
export OTEL_EXPORTER_OTLP_ENDPOINT="$COLLECTOR_URL"
If you don't have a backend, an OpenTelemetry Collector with the file exporter writes newline-delimited OTLP JSON:
receivers:
otlp:
protocols:
grpc:
exporters:
file:
path: ./claude-logs.jsonl
service:
pipelines:
logs:
receivers: [otlp]
exporters: [file]
Work normally for a few days, then group by source with jq and awk:
jq -r '.resourceLogs[].scopeLogs[].logRecords[]
| [.attributes[] | {(.key): (.value | to_entries[0].value)}]
| add
| select(.["event.name"] == "api_request")
| [(.query_source // "unknown"), (.cost_usd // 0 | tonumber)]
| @tsv' claude-logs.jsonl \
| awk -F'\t' '
{ cost[$1] += $2; reqs[$1]++; tc += $2; tr++ }
END { for (k in cost)
printf "%-22s %5.1f%% cost %5.1f%% reqs\n",
k, 100 * cost[k] / tc, 100 * reqs[k] / tr }'
Each output row is one query_source with its share of estimated cost and of requests. Two things to keep in mind when you read it. cost_usd is Claude Code's own estimate at list price, not an invoice, as the cost docs say. And the share depends on how much of your day runs as background agents, which is the whole argument.

Group the request events by source and the summaries get a bin of their own.
Why the numbers disagree
Here is every figure on the table, each from a different method and workload:
| Source | Share | What it measured |
|---|---|---|
| The gist | 16.4% of spend, 19.7% of requests | The author's setup, one week, OpenTelemetry estimates |
| Issue #86879 | 22.7% of requests, 18.0% of weighted tokens | One 4-hour session, 27 subagents, 2.1.233 |
| Commenters on #86879 | From about 10% of spend over 30 days to 26% of cache reads in one session | Separate workloads and methods |
| Claude Code engineer on HN | About 2% of cost, just under 4% of requests | Telemetry users, last week |
| Same reply, terminal only | About 6% of cost | Where nearly all these calls happen |
The high numbers come from people running mostly background agents. The engineer said as much: 16.4% "sounds like it was for a team running mostly background agents, and doesn't seem like a typical session." The low numbers are an average over everyone who sends telemetry, most of whom run few background agents. Neither one is your number. A replay of the 30-second timer over one person's transcripts, posted on the gist, landed at about 8%, partly because it assumed a warm cache.
The cache is the part that moves the bill. The engineer said a fix for "summaries that miss the prompt cache" shipped in 2.1.311 and later, calling misses the most expensive case. The team is also working on "a much cheaper mode that sends short notes to a smaller model instead of the full transcript." The reply adds: "we agree that's a lot for a status line." So if you measured before 2.1.311, measure again on a current build before you decide anything.
A status that doesn't need a model call
I build Hermes IDE, so weigh this section with that in mind. First, what it doesn't do: by default, Hermes runs Claude Code in a real terminal, unchanged, so background agents that Claude Code starts there get summarized exactly as they would anywhere else.
What Hermes does differently is its own per-session status. Every session shows one state: working, needs approval, asked you, done, error or idle, and Hermes says when a state is a guess. The README says Hermes "observes what agents report on purpose and what git shows," and the architecture guide describes that status as a state machine driven by the terminal output. Neither document describes a model call in that path. The cost: a state tells you less than a sentence about what the agent is doing right now.

Two real agent sessions side by side: Codex on orders-types after adding a RefundStatus type, and Claude Code on orders-api after a follow-up turn adding a log line, each in its own hermes/ worktree, with the sessions sidebar (status, context use, cost, branch) on the left.
Each session also shows its spend: the cost the agent reports, or an estimate from its transcript, clearly marked as estimated, with token totals. That tells you which session the money went to. It won't break out agent_summary, and since those calls skip the transcript, a transcript estimate can't see them. For that share, the OpenTelemetry query above is the tool.
You can download Hermes. It is source-available under BSL 1.1 and runs on macOS, Windows and Linux. The current release is 2.3.4. It is not affiliated with Nous Research or its Hermes Agent.
What to do with this today
- Run the query above for a few working days on a current Claude Code build.
- If
agent_summaryis a small slice, leave it alone. The label is useful. - If it is large, look at why first: how many background agents run at once, and on which model. A subagent on Haiku gets summarized by Haiku.
- Add your numbers and version to #86879. A supported opt-out is what that issue asks for, and real measurements are what moves it.
- If you reach for the preload anyway, read it first, check your terms, and verify it after every update.
If this was useful
The fix for this kind of surprise is the same every time: per-request telemetry, grouped by who issued the request, before you trust anyone's percentage, including the ones in this post. The LLM Observability Pocket Guide is the short read on picking the tracing and eval tools that give you that view, so a hidden 3-5 word call shows up as its own line instead of a mystery in the monthly total.

Top comments (0)