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
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:
- One or more MCP servers that set an
instructionsfield (e.g.claude-peers, and many community MCP servers). Each connection re-emits the instructions into the system prompt. - Multiple
SessionStarthooks configured in~/.claude/settings.jsonwhoseadditionalContextoutputs are non-trivial (skill catalog, model profile, watcher session banner, etc.). - Claude Code's internal session-resume behavior in v2.1.x, which silently re-enters the same session in the background and re-fires
SessionStartwithout the user typingclaude --resumeor 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_tokensdrops to a 16,520 floor whilecache_creation_input_tokensspikes ~22,000 each time - 0
PreCompact/PostCompactevents — 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 sessionre-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
additionalContextfrom 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
- 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.
- 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.
- 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.
- 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
- Disclose the session-resume behavior in docs, and surface in the UI when a session is silently resumed vs. freshly started.
- De-duplicate MCP
instructionsandSessionStarthookadditionalContextacross resumes within the same logical session (they should be cached as part of the stable prefix, not re-created each resume). - Add a counter /
/costbreakdown that shows how many tokens came from hookadditionalContextvs. user text vs. tool output, so heavy users can audit what they are being billed for. - Fire
SessionStartonce 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(providesinstructions) - Hooks: 5
SessionStart, 4UserPromptSubmit, 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>.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗