[BUG] defer_loading + cache_control 400 still present in 2.1.223 (recurrence of #30920)

Status Fixed / completed
Reported on v2.1.223
Maintainer reply None cached
Activity 2 comments · opened Aug 14, 2026 · closed Aug 17, 2026

Preflight Checklist

  • [x] I have searched existing issues — this is a recurrence of #30920, which was auto-closed as not planned after 4 weeks of inactivity and is now locked, so I cannot comment there. The closing bot explicitly asks to file a new issue referencing the old one.
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.223)

What's Wrong?

defer_loading=true and cache_control are still being set on the same tool definition, producing a 400 that makes the session unusable. This was first reported in 2.1.69 (March 2026) in #30920 and is still present in 2.1.223 (August 2026) — roughly five months and ~150 releases later.

API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
"message":"Tool 'mcp__github__search_issues' cannot have both defer_loading=true
and cache_control set. Tools with defer_loading cannot use prompt caching."},
"request_id":"req_011Ce34LWSb652PpH21ijLLi"}

Root cause in the shipped binary

What I think is new relative to #30920: the offending code is directly visible in the 2.1.223 binary. The tool-schema builder applies both flags through two independent conditionals, with nothing making them mutually exclusive:

let p = { name: d.name, description: d.description, input_schema: d.input_schema,
          ...d.strict && {strict:!0}, ...d.eager_input_streaming && {eager_input_streaming:!0} };
if (t.deferLoading) p.defer_loading = !0;
if (t.cacheControl) p.cache_control  = t.cacheControl;

Whenever a tool is both deferred and the one carrying the cache breakpoint, the illegal combination is emitted. cache_control marks the end of the cacheable tool prefix, so this lands on whichever tool sits last in the list — an MCP tool in practice, which is why it always surfaces as mcp__*.

Worth noting: the codebase does guard the adjacent case (All tools have defer_loading set is handled elsewhere), so the missing guard here looks like an oversight rather than a deliberate constraint.

Possibly related: inc-5316

The binary also contains a cache-diagnosis string referencing an internal incident:

defer_loading presence flipped (deferred-tool hint section, inc-5316)

That path is about cache invalidation detection when defer_loading appears/disappears, not about the field collision itself. If inc-5316 was treated as covering this report, that may explain why the collision was never fixed — flagging in case it helps route this correctly.

ENABLE_TOOL_SEARCH=false is not a reliable workaround

The documented workaround is incomplete. The env var parses correctly:

function ud(e){ ... return ["0","false","no","off"].includes(t) }   // → mode "standard"
function pQr(){ let e=process.env.ENABLE_TOOL_SEARCH; ... if(ud(e)) return "standard"; return "tst" }

but two things bite in practice:

  1. The mode is resolved from process.env at startup, so setting it mid-session does not rebuild already-constructed tool schemas — a full restart is required, which is not obvious from the error.
  2. Any unrecognized value (False, disabled, empty string) silently falls through to the default "tst" — tool search enabled. Only false / 0 / no / off work.

So ENABLE_TOOL_SEARCH=false claude (set before launch) works, while toggling it in a running session does not.

What Should Happen?

Omit cache_control on any tool that has defer_loading: true (or move the cache breakpoint to the last non-deferred tool). A guard next to the existing All tools have defer_loading set check would be enough.

Steps to Reproduce

  1. Configure several MCP servers (~50+ tools total) so tool deferral kicks in
  2. Start claude with tool search active (default on first-party endpoints)
  3. Issue any prompt that attaches MCP tools

Every request fails with the 400 above. Switching models does not help — and the client surfaces the failure as There's an issue with the selected model (...). It may not exist or you may not have access to it., which sends users toward model/permission debugging instead of the real cause. That misleading message may be worth fixing independently.

Environment

  • Claude Code 2.1.223 (Homebrew cask, native binary)
  • macOS (Darwin arm64)
  • First-party Anthropic endpoint (no proxy, no Bedrock/Vertex)
  • Multiple stdio + http MCP servers configured

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗