claude-code - ✅(Solved) Fix Users are billed for ~24M hidden input tokens/month per heavy seat — silent session resumes re-fire SessionStart hooks + re-send MCP instructions, with no UI disclosure or consent [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#50799Fetched 2026-04-20 12:12:43
View on GitHub
Comments
1
Participants
2
Timeline
9
Reactions
0
Author
Participants
Timeline (top)
labeled ×5cross-referenced ×3commented ×1

On Claude Code v2.1.114, sessions silently "resume" without user action, re-firing every SessionStart hook and re-attaching every MCP server's instructions field. This repeatedly invalidates the prompt cache and re-creates thousands of tokens' worth of system-prompt prefix every few turns, with no UI indication and no user consent. Heavy users are billed for millions of hidden input tokens per month.

This is a billing transparency violation: users pay for tokens they did not knowingly cause and cannot observe in the UI.

Root Cause

  1. Billing transparency violation — users are billed for context they never typed, never saw in the UI, and cannot disable without removing features (MCP, hooks) that the product actively encourages.
  2. Prompt-cache model is broken by design here — caching is sold as a cost saver, but silent re-injection repeatedly invalidates exactly the prefix that should stay cached.
  3. Auto-compact fires earlier than the user expects — because the input prefix balloons silently, users hit the compact threshold mid-task for reasons unrelated to their own work.
  4. No disclosure — there is no CLI flag, settings entry, or docs page that says "resuming a session re-runs all your SessionStart hooks and re-sends MCP instructions."

Fix Action

Fix / Workaround

Mitigation I had to apply locally

I patched ~/.claude/hooks/edit-counter.js so its PostToolUse reminder writes to a log file instead of calling contextOutput(). That alone eliminated ~17.5 KB / session of injected text. But this is a workaround for a single hook — the structural issue (resume re-firing everything) is still there.

PR fix notes

PR #203: test(hooks): token-cost caps on every hook (#203)

Description (problem / solution / changelog)

Closes ROADMAP #203.

Summary

CC issue #50799 documents hidden SessionStart hook billing — hooks that emit unbounded output silently eat user tokens. Sentiment research 2026-04-19 flagged this surface area for the SDLC Wizard (5 hooks fire per prompt/session in every consumer repo).

Only sdlc-prompt-check.sh had a size test (baseline <1000 chars). The other 4 hooks were unbounded.

Changes

4 new tests in tests/test-hooks.sh, each with observed-headroom caps:

HookCapObserved
tdd-pretool-check<500219
model-effort-check<500211
instructions-loaded-check (worst-case: stale ≥3 minor + dual-install + all sub-checks)<3000~557
sdlc-prompt-check (worst-case: bump block firing + baseline)<1500~1220

The sdlc-prompt-check worst-case test is separate from the existing <1000 baseline test so the baseline stays tight while the bump-firing path has its own explicit cap.

Test plan

  • 96/96 pass: bash tests/test-hooks.sh
  • Negative control: injecting 2KB of echo bloat at the top of each hook fails 3-4 tests (target + spillover into other hook tests that also see the bloated output). Restoring each hook returns to 96/96.
  • No regressions: test-cli 70/70, test-doc-consistency 22/22

Changed files

  • .reviews/handoff.json (modified, +18/-19)
  • .reviews/response.json (modified, +7/-7)
  • ROADMAP.md (modified, +5/-0)
  • tests/e2e/score-history.jsonl (modified, +3/-0)
  • tests/test-hooks.sh (modified, +111/-1)
RAW_BUFFERClick to expand / collapse

Summary

On Claude Code v2.1.114, sessions silently "resume" without user action, re-firing every SessionStart hook and re-attaching every MCP server's instructions field. This repeatedly invalidates the prompt cache and re-creates thousands of tokens' worth of system-prompt prefix every few turns, with no UI indication and no user consent. Heavy users are billed for millions of hidden input tokens per month.

This is a billing transparency violation: users pay for tokens they did not knowingly cause and cannot observe in the UI.

Reproducibility — specific combination required

This does not manifest on every user's install. It shows up strongly when all three of the following are present simultaneously:

  1. One or more MCP servers that set an instructions field (e.g. claude-peers, and many community MCP servers). Each connection re-emits the instructions into the system prompt.
  2. Multiple SessionStart hooks configured in ~/.claude/settings.json whose additionalContext outputs are non-trivial (skill catalog, model profile, watcher session banner, etc.).
  3. Claude Code's internal session-resume behavior in v2.1.x, which silently re-enters the same session in the background and re-fires SessionStart without the user typing claude --resume or restarting.

Users with a vanilla install (no MCP instructions, no hook additionalContext) will not see large cache churn, which is likely why this has gone unreported. The issue is the interaction between a legitimate extensibility surface (hooks/MCP) and an undocumented resume behavior.

Observed evidence (this machine, one ~5.7h session)

Transcript: ~/.claude/projects/<cwd>/fd839c9f-34e7-42ce-aa65-121fbbdc1814.jsonl

  • 49 SessionStart re-fires in a single session (expected: 1)
  • 26 cache resets: cache_read_input_tokens drops to a 16,520 floor while cache_creation_input_tokens spikes ~22,000 each time
  • 0 PreCompact / PostCompact events — this is NOT auto-compaction, this is silent re-caching
  • Per-turn banners visibly repeat in the terminal (screenshots attached): Listening for channel messages from: server:claude-peers / Experimental — inbound messages will be pushed into this session re-prints between turns even though the user never restarted

Heavy-user billing impact

Conservative per-session assembly (measured on my install):

  • SessionStart hook outputs: ~4–8 KB
  • MCP instructions (claude-peers alone): ~2.1 KB
  • Hook-driven additionalContext from UserPromptSubmit: ~1–3 KB / turn
  • Per-resume cache-creation: ~22,000 tokens observed

Heavy user estimate (8h/day, ~50 resume-cycles/day, 22 working days/month):

  • 22,000 tokens × 50 × 22 = ~24.2M cache-creation tokens / month / seat
  • At Opus 4.7 input cache-creation rate: 24.2M × $6.25/MTok ≈ $151/month/seat of hidden billable input that the user neither authored nor saw.

Even halving every assumption (~12M tokens/month) this is ~$75 of invisible input per seat per month — enough to matter for any team running Claude Code daily.

Why this matters beyond dollars

  1. Billing transparency violation — users are billed for context they never typed, never saw in the UI, and cannot disable without removing features (MCP, hooks) that the product actively encourages.
  2. Prompt-cache model is broken by design here — caching is sold as a cost saver, but silent re-injection repeatedly invalidates exactly the prefix that should stay cached.
  3. Auto-compact fires earlier than the user expects — because the input prefix balloons silently, users hit the compact threshold mid-task for reasons unrelated to their own work.
  4. No disclosure — there is no CLI flag, settings entry, or docs page that says "resuming a session re-runs all your SessionStart hooks and re-sends MCP instructions."

Ask

  1. Disclose the session-resume behavior in docs, and surface in the UI when a session is silently resumed vs. freshly started.
  2. De-duplicate MCP instructions and SessionStart hook additionalContext across resumes within the same logical session (they should be cached as part of the stable prefix, not re-created each resume).
  3. Add a counter / /cost breakdown that shows how many tokens came from hook additionalContext vs. user text vs. tool output, so heavy users can audit what they are being billed for.
  4. Fire SessionStart once per session ID, not once per silent resume.

Mitigation I had to apply locally

I patched ~/.claude/hooks/edit-counter.js so its PostToolUse reminder writes to a log file instead of calling contextOutput(). That alone eliminated ~17.5 KB / session of injected text. But this is a workaround for a single hook — the structural issue (resume re-firing everything) is still there.

Environment

  • Claude Code: v2.1.114
  • Model: Opus 4.7 (xhigh effort)
  • Plan: Claude Max
  • OS: macOS 25.4.0 (Darwin arm64)
  • MCP servers active: claude-peers (provides instructions)
  • Hooks: 5 SessionStart, 4 UserPromptSubmit, 10+ PreToolUse:Bash (standard productivity hook set)

Attachments

Two redacted terminal screenshots are attached showing the repeated Listening for channel messages / Experimental — inbound messages will be pushed banners re-printing between turns within a single uninterrupted session. Personal paths have been masked as ~/PROJECT / <redacted-path>.

extent analysis

TL;DR

The issue can be mitigated by modifying the session-resume behavior to de-duplicate MCP instructions and SessionStart hook additionalContext across resumes within the same logical session.

Guidance

  • Identify and modify the SessionStart hooks to cache their additionalContext outputs instead of re-creating them on each resume.
  • De-duplicate MCP instructions across resumes to prevent re-injection into the system prompt.
  • Consider adding a counter or /cost breakdown to track token usage from different sources (hook additionalContext, user text, tool output).
  • Review the hooks/edit-counter.js patch applied locally and consider applying similar modifications to other hooks to reduce injected text.

Example

// Example of caching additionalContext output in a SessionStart hook
const cache = {};

module.exports = {
  // ...
  additionalContext: (context) => {
    if (cache[context.sessionId]) {
      return cache[context.sessionId];
    }
    const output = // calculate additionalContext output
    cache[context.sessionId] = output;
    return output;
  },
};

Notes

The provided patch for hooks/edit-counter.js is a workaround for a single hook, and the structural issue of resume re-firing everything is still present. A more comprehensive solution would require modifying the session-resume behavior and caching mechanism.

Recommendation

Apply a workaround by modifying the SessionStart hooks and MCP instructions to cache their outputs and prevent re-injection, as this will help mitigate the billing transparency violation and reduce hidden input tokens.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - ✅(Solved) Fix Users are billed for ~24M hidden input tokens/month per heavy seat — silent session resumes re-fire SessionStart hooks + re-send MCP instructions, with no UI disclosure or consent [1 pull requests, 1 comments, 2 participants]