[BUG] defer_loading + cache_control 400 still present in 2.1.223 (recurrence of #30920)
Preflight Checklist
- [x] I have searched existing issues — this is a recurrence of #30920, which was auto-closed as
not plannedafter 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:
- The mode is resolved from
process.envat 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. - Any unrecognized value (
False,disabled, empty string) silently falls through to the default"tst"— tool search enabled. Onlyfalse/0/no/offwork.
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
- Configure several MCP servers (~50+ tools total) so tool deferral kicks in
- Start
claudewith tool search active (default on first-party endpoints) - 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
3 Comments
Update (2026-08-15) — regression bisected to 2.1.223; last known-good is 2.1.222
I traced my install history through the Homebrew cask cache and could pinpoint the exact version boundary on my machine (
arm64, first-party endpoint, ~10 MCP servers / ~286 tools, tool search active):| Version | Released | Installed | Result |
|---|---|---|---|
| 2.1.220 | 2026-07-25 | 2026-08-05 | OK |
| 2.1.221 | 2026-08-04 | 2026-08-12 | OK |
| 2.1.222 | 2026-08-04 | 2026-08-13 | OK — last known-good |
| 2.1.223 | 2026-08-06 | 2026-08-13 | ❌ 400 appears (same-day session at 22:52 already shows 3×
cannot have both defer_loadingerrors) || 2.1.224 | 2026-08-07 | 2026-08-15 | ❌ still present |
Downgrading to 2.1.222 eliminates the error completely in the same configuration (10 MCP servers, tool search still
"true") — verified by execution, not inspection: real MCP calls (github get_me, etc.) now go through without any 400.Supporting the hypothesis that 2.1.223 changed the
defer_loadingthreshold/application rather than the collision being brand new: the collision code path likely predates it (as in #30920 from 2.1.69), but it only became reproducible on my setup once 2.1.223 started deferring tools under conditions that 2.1.222 did not.Note on the workaround:
ENABLE_TOOL_SEARCH=false claudedoes suppress the error on 2.1.223/2.1.224 (consistent with the report), but it is not needed on 2.1.222 — the field collision simply does not occur there.**Correction (2026-08-15) — my earlier bisect was wrong: 2.1.222 is not known-good, and the version is not the causal variable**
I have to retract the version boundary I posted in my previous comment. Further testing today falsified it, and I'd rather correct it now than send anyone chasing a phantom regression in 2.1.223.
What happened: I downgraded 2.1.224 → 2.1.222 and the error disappeared, so I concluded 2.1.222 was clean. It wasn't — the failure is intermittent, and I had simply not hit it yet in the few sessions I ran. Several hours later, the same 2.1.222 install threw the same 400. I then downgraded further to 2.1.220 (which my table listed as "OK"), and it failed there too, within minutes.
Error counts across my local session logs, all projects, all versions:
| Version | 400 errors | First | Last |
|---|---|---|---|
| 2.1.220 | 4 | 2026-08-15 18:08:51 | 2026-08-15 18:18:36 |
| 2.1.222 | 1 | 2026-08-15 17:57:18 | — |
| 2.1.223 | 39 | 2026-08-14 13:50:44 | 2026-08-14 21:11:24 |
| 2.1.224 | 9 | 2026-08-15 11:21:42 | 2026-08-15 11:44:39 |
Four versions affected, including both I had reported as good. The single most convincing data point: one session (
f12d42c5) spans a downgrade and failed on both sides of it — 17:57:18 on 2.1.222, then 18:08:51 on 2.1.220, same project, same MCP config, same message.My original table's "OK" rows were built from absence of errors in the logs of sessions I happened to run, which is not evidence of a fixed version when the bug is intermittent. That was the methodological error.
What actually predicts the failure:
ENABLE_TOOL_SEARCH: "true"insettings.json. That was set throughout every test above and I never varied it, which is exactly why downgrading looked like it helped when it didn't. This is consistent with the original report —ENABLE_TOOL_SEARCH=falsesuppresses it — but my claim that the flag was "not needed on 2.1.222" is withdrawn.Intermittency is the part worth investigating. On 2.1.222 I have three sessions in the same project, same day, with an identical MCP set (
asc,xcodebuildmcp,revenuecat,supabase,sentry+ 7 global servers, ~286 tools): two clean, one 400. Nothing in my configuration distinguishes them. Sodefer_loading+cache_controlare not colliding on every request — something request-scoped decides whether a given tool gets deferred while still carrying a cache breakpoint. The tool named in the message varies accordingly (mcp__github__issue_read,mcp__supabase__list_*,mcp__github__search_*); it appears to be simply whichever tool the API validates first, not a property of that server. I verified the Supabase server's schemas directly viatools/list— they're clean draft-07, no nestedanyOf, no oversized names.Also worth noting: the error fires on the very first message of a session, before any tool call. In the failing session it lands at line 395 of 397, preceded only by startup attachments. So this is a rejection of the tool declarations, not of any tool invocation.
One reporting-quality issue that may be hurting you elsewhere: these 400s are being truncated when written to the session logs, consistently at ~20 characters after
Tool '. Out of 53 logged occurrences, exactly one survived intact:Every other entry stops mid-identifier —
Tool 'mcp__supabase__list_ (reset after 30s)— which is what most users will paste into a bug report. It reads like a different, server-specific problem and cost me a full day of misdirected bisecting. Worth fixing independently of the root cause.Environment: macOS
arm64, Homebrew cask, first-party API endpoint, 12 MCP servers / ~286 tools,ENABLE_TOOL_SEARCH: "true". I'm back on 2.1.224 since the version makes no difference, and will keepENABLE_TOOL_SEARCHon to preserve reproducibility if you'd like additional traces — happy to capture whatever would help pin down the request-scoped trigger.This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.