CLAUDE LABJP
RELEASE — Claude Code v2.1.251 landed on August 28 with close to a hundred changes, built around two themes: tightening permissions and sandboxing, and making usage and cost visibleSECURITY — Read, Write, and Edit could follow a symlink swapped inside the working directory after the permission check had passed, reading or writing outside the approved locationSECURITY — Grep and Glob were not applying Read(...) deny rules to files reached through a symlinked search path. Plugin command path traversal was closed off in the same releaseCOST — /cost now shows a per-session prompt cache line covering hit ratio, misses, tokens re-cached, and warm versus cold, with a matching prompt_cache object for status line scriptsHOOKS — New PreModelSwitch and PostModelSwitch events let you block, confirm, or annotate a model switch, and SessionStart resume hooks now receive staleness and estimated re-cache costMODEL — Seat-billed Enterprise now defaults to Opus 5, and /effort saves a default per model. The 50% weekly limit increase runs through August 31, which leaves one dayRELEASE — Claude Code v2.1.251 landed on August 28 with close to a hundred changes, built around two themes: tightening permissions and sandboxing, and making usage and cost visibleSECURITY — Read, Write, and Edit could follow a symlink swapped inside the working directory after the permission check had passed, reading or writing outside the approved locationSECURITY — Grep and Glob were not applying Read(...) deny rules to files reached through a symlinked search path. Plugin command path traversal was closed off in the same releaseCOST — /cost now shows a per-session prompt cache line covering hit ratio, misses, tokens re-cached, and warm versus cold, with a matching prompt_cache object for status line scriptsHOOKS — New PreModelSwitch and PostModelSwitch events let you block, confirm, or annotate a model switch, and SessionStart resume hooks now receive staleness and estimated re-cache costMODEL — Seat-billed Enterprise now defaults to Opus 5, and /effort saves a default per model. The 50% weekly limit increase runs through August 31, which leaves one day
Articles/Claude Code
Claude Code/2026-08-30Intermediate

Your Config Asks for effort xhigh With Thinking Off, and It Now Quietly Runs at high

Disabling thinking while asking for effort xhigh or max returns a 400. That failure now degrades silently to high instead, so here is a small preflight that catches the mismatch before you send the request.

Claude Code239effort2thinkingOpus 5troubleshooting89

My overnight unattended tasks all finished. No failure logs. But the heaviest stage came back visibly sloppier than usual.

Tracing it back, the stage I had pinned to xhigh had actually run at high. Not one error along the way. Honestly, a hard failure would have been easier to deal with.

You only catch this mismatch once you know that thinking and effort constrain each other. Let me walk through how they interact.

When is not supported when thinking is disabled shows up

Start with what this looked like back when it surfaced as an error. The API returns a 400, and the body reads:

output_config.effort 'xhigh' is not supported when thinking is disabled

Claude Opus 5 thinks by default. You can turn that off with thinking: {"type": "disabled"}, but only while effort sits at high or below. At xhigh and max, the model refuses to run with thinking switched off. The check runs per request, so there is no "it passed at the start of the session" loophole.

Here is the smallest reproduction:

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "max_tokens": 1024,
    "thinking": {"type": "disabled"},
    "output_config": {"effort": "xhigh"},
    "messages": [{"role": "user", "content": "ping"}]
  }'

Drop effort to high, or delete the thinking line entirely, and it goes through. The error text itself points at both exits: use effort high or below, or leave thinking on.

So far, so reasonable. The interesting part comes next.

Each model rejects a different set of values

The thinking types a model accepts have shifted from generation to generation. Reuse a config file and swap only the model name, and this is where you trip.

ModelThinking typesDefaultRejected with 400
Claude Fable 5Adaptive onlyAlways onenabled / disabled
Claude Opus 5Adaptive onlyOnenabled, plus disabled at effort xhigh or max
Claude Opus 4.8 / 4.7Adaptive onlyOffenabled
Claude Sonnet 5Adaptive onlyOnenabled
Claude Opus 4.6 / Sonnet 4.6Adaptive and extended (deprecated)OffNone
Claude Opus 4.5 / Haiku 4.5 / Sonnet 4.5Extended onlyOffadaptive

Two things are worth pulling out of that table.

First, the older thinking: {"type": "enabled", "budget_tokens": N} form is rejected on 4.7 and later. On the 4.6 models it still works while being deprecated, so the breakage surfaces as "but this ran fine on 4.6."

Second, the failure runs the other direction too. The 4.5 generation does not accept adaptive. Standardize on the newer form, then drop back a generation for a cost-sensitive job, and you get a 400 from the other side.

effort has the same kind of step. xhigh arrived later than max, so some models take max but not xhigh. Haiku 4.5 and Sonnet 4.5 do not support the effort parameter at all.

What is left after the error goes away

Claude Code v2.1.251, released on August 28, 2026, changed how this combination is handled. If thinking is disabled and effort is set to xhigh or max, the request is now sent as high instead of failing.

As a user-facing change, that is genuinely kind. You no longer get knocked out of a working session by a 400.

It still gave me pause, though. The failure did not go away; it stopped being visible. Running unattended pipelines nightly as an indie developer, I find loud errors comparatively easy to live with — a failed stage just gets rerun. A stage that completed at a level I never asked for is something I only start to suspect by staring at the output quality.

The same release also made the default /effort level save per model. Finer-grained settings are welcome, but they add another path: switch models, and whatever you last chose for that model comes back.

For now, I check /effort against /status before any heavy stage, so I can see with my own eyes what the session is carrying. It is a crude habit, but crude habits work well against things that change quietly. I audit for a close cousin of this problem too — Claude Code silently ignores a settings.json key when you misspell it by one character.

Rule the combination out before you send it

If you call the API directly, the reliable move is to reject bad combinations locally. Everything the check needs is in the table above, so keep the table as a constant and consult it right before you assemble the request.

#!/usr/bin/env python3
"""Validate thinking / effort combinations before sending the request."""
 
# thinking: accepted types, plus the model's default
THINKING = {
    "claude-fable-5":    {"accepts": {"adaptive"}, "default": "always-on"},
    "claude-mythos-5":   {"accepts": {"adaptive"}, "default": "always-on"},
    "claude-opus-5":     {"accepts": {"adaptive", "disabled"}, "default": "on"},
    "claude-opus-4-8":   {"accepts": {"adaptive", "disabled"}, "default": "off"},
    "claude-opus-4-7":   {"accepts": {"adaptive", "disabled"}, "default": "off"},
    "claude-sonnet-5":   {"accepts": {"adaptive", "disabled"}, "default": "on"},
    "claude-opus-4-6":   {"accepts": {"adaptive", "enabled", "disabled"}, "default": "off"},
    "claude-sonnet-4-6": {"accepts": {"adaptive", "enabled", "disabled"}, "default": "off"},
    "claude-opus-4-5":   {"accepts": {"enabled", "disabled"}, "default": "off"},
    "claude-haiku-4-5":  {"accepts": {"enabled", "disabled"}, "default": "off"},
    "claude-sonnet-4-5": {"accepts": {"enabled", "disabled"}, "default": "off"},
}
 
# effort: accepted levels (models absent here do not support effort at all)
EFFORT = {
    "claude-fable-5":    {"low", "medium", "high", "xhigh", "max"},
    "claude-mythos-5":   {"low", "medium", "high", "xhigh", "max"},
    "claude-opus-5":     {"low", "medium", "high", "xhigh", "max"},
    "claude-opus-4-8":   {"low", "medium", "high", "xhigh", "max"},
    "claude-opus-4-7":   {"low", "medium", "high", "xhigh", "max"},
    "claude-sonnet-5":   {"low", "medium", "high", "xhigh", "max"},
    "claude-opus-4-6":   {"low", "medium", "high", "max"},
    "claude-sonnet-4-6": {"low", "medium", "high", "max"},
    "claude-opus-4-5":   {"low", "medium", "high", "max"},
}
 
NO_DISABLE_AT = {"xhigh", "max"}   # thinking cannot be off at these levels
STRICT_MODELS = {"claude-opus-5"}  # models that enforce the rule above
 
 
def normalize(model: str) -> str:
    """Strip a date suffix such as claude-haiku-4-5-20251001."""
    parts = model.split("-")
    while parts and parts[-1].isdigit() and len(parts[-1]) == 8:
        parts.pop()
    return "-".join(parts)
 
 
def preflight(model: str, thinking_type=None, effort=None):
    key = normalize(model)
    if key not in THINKING:
        return [("unknown", f"{model} is not in the table; add it")]
 
    findings = []
    spec = THINKING[key]
 
    if thinking_type and thinking_type not in spec["accepts"]:
        findings.append(("400", f'{key} rejects thinking.type "{thinking_type}"'))
 
    if effort:
        allowed = EFFORT.get(key)
        if allowed is None:
            findings.append(("400", f"{key} does not support the effort parameter"))
        elif effort not in allowed:
            findings.append(("400", f'effort "{effort}" is unavailable on {key}'))
        elif (key in STRICT_MODELS and effort in NO_DISABLE_AT
              and thinking_type == "disabled"):
            findings.append(("400", f'effort "{effort}" cannot be combined with thinking disabled'))
 
    if effort in NO_DISABLE_AT and thinking_type is None and spec["default"] == "off":
        findings.append(("warn", f"{key} defaults to thinking off; set adaptive "
                                 f'explicitly if you want effort "{effort}" to matter'))
 
    return findings or [("ok", "this combination is accepted")]

Running nine cases through it locally produced this:

[    400] claude-opus-5 / thinking=disabled / effort=xhigh
          -> effort "xhigh" cannot be combined with thinking disabled
[     ok] claude-opus-5 / thinking=disabled / effort=high
          -> this combination is accepted
[     ok] claude-opus-5 / thinking=(unset) / effort=xhigh
          -> this combination is accepted
[   warn] claude-opus-4-7 / thinking=(unset) / effort=xhigh
          -> claude-opus-4-7 defaults to thinking off; set adaptive explicitly if you want effort "xhigh" to matter
[    400] claude-sonnet-5 / thinking=enabled / effort=high
          -> claude-sonnet-5 rejects thinking.type "enabled"
[     ok] claude-sonnet-4-6 / thinking=enabled / effort=medium
          -> this combination is accepted
[    400] claude-haiku-4-5-20251001 / thinking=adaptive / effort=(unset)
          -> claude-haiku-4-5 rejects thinking.type "adaptive"
[    400] claude-haiku-4-5-20251001 / thinking=enabled / effort=low
          -> claude-haiku-4-5 does not support the effort parameter
[    400] claude-fable-5 / thinking=disabled / effort=(unset)
          -> claude-fable-5 rejects thinking.type "disabled"

Five of the nine were 400-equivalent, one raised a warning, and three passed. The line to look at is the fourth one, the warn. The API would not reject it. Opus 4.7 simply defaults to thinking off, so asking for xhigh alone will not buy you the depth you had in mind.

I deliberately surface both classes from the same function: combinations the API rejects, and combinations that pass while missing your intent. You will eventually notice the first kind on your own. Nobody tells you about the second.

normalize() exists so that dated IDs like claude-haiku-4-5-20251001 resolve to a table key. It only strips trailing eight-digit runs, so version numbers such as claude-opus-4-5 survive intact.

Changing effort rewrites your cached prefix

There is a tail to this story.

Your thinking configuration and your effort value are both part of the cached prompt prefix. Change either mid-conversation and the prefix you have been building stops matching, which drops cache_read_input_tokens to zero.

So the natural reaction to a 400 — "fine, I will send high from the next request" — has a quiet second cost attached. The error disappears and so does the cache.

The fix is unremarkable: hold effort constant for the life of a conversation or session, and vary it across workloads instead. Writing a default out explicitly is treated the same as omitting it, so adding "effort": "high" does not by itself invalidate anything.

Where exactly to draw those boundaries is its own decision, and I wrote up how I split mine in Whether to stretch cache TTL to an hour depends on where you come back to, not how long you step away.

One check worth running today

Grep your launch scripts, agent definitions, and CI job files for xhigh and max:

grep -rn 'xhigh\|"max"' --include='*.json' --include='*.md' --include='*.sh' .

For each hit, confirm that the model named in the same place actually supports xhigh, and that thinking is not switched off alongside it. Any config where those two disagree is not failing right now. It is finishing at a level you did not choose.

I spent longer than I would like blaming the wrong thing for a drop in output quality. If this shortens that detour for you, that is a good outcome.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-08-25
A One-Letter Typo in settings.json Is Ignored Without a Single Warning
I diffed claude doctor output between a settings.json with misspelled keys and a correct one. There was no difference at all. Here is what actually gets validated, what slips through, and a small check that catches typos before they cost you a day.
Claude Code2026-07-01
My Claude Code Hooks Stopped Firing After an Update — the Hyphenated Matcher Exact-Match Change in v2.1.195
In Claude Code v2.1.195, hook matchers containing a hyphen switched from partial match to exact match, silently disabling an existing PreToolUse hook. Here is how I isolated the cause and how to write matchers that won't break.
Claude Code2026-05-31
What to Do When Claude Code Hits the Context Window Limit
Long Claude Code sessions eventually hit the 200k token limit and stall. Here's how to use /compact, --continue, and session design patterns to keep development moving without losing context.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →