Bug: cache_control TTL ordering error when hooks/MCP inject additionalContext into long conversations
Bug Description
Claude Code intermittently returns a 400 API error when hooks or MCP servers inject additionalContext into conversations, particularly in longer sessions (100+ messages):
API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
"message":"messages.126.content.2.cache_control.ttl: a ttl='1h' cache_control block
must not come after a ttl='5m' cache_control block. Note that blocks are processed
in the following order: `tools`, `system`, `messages`."}}
Steps to Reproduce
- Configure a project with hooks that inject
additionalContext(e.g., aPreToolUsehook that returns{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"some context"}}) - Also configure an MCP server for the same project
- Have a long conversation (100+ tool calls) with the agent
- The error occurs intermittently — more likely in longer conversations
Expected Behavior
Hooks and MCP servers should be able to inject additionalContext without triggering cache_control TTL ordering violations. The hooks themselves do not set any cache_control metadata — they return plain strings.
Actual Behavior
Claude Code's internal message construction appears to mix ttl='1h' and ttl='5m' cache_control blocks when building the API request. When hook-injected additionalContext shifts content block positions, a 1-hour TTL block can end up after a 5-minute TTL block, violating the API's ordering requirement.
Environment
- Claude Code version: 2.1.81
- OS: macOS Sequoia (Darwin 25.3.0, arm64)
- Hooks: PreToolUse (additionalContext injection), SessionStart, Stop, SessionEnd
- MCP: Streamable HTTP transport, single server
- The error appears across multiple projects using the same hook pattern
Analysis
The prompt caching documentation states: "Cache entries with longer TTL must appear before shorter TTLs."
The root cause appears to be in Claude Code's internal request construction, not in user hooks or MCP servers. The hooks only return plain additionalContext strings — they never set cache_control, ttl, or any caching metadata. The TTL blocks are added by Claude Code itself.
Related Issues
- #25597 — Invalid Ephemeral Cache Control Inputs (similar domain, different error)
- #30989 — defer_loading + cache_control conflict
- anthropics/claude-agent-sdk-typescript#188 — Agent SDK defaults to 1-hour cache TTL instead of 5-minute
Workaround
None currently. The error is recoverable by retrying the conversation, but it disrupts active sessions. Reducing hook frequency (e.g., throttling PreToolUse to every 10+ seconds) may reduce occurrence but does not eliminate it.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗