DEV Community

Cover image for Claude Code Sends a Background Agent's Whole Transcript Every 30s for a 3-5 Word Status
Gabriel Anhaia
Gabriel Anhaia

Posted on Edited on AI-assisted

Claude Code Sends a Background Agent's Whole Transcript Every 30s for a 3-5 Word Status


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.

A stopwatch whose hand pulls a long paper scroll into a large machine, which prints a single tiny tag at the other end
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);
};
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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 }'
Enter fullscreen mode Exit fullscreen mode

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.

Paper receipts dropping from a conveyor into labelled bins, with one bin filling faster than expected
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 in Hermes IDE, Codex and Claude Code, each in its own worktree, with the sessions sidebar showing status, context use, cost and branch
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

  1. Run the query above for a few working days on a current Claude Code build.
  2. If agent_summary is a small slice, leave it alone. The label is useful.
  3. 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.
  4. Add your numbers and version to #86879. A supported opt-out is what that issue asks for, and real measurements are what moves it.
  5. 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.

LLM Observability Pocket Guide: Picking the Right Tracing & Evals Tools for Your Team

Top comments (0)