<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pesimmoq</title>
    <description>The latest articles on DEV Community by Pesimmoq (@persimmoq).</description>
    <link>https://dev.to/persimmoq</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185628%2F5303ed89-af3f-472d-8aa0-10436bb6f3d6.png</url>
      <title>DEV Community: Pesimmoq</title>
      <link>https://dev.to/persimmoq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/persimmoq"/>
    <language>en</language>
    <item>
      <title>Migrating from Haiku 4.5 to Claude Haiku 5.5: 6 breaking changes and when the price goes up 5x</title>
      <dc:creator>Pesimmoq</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:10:55 +0000</pubDate>
      <link>https://dev.to/persimmoq/migrating-from-haiku-45-to-claude-haiku-55-6-breaking-changes-and-when-the-price-goes-up-5x-1m65</link>
      <guid>https://dev.to/persimmoq/migrating-from-haiku-45-to-claude-haiku-55-6-breaking-changes-and-when-the-price-goes-up-5x-1m65</guid>
      <description>&lt;p&gt;Claude Haiku 5.5 (&lt;code&gt;claude-haiku-5-5&lt;/code&gt;) was released on October 7, 2026. It is the first Haiku with adaptive thinking and effort, has a 1M-token context window and 128K output, and its API shape now matches Opus 5.5 / Sonnet 5.5. Pricing is $0.10 / $0.50, one tenth of Haiku 4.5, but it is also the first Haiku where the price goes up 5x once the prompt exceeds 100K tokens.&lt;/p&gt;

&lt;p&gt;Haiku 4.5 was the last generation that accepted &lt;code&gt;budget_tokens&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt;, and prefill, so changing only &lt;code&gt;model&lt;/code&gt; breaks more things than it does on the other 5.5 models. Based on the official migration guide (as of October 12, 2026), here are the 6 changes that return 400 and the things that change silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I verified
&lt;/h2&gt;

&lt;p&gt;On October 12, 2026, I checked items 1 and 2, plus the effort restriction on &lt;code&gt;disabled&lt;/code&gt;, against the live API. All requests were short, with &lt;code&gt;max_tokens: 64&lt;/code&gt;, and the three 400s are not billed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1) thinking: {type: "enabled", budget_tokens: 2000}
   HTTP 400  "thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.

2) temperature: 0.2
   HTTP 400  `temperature` is deprecated for this model.

3) thinking: {type: "disabled"} + effort xhigh
   HTTP 400  output_config.effort 'xhigh' is not supported when thinking is disabled on this model. Use effort 'high' or below, or enable thinking.

4) thinking: {type: "disabled"} + effort low
   HTTP 200  stop_reason=end_turn  content=['text']  input_tokens=15  output_tokens=3  thinking_tokens=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Case 4 also shows that &lt;code&gt;disabled&lt;/code&gt; is accepted with &lt;code&gt;low&lt;/code&gt;, and that &lt;code&gt;usage.output_tokens_details.thinking_tokens&lt;/code&gt; comes back as 0. For the other items (prefill, &lt;code&gt;computer_20250124&lt;/code&gt;, history edits, the organization boundary), I am relying on the official migration guide and did not test them against the live API.&lt;/p&gt;

&lt;p&gt;In my own code, the only place that used Haiku 4.5 was one worker in a crawling agent. It passed neither &lt;code&gt;thinking&lt;/code&gt; nor &lt;code&gt;temperature&lt;/code&gt;, so changing the model ID was all it needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  6 changes that return 400
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;budget_tokens&lt;/code&gt; returns 400
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Haiku 4.5
&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Haiku 5.5
&lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adaptive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;output_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thinking amount is controlled by &lt;code&gt;effort&lt;/code&gt; (&lt;code&gt;low&lt;/code&gt; to &lt;code&gt;max&lt;/code&gt;). The default is &lt;code&gt;medium&lt;/code&gt;. &lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt; turns thinking off, but it works only with &lt;code&gt;high&lt;/code&gt; or below; combining it with &lt;code&gt;xhigh&lt;/code&gt; / &lt;code&gt;max&lt;/code&gt; returns 400. While thinking is &lt;code&gt;disabled&lt;/code&gt;, changing effort per message also returns 400.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;temperature&lt;/code&gt; / &lt;code&gt;top_p&lt;/code&gt; / &lt;code&gt;top_k&lt;/code&gt; return 400
&lt;/h3&gt;

&lt;p&gt;Any non-default value returns 400. Remove them and steer with the prompt instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A trailing assistant turn (prefill) returns 400
&lt;/h3&gt;

&lt;p&gt;End &lt;code&gt;messages&lt;/code&gt; with a user turn. If you used prefill to force JSON, switch to structured outputs (&lt;code&gt;output_config.format&lt;/code&gt;). Note that Haiku 5.5 on Amazon Bedrock has neither structured outputs nor &lt;code&gt;strict&lt;/code&gt;. On Bedrock, specify the format in the prompt and validate the output yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;computer_20250124&lt;/code&gt; returns 400 on every platform
&lt;/h3&gt;

&lt;p&gt;On the Claude API and Google Cloud, switch to &lt;code&gt;computer_toolset_20260801&lt;/code&gt; (no beta header needed). On Amazon Bedrock, switch to &lt;code&gt;computer_20251124&lt;/code&gt; (&lt;code&gt;computer-use-2025-11-24&lt;/code&gt;). Remove the &lt;code&gt;computer-use-2025-01-24&lt;/code&gt; header. The browser tool (&lt;code&gt;browser_toolset_20260801&lt;/code&gt;) is newly available on the Claude API and Google Cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Editing conversation history returns 400
&lt;/h3&gt;

&lt;p&gt;This check did not exist on Haiku 4.5. If you change &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, or earlier turns and then send thinking blocks back, you get 400. The check is on by default for accounts created on or after August 31, 2026; for older accounts it applies only when you send &lt;code&gt;prefix_mismatch_behavior&lt;/code&gt;. Keep history append-only, and make instruction changes with a mid-conversation &lt;code&gt;role: "system"&lt;/code&gt; message. I covered the details in a separate article (&lt;a href="https://zenn.dev/persimmoq/articles/claude-fable-invalid-signature-thinking" rel="noopener noreferrer"&gt;Invalid signature in thinking block&lt;/a&gt;, in Japanese).&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Thinking blocks can only be read by the organization that created them
&lt;/h3&gt;

&lt;p&gt;If you send them from a different organization, they are silently dropped without an error, and the model answers without that reasoning. A service that carries conversations for multiple customers in one store should replay each conversation in the organization that created it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes without an error
&lt;/h2&gt;

&lt;p&gt;Thinking is on by default. Code that never set &lt;code&gt;thinking&lt;/code&gt; will start thinking on Haiku 5.5. Because &lt;code&gt;max_tokens&lt;/code&gt; is a cap on thinking plus the answer combined, a small value can be used up by the &lt;code&gt;thinking&lt;/code&gt; block alone and leave the text empty. Select the block by &lt;code&gt;type&lt;/code&gt;, not &lt;code&gt;content[0].text&lt;/code&gt;. The default for &lt;code&gt;display&lt;/code&gt; is &lt;code&gt;omitted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tokens increase by about 30%. This is the same new tokenizer as 4.7 and later. Images also use the high-resolution tier (about 2.5x for 2000x1500). Re-measure with &lt;code&gt;count_tokens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Thinking blocks from every turn stay in the input. Haiku 4.5 kept only the previous turn's. Multi-turn input grows by more than 30%. To remove them, use thinking block clearing in context editing (deleting them yourself triggers item 5).&lt;/p&gt;

&lt;p&gt;The minimum cacheable prompt is 512 tokens (4,096 on 4.5). Short prompts you had given up on caching now work.&lt;/p&gt;

&lt;p&gt;Refusals were added. &lt;code&gt;cyber&lt;/code&gt; / &lt;code&gt;frontier_llm&lt;/code&gt; / &lt;code&gt;bio&lt;/code&gt; / &lt;code&gt;general_harms&lt;/code&gt;. You get HTTP 200 with &lt;code&gt;stop_reason: "refusal"&lt;/code&gt;. There is no server-side fallback, so handle it yourself. Resending usually gets refused again.&lt;/p&gt;

&lt;p&gt;Priority Tier is not supported. Context-awareness tags are also not injected (use task budgets for budgeting).&lt;/p&gt;

&lt;p&gt;Forced &lt;code&gt;tool_choice&lt;/code&gt; still works. Unlike Opus 5.5 / Sonnet 5.5, it does not return 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the price goes up 5x
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt length&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Cache read&lt;/th&gt;
&lt;th&gt;Batch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100K tokens or less&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$0.01&lt;/td&gt;
&lt;td&gt;$0.05 / $0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over 100K tokens&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;$0.25 / $1.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Prompt length" is the total input tokens of a single request, including cache reads and cache writes. Even if caching makes the request cheaper, once the total crosses the threshold that request is billed at the higher rate. The decision is made per request, and earlier requests are not re-billed.&lt;/p&gt;

&lt;p&gt;So an agent that runs a long conversation on Haiku 5.5 needs to either shrink it with compaction as input approaches 100K tokens, or compare against Sonnet 5.5 ($2 / $10), whose price does not depend on length. Haiku 5.5 above 100K tokens ($0.50 / $2.50) is still 1/4 of Sonnet 5.5, so it is often still cheaper, but an estimate built on "Haiku is 1/20" will be off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start with effort
&lt;/h2&gt;

&lt;p&gt;The official starting points: &lt;code&gt;medium&lt;/code&gt; for most work (including agentic coding); &lt;code&gt;low&lt;/code&gt; for chat, short tool tasks, and simple high-volume processing; &lt;code&gt;high&lt;/code&gt; for knowledge work, long agentic work, and strict instruction following. At &lt;code&gt;low&lt;/code&gt;, long agentic tasks more often skip searches, stop early, or skip checks. Asking in the prompt to "answer directly" does not reduce thinking; lower the effort instead.&lt;/p&gt;

&lt;p&gt;If you give the model a search tool, put today's date in the system prompt so it answers based on recent search results. Render it once at the start of the conversation and keep sending the same &lt;code&gt;system&lt;/code&gt;. If the day rolls over, put the date in the latest turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding these statically
&lt;/h2&gt;

&lt;p&gt;Before changing &lt;code&gt;model&lt;/code&gt;, I updated a tool that lists the affected places as file:line. The check for Haiku 5.5 is &lt;code&gt;--target haiku-5-5&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/volvox-ltd/claude5-migration-kit
python claude5-migration-kit/scan.py path/to/your/app &lt;span class="nt"&gt;--target&lt;/span&gt; haiku-5-5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;400 on &lt;code&gt;budget_tokens&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;adaptive&lt;/code&gt; + &lt;code&gt;effort&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;400 on &lt;code&gt;temperature&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Remove it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;400 on prefill&lt;/td&gt;
&lt;td&gt;Structured outputs (validate yourself on Bedrock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;400 on &lt;code&gt;computer_20250124&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;New toolset; &lt;code&gt;computer_20251124&lt;/code&gt; on Bedrock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;400 on history edits&lt;/td&gt;
&lt;td&gt;Append-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Thinking vanishes across organizations&lt;/td&gt;
&lt;td&gt;Replay in the creating organization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Opus 5.5 / Sonnet 5.5 changes (no &lt;code&gt;disabled&lt;/code&gt;, no forced tools, and so on) are covered in the Japanese article, which includes a section on those two models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zenn.dev/persimmoq/articles/claude-opus-5-5-sonnet-5-5-breaking-changes" rel="noopener noreferrer"&gt;https://zenn.dev/persimmoq/articles/claude-opus-5-5-sonnet-5-5-breaking-changes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Official: &lt;a href="https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide&lt;/a&gt; / &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/about-claude/pricing&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with help from Claude, Anthropic's AI model. It drafted the post from the official Haiku 5.5 migration guide and pricing page, and I checked it before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>anthropic</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>LiteLLM 1.107.0 was malware. A 5-minute check for AI developers, and the one-line setting that would have skipped it</title>
      <dc:creator>Pesimmoq</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:10:30 +0000</pubDate>
      <link>https://dev.to/persimmoq/litellm-11070-was-malware-a-5-minute-check-for-ai-developers-and-the-one-line-setting-that-5h2a</link>
      <guid>https://dev.to/persimmoq/litellm-11070-was-malware-a-5-minute-check-for-ai-developers-and-the-one-line-setting-that-5h2a</guid>
      <description>&lt;p&gt;On October 21, 2026, a malicious &lt;code&gt;litellm==1.107.0&lt;/code&gt; was published to PyPI. It is the second compromise of the package this year, separate from the one in March. PyPI has removed it. The safe version is &lt;strong&gt;1.106.2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;LiteLLM is the library many agents and proxies use to call Claude, GPT and other models through one interface. It often arrives as a dependency you never installed on purpose. And this payload goes after &lt;strong&gt;AI coding tool credentials by name&lt;/strong&gt;: Claude, Cursor, Copilot, Gemini CLI, Codex CLI and others.&lt;/p&gt;

&lt;p&gt;The best technical analysis so far is the write-up by GMO Flatt Security (in Japanese). This post summarizes it for an English-speaking audience and adds the part I couldn't find elsewhere: how to make your installer refuse brand-new releases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.flatt.tech/entry/litellm_2nd_compromise" rel="noopener noreferrer"&gt;https://blog.flatt.tech/entry/litellm_2nd_compromise&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;Times are UTC, from Flatt Security's timeline and PyPI's release metadata.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10-13 06:43&lt;/td&gt;
&lt;td&gt;Attacker's GitHub account created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-13 06:46&lt;/td&gt;
&lt;td&gt;TLS certificate issued for the exfiltration domain &lt;code&gt;litellm[.]dev&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-21 10:44&lt;/td&gt;
&lt;td&gt;Legitimate 1.106.2 published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-21 11:39&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Malicious 1.107.0 published&lt;/strong&gt;, about an hour later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-21&lt;/td&gt;
&lt;td&gt;PyPI removes 1.107.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three details matter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The GitHub tag is clean.&lt;/strong&gt; The malicious files exist only in the wheels on PyPI. Reading the source would not have caught it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs every time Python starts, not just on import.&lt;/strong&gt; The wheel drops a &lt;code&gt;.pth&lt;/code&gt; file and a &lt;code&gt;sitecustomize.py&lt;/code&gt; into &lt;code&gt;site-packages&lt;/code&gt;. Python loads both automatically at startup, so any Python process in that environment triggers it, even one that never imports &lt;code&gt;litellm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It leaves a backdoor.&lt;/strong&gt; A daemon fetches and executes remote code every hour, with the C2 domain read from a file on GitHub.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The legitimate &lt;code&gt;1.107.0rc1&lt;/code&gt; and &lt;code&gt;rc2&lt;/code&gt; had been published days earlier, so &lt;code&gt;1.107.0&lt;/code&gt; looked like the expected next release. Anyone who ran &lt;code&gt;pip install -U litellm&lt;/code&gt; on October 21 is the most likely victim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it steals
&lt;/h2&gt;

&lt;p&gt;Flatt Security lists 16 categories. The ones AI developers should care about most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI tool credentials&lt;/strong&gt;: Claude, Cursor, Copilot, Gemini CLI, Codex CLI, Windsurf, Aider, OpenCode, Hugging Face tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt;, including other processes' via &lt;code&gt;/proc/*/environ&lt;/code&gt;. If you pass &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; or &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; as env vars, assume they're gone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; files&lt;/strong&gt; and private key files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM's own database&lt;/strong&gt;, if you run the proxy: it queries the credentials table for your provider keys&lt;/li&gt;
&lt;li&gt;SSH keys, AWS / GCP / Azure credentials, browser-saved passwords, shell history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Claude Code specifically: per the official docs, credentials live in the &lt;strong&gt;macOS Keychain&lt;/strong&gt; on macOS and in &lt;strong&gt;&lt;code&gt;~/.claude/.credentials.json&lt;/code&gt;&lt;/strong&gt; on Linux and Windows. macOS also falls back to the file when the Keychain can't be written, for example over SSH. If you run Claude Code in CI or on a Linux box, treat that file as exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-minute check
&lt;/h2&gt;

&lt;p&gt;I ran these on my own Mac. Nothing matched, but &lt;code&gt;pip show&lt;/code&gt; alone would not have told me that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Find every installed copy.&lt;/strong&gt; &lt;code&gt;pip show&lt;/code&gt; only checks the current environment. Search for the dist-info folder instead, which carries the version in its name and also catches copies inside &lt;code&gt;uv tool&lt;/code&gt; and &lt;code&gt;pipx&lt;/code&gt; environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~ &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 8 &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s1"&gt;'litellm-*.dist-info'&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;litellm-1.107.0.dist-info&lt;/code&gt; means that environment is infected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check lockfiles.&lt;/strong&gt; A pinned &lt;code&gt;1.107.0&lt;/code&gt; will reinfect whoever rebuilds the environment from an internal mirror or cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rIl&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'litellm'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'requirements*.txt'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'pyproject.toml'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'uv.lock'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'poetry.lock'&lt;/span&gt; ~/code 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Look for the markers.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2&amp;gt;&amp;amp;1
ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'cook|remote_code_loader'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first file means the data has already been sent. The lock file or a matching process means the backdoor is running. On macOS &lt;code&gt;/tmp&lt;/code&gt; is cleared on reboot, so a missing marker is not proof of safety on its own. Combine it with step 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. If you were hit&lt;/strong&gt;, follow Flatt Security's removal steps, then rotate everything as if it was stolen. The AI-specific items people forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revoke and reissue API keys in the Anthropic Console, OpenAI, Google AI Studio and any other provider&lt;/li&gt;
&lt;li&gt;Claude Code: &lt;code&gt;/logout&lt;/code&gt;, then &lt;code&gt;/login&lt;/code&gt;. Do the same for Cursor, Codex and the rest&lt;/li&gt;
&lt;li&gt;Every provider key stored in a LiteLLM proxy&lt;/li&gt;
&lt;li&gt;Everything in your &lt;code&gt;.env&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;GitHub tokens, PyPI / npm tokens, SSH keys, cloud credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also check each provider's usage page for traffic since October 21 that you don't recognize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't install anything younger than 3 days
&lt;/h2&gt;

&lt;p&gt;The malicious release was live for less than a day. &lt;strong&gt;Refusing releases that are only hours old&lt;/strong&gt; would have skipped it entirely. Flatt Security's own registry proxy applies a 3-day quarantine, and its users were not affected. You can get the same effect with features already built into uv and pip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;uv&lt;/strong&gt;: &lt;code&gt;exclude-newer&lt;/code&gt; accepts a duration such as &lt;code&gt;24 hours&lt;/code&gt;, &lt;code&gt;1 week&lt;/code&gt; or &lt;code&gt;3 days&lt;/code&gt;, or an ISO 8601 duration such as &lt;code&gt;P3D&lt;/code&gt;. It is compared against each file's upload time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;exclude-newer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3 days"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a global &lt;code&gt;~/.config/uv/uv.toml&lt;/code&gt;, drop the &lt;code&gt;[tool.uv]&lt;/code&gt; line and keep &lt;code&gt;exclude-newer = "3 days"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pip&lt;/strong&gt;: &lt;code&gt;--uploaded-prior-to&lt;/code&gt; accepts a duration in days in the &lt;code&gt;P3D&lt;/code&gt; form. It only works with indexes that expose upload times, which PyPI does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--uploaded-prior-to&lt;/span&gt; P3D litellm

&lt;span class="c"&gt;# or make it the default&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PIP_UPLOADED_PRIOR_TO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;P3D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your pip doesn't recognize the option, upgrade pip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catch is urgent fixes.&lt;/strong&gt; When a library ships a same-day security fix, you can't wait three days. uv lets you exempt single packages with &lt;code&gt;exclude-newer-package&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;exclude-newer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3 days"&lt;/span&gt;
&lt;span class="n"&gt;exclude-newer-package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cryptography&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With pip, unset the variable for that one install. "Wait by default, opt out on purpose" leaves a trace in your shell history or commit when you skip the delay.&lt;/p&gt;

&lt;p&gt;For Node.js, pnpm has a setting for the same purpose, &lt;code&gt;minimumReleaseAge&lt;/code&gt; (&lt;a href="https://pnpm.io/settings" rel="noopener noreferrer"&gt;https://pnpm.io/settings&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;litellm==1.107.0&lt;/code&gt; is malware. Use 1.106.2. The GitHub source was clean; only the PyPI wheels were poisoned.&lt;/li&gt;
&lt;li&gt;It runs on every Python startup and targets AI tool credentials, environment variables, &lt;code&gt;.env&lt;/code&gt; files and the LiteLLM proxy database.&lt;/li&gt;
&lt;li&gt;Check for installed copies with &lt;code&gt;find&lt;/code&gt;, not just &lt;code&gt;pip show&lt;/code&gt;. Check lockfiles and the marker files.&lt;/li&gt;
&lt;li&gt;If you were hit, rotate every key, including your model provider API keys.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;exclude-newer = "3 days"&lt;/code&gt; in uv or &lt;code&gt;PIP_UPLOADED_PRIOR_TO=P3D&lt;/code&gt; for pip, and exempt urgent fixes explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run Claude agents on Claude Managed Agents, vault credentials of type &lt;code&gt;environment_variable&lt;/code&gt; sit in the sandbox only as opaque placeholders and are swapped for the real secret at egress, so a poisoned dependency that dumps environment variables gets the placeholder, not the key: &lt;a href="https://platform.claude.com/docs/en/managed-agents/vaults" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/managed-agents/vaults&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I write about Claude API changes as they ship. The English edition of my Claude 5 migration guide is on Gumroad: &lt;a href="https://persimmoq.gumroad.com/l/claude-5-migration-guide" rel="noopener noreferrer"&gt;https://persimmoq.gumroad.com/l/claude-5-migration-guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>What shared hosting WordPress users can do about php2shell</title>
      <dc:creator>Pesimmoq</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:48:00 +0000</pubDate>
      <link>https://dev.to/persimmoq/what-shared-hosting-wordpress-users-can-do-about-php2shell-505i</link>
      <guid>https://dev.to/persimmoq/what-shared-hosting-wordpress-users-can-do-about-php2shell-505i</guid>
      <description>&lt;p&gt;On October 21, 2026, around 22:40 Japan time, fixes for two critical PHP vulnerabilities shipped, nicknamed "php2shell." Most of the writeups that followed assume you administer your own server: set this directive, restart these workers, update this proxy config. If you run WordPress on shared hosting, the kind where you don't get to edit php.ini and can't update the PHP binary yourself, none of that is actually available to you.&lt;/p&gt;

&lt;p&gt;I run seven sites on shared hosting for a small company. This post sorts out, based on the official advisories and the hosting providers' own incident reports, what shared hosting users can do themselves, what they cannot do no matter what, and what just has to be waited out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP's official announcement: &lt;a href="https://www.php.net/" rel="noopener noreferrer"&gt;https://www.php.net/&lt;/a&gt; (security advisory for 2026-10-21)&lt;/li&gt;
&lt;li&gt;Advisories: CVE-2026-109618 (&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;GHSA-w8hw-w9fx-j79w&lt;/a&gt;), CVE-2026-109619 (&lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;GHSA-6wmf-4rv5-p3rp&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;An alert from Japan's IPA (in Japanese): &lt;a href="https://www.ipa.go.jp/security/security-alert/2026/pre-alert20261021.html" rel="noopener noreferrer"&gt;https://www.ipa.go.jp/security/security-alert/2026/pre-alert20261021.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A writeup from GMO Flatt Security (in Japanese): &lt;a href="https://blog.flatt.tech/entry/php2shell" rel="noopener noreferrer"&gt;https://blog.flatt.tech/entry/php2shell&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two unauthenticated remote code execution vulnerabilities, CVSS v4.0 score 9.2. The flaw is in PHP itself, not in WordPress or Laravel code, so a well written application is not protected just because it's well written. The official advisory names mod_php as the highest risk, with PHP-FPM, php-cgi, FrankenPHP, and LiteSpeed also affected. Fixed versions are 8.5.12, 8.4.27, 8.3.36, and 8.2.35. PHP 8.1 and earlier are outside the official support window, so only a minimal fix is available for people who build PHP from source. Attacks were observed before the public disclosure: Cloudflare says it saw attack traffic on the morning of October 21.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the claims that were circulating
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PHP powers 80% of the web&lt;/td&gt;
&lt;td&gt;Overstated&lt;/td&gt;
&lt;td&gt;W3Techs puts server-side language share at 69.3% among sites where it's known. Wiz, who discovered the bug, says "70%"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The real danger starts right after the patch ships (a 1-day attack)&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;td&gt;True in general, and in this case attacks started even before disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI can reverse engineer an exploit in minutes&lt;/td&gt;
&lt;td&gt;Overstated&lt;/td&gt;
&lt;td&gt;Wiz wrote that "a frontier AI model" built exploit code for multiple applications "in a few hours," not minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS 10.0&lt;/td&gt;
&lt;td&gt;Wrong&lt;/td&gt;
&lt;td&gt;The official score is 9.2. A member of the PHP security team corrected an earlier 10.0 figure that had leaked out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setting &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt; fixes it&lt;/td&gt;
&lt;td&gt;Only partly&lt;/td&gt;
&lt;td&gt;That mitigates one path in CVE-2026-109619. It does nothing for the other vulnerability, CVE-2026-109618&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row matters most. The single mitigation that spread the widest before disclosure, the one-line fix, only covers part of one of the two CVEs. Alexandre Daubois, a member of the PHP security team, wrote a correction from the inside: &lt;a href="https://daubois.dev/blog/cve-2026-109618-php2shell-rumors/" rel="noopener noreferrer"&gt;https://daubois.dev/blog/cve-2026-109618-php2shell-rumors/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What shared hosting users cannot do
&lt;/h2&gt;

&lt;p&gt;The official mitigations are mostly out of reach if you're on shared hosting. Setting &lt;code&gt;memory_limit=-1&lt;/code&gt; in a way the application can't override, the first mitigation the advisory lists, assumes you control the FPM pool configuration or can set &lt;code&gt;php_admin_value&lt;/code&gt; in Apache, neither of which a shared hosting account gives you. Making &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt; actually take effect requires restarting every worker process, and the advisory explicitly says not to rely on &lt;code&gt;ini_set()&lt;/code&gt; or &lt;code&gt;.user.ini&lt;/code&gt; instead, both of which are the only levers a shared hosting user actually has. Limiting request size or multipart bodies at the web server or proxy level is also the hosting company's job, not something you can configure from a control panel. In practice, the only real fix for a shared hosting site is the hosting company updating PHP itself. There is no patch a user can apply.&lt;/p&gt;

&lt;p&gt;At major Japanese shared hosting providers, the response to that constraint played out two different ways. One provider announced by 6:45 on October 22 that it had already applied the official patch to every server running PHP 8.2 through 8.5, was rolling out its own patches for older versions as they became ready, and had temporarily disabled its control panel and server panel as a precaution. Another provider went further: starting on October 21 it began emergency maintenance, and from around 3:09 on October 22 it took down public access to sites on some of its servers entirely, including sites that weren't even running a vulnerable PHP version, choosing to protect everyone quickly over patching server by server. According to its report (in Japanese, &lt;a href="https://lolipop.jp/info/news/8239/" rel="noopener noreferrer"&gt;https://lolipop.jp/info/news/8239/&lt;/a&gt;), it restored access around 13:00 on October 22 after applying the official patches for 8.2 through 8.5, equivalent fixes for the already end-of-life 7.4 through 8.1, and its own custom patch for 5.3 through 7.3, and it disabled &lt;code&gt;session.upload_progress&lt;/code&gt; across every version as a blanket measure. It said it had seen access attempts that looked like they were targeting the vulnerability, but had not confirmed any unauthorized access or data leak. WordPress core doesn't use PHP sessions, so that last change doesn't affect a stock WordPress install, but a custom upload form or a plugin that relies on sessions is worth checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Move PHP to 8.2 or newer
&lt;/h3&gt;

&lt;p&gt;This is the single most effective step. As providers' own announcements show, patches land for 8.2 through 8.5 first. A site still sitting on 7.4 or 8.0 is both last in line for a fix and on a version with no official patch at all. Check your PHP version setting in your hosting control panel; some providers let you set it per domain. If an older plugin assumes an old PHP version, as happened on one of my own sites with a plugin built for PHP 7.4, update the plugin before you switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check for signs of compromise
&lt;/h3&gt;

&lt;p&gt;A patch doesn't undo damage done before it was applied, and attacks were already happening before disclosure. For WordPress, check the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After SSHing in and moving to the WordPress root&lt;/span&gt;
wp core verify-checksums
wp plugin verify-checksums &lt;span class="nt"&gt;--all&lt;/span&gt;
find wp-content/uploads &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.php'&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s1"&gt;'./wp-content/cache/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for new administrator accounts (filter the users list by the administrator role), for &lt;code&gt;.php&lt;/code&gt; files inside &lt;code&gt;wp-content/uploads/&lt;/code&gt; (a PHP file in an image directory is almost always a sign of compromise), for core or plugin files that don't match the official checksums, and for PHP files modified in the last few days. If you don't have SSH access, sorting the &lt;code&gt;uploads&lt;/code&gt; directory by date in an FTP client already tells you a lot.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. If you find anything, rotate every credential
&lt;/h3&gt;

&lt;p&gt;Wiz's writeup notes that this vulnerability can, in some cases, expose other requests or the server's own memory, which can include passwords, auth tokens, and API keys. If you find any sign of compromise, rotate all of the following: your WordPress admin password, the database password and the authentication keys in &lt;code&gt;wp-config.php&lt;/code&gt; (&lt;code&gt;AUTH_KEY&lt;/code&gt; and the others; WordPress's generator is at &lt;a href="https://api.wordpress.org/secret-key/1.1/salt/" rel="noopener noreferrer"&gt;https://api.wordpress.org/secret-key/1.1/salt/&lt;/a&gt;), your server's FTP and SSH passwords, and any API keys for external services your site uses, such as payment processors, email delivery, or Google. Even without a confirmed compromise, if you're storing API keys in &lt;code&gt;wp-config.php&lt;/code&gt; or in plugin settings, rotating them is worth considering.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. If you're already behind Cloudflare, you're covered
&lt;/h3&gt;

&lt;p&gt;Cloudflare says WAF customers, including on the free plan, have been protected by default since 3:02 UTC on October 21 (&lt;a href="https://blog.cloudflare.com/php-vulnerabilities-waf-protection/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/php-vulnerabilities-waf-protection/&lt;/a&gt;), as long as your DNS is proxied through Cloudflare. That doesn't replace updating PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you don't need to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Update WordPress core or plugins expecting that to fix this. The flaw is in PHP itself, so a fully updated WordPress install is still vulnerable. Keeping WordPress updated is good practice regardless, just not a fix for this issue.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;session.upload_progress.enabled = 0&lt;/code&gt; to &lt;code&gt;.htaccess&lt;/code&gt; or &lt;code&gt;.user.ini&lt;/code&gt;. The official advisory explicitly says not to rely on &lt;code&gt;.user.ini&lt;/code&gt; for this, and believing it worked when it didn't is worse than not trying at all.&lt;/li&gt;
&lt;li&gt;Panic and delete the site. Checking for compromise and rotating credentials comes first. Deleting the site destroys the evidence you'd need to investigate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;php2shell is a flaw in PHP itself, so on shared hosting the only real fix is the hosting company updating PHP, something users cannot do on their own. What a user can do comes down to three things: move to PHP 8.2 or newer, check for signs of compromise, and rotate every credential if any sign turns up. The widely shared one-line fix only covers part of one of the two CVEs, and writing it into &lt;code&gt;.user.ini&lt;/code&gt; doesn't even apply it. Hosting providers were updating their advisories every few hours, so check your own provider's announcement rather than anyone else's.&lt;/p&gt;

&lt;p&gt;This post was drafted on the morning of October 22 and updated on October 23 after one provider's incident report. Response details from hosting companies keep changing, so treat official advisories as the current source, not this post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with help from Claude, Anthropic's AI model. It drafted the post from PHP's own security advisories, the GHSA entries for CVE-2026-109618 and CVE-2026-109619, and the hosting providers' incident reports, and I reviewed it before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>wordpress</category>
      <category>security</category>
      <category>vulnerability</category>
    </item>
    <item>
      <title>LiteLLM 1.107.0 was malware. A 5-minute check for AI developers, and the one-line setting that would have skipped it</title>
      <dc:creator>Pesimmoq</dc:creator>
      <pubDate>Thu, 22 Oct 2026 23:51:57 +0000</pubDate>
      <link>https://dev.to/persimmoq/litellm-11070-was-malware-a-5-minute-check-for-ai-developers-and-the-one-line-setting-that-3j36</link>
      <guid>https://dev.to/persimmoq/litellm-11070-was-malware-a-5-minute-check-for-ai-developers-and-the-one-line-setting-that-3j36</guid>
      <description>&lt;p&gt;On October 21, 2026, someone published a malicious &lt;code&gt;litellm==1.107.0&lt;/code&gt; to PyPI. It's the second time the package has been compromised this year, and it's unrelated to the March incident. PyPI has pulled it, and 1.106.2 is safe.&lt;/p&gt;

&lt;p&gt;If you build with LLMs, there's a decent chance LiteLLM is somewhere on your machine even if you never installed it yourself. A lot of agents and proxies use it to talk to Claude, GPT and the rest through one interface. That's what makes this one nasty: the payload goes looking for AI coding tool credentials by name, including Claude, Cursor, Copilot, Gemini CLI and Codex CLI.&lt;/p&gt;

&lt;p&gt;GMO Flatt Security published the most detailed analysis I've seen, but it's in Japanese. This post summarizes it in English and adds something I couldn't find in any of the write-ups: how to tell uv and pip to refuse brand-new releases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.flatt.tech/entry/litellm_2nd_compromise" rel="noopener noreferrer"&gt;https://blog.flatt.tech/entry/litellm_2nd_compromise&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;The timeline below comes from Flatt Security's analysis and PyPI's release metadata. Times are UTC.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10-13 06:43&lt;/td&gt;
&lt;td&gt;Attacker's GitHub account created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-13 06:46&lt;/td&gt;
&lt;td&gt;TLS certificate issued for the exfiltration domain &lt;code&gt;litellm[.]dev&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-21 10:44&lt;/td&gt;
&lt;td&gt;Legitimate 1.106.2 published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-21 11:39&lt;/td&gt;
&lt;td&gt;Malicious 1.107.0 published, about an hour later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-21&lt;/td&gt;
&lt;td&gt;PyPI removes 1.107.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The v1.107.0 tag on GitHub is clean. The malicious files only exist in the wheels uploaded to PyPI, so reading the source wouldn't have saved you.&lt;/p&gt;

&lt;p&gt;It also doesn't wait for you to import anything. The wheel drops a &lt;code&gt;.pth&lt;/code&gt; file and a &lt;code&gt;sitecustomize.py&lt;/code&gt; into &lt;code&gt;site-packages&lt;/code&gt;, and Python loads both on every startup. Any Python process in that environment sets it off. On top of the data theft, it starts a daemon that pulls and runs remote code once an hour, with the server address read from a file on GitHub.&lt;/p&gt;

&lt;p&gt;The timing was clever, too. &lt;code&gt;1.107.0rc1&lt;/code&gt; and &lt;code&gt;rc2&lt;/code&gt; had gone out normally a few days earlier, so a final &lt;code&gt;1.107.0&lt;/code&gt; was exactly what you'd expect next. If you ran &lt;code&gt;pip install -U litellm&lt;/code&gt; on October 21, you're the person this was aimed at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it steals
&lt;/h2&gt;

&lt;p&gt;Flatt Security counted 16 categories of data. For AI developers, the AI tool credentials are the obvious worry: Claude, Cursor, Copilot, Gemini CLI, Codex CLI, Windsurf, Aider, OpenCode and Hugging Face tokens are all on the list.&lt;/p&gt;

&lt;p&gt;Environment variables are the less obvious one. It reads its own and other processes' through &lt;code&gt;/proc/*/environ&lt;/code&gt;, so if you pass &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; or &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; that way, assume they've been taken. It also collects &lt;code&gt;.env&lt;/code&gt; files and private key files, and if you run the LiteLLM proxy, it queries the proxy's database for the provider keys stored there. SSH keys, cloud credentials, browser passwords and shell history round it out.&lt;/p&gt;

&lt;p&gt;A note on Claude Code. According to the official docs, it keeps credentials in the Keychain on macOS and in &lt;code&gt;~/.claude/.credentials.json&lt;/code&gt; on Linux and Windows. macOS falls back to that file when the Keychain isn't writable, which happens over SSH. If you run Claude Code in CI or on a Linux server, treat that file as exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-minute check
&lt;/h2&gt;

&lt;p&gt;I ran all of this on my own Mac. Nothing turned up, but I wouldn't have known that from &lt;code&gt;pip show&lt;/code&gt; alone, because it only looks at whichever environment is active.&lt;/p&gt;

&lt;p&gt;Searching for the dist-info folder works better. The version is in the folder name, and it finds copies tucked inside &lt;code&gt;uv tool&lt;/code&gt; and &lt;code&gt;pipx&lt;/code&gt; environments as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~ &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 8 &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s1"&gt;'litellm-*.dist-info'&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;litellm-1.107.0.dist-info&lt;/code&gt;, that environment is infected.&lt;/p&gt;

&lt;p&gt;Next, check your lockfiles. A pinned &lt;code&gt;1.107.0&lt;/code&gt; will infect whoever rebuilds that environment from an internal mirror or a cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rIl&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'litellm'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'requirements*.txt'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'pyproject.toml'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'uv.lock'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'poetry.lock'&lt;/span&gt; ~/code 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look for the files the malware leaves behind.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2&amp;gt;&amp;amp;1
ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'cook|remote_code_loader'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first file means your data has already been sent. The lock file, or a matching process, means the backdoor is running. Keep in mind that macOS clears &lt;code&gt;/tmp&lt;/code&gt; on reboot, so not finding them doesn't prove much by itself. Read it together with the &lt;code&gt;find&lt;/code&gt; result.&lt;/p&gt;

&lt;p&gt;If you were hit, follow Flatt Security's removal steps and then rotate everything as if it were stolen. The ones I'd expect AI developers to forget are their model provider API keys (Anthropic Console, OpenAI, Google AI Studio), their Claude Code login (&lt;code&gt;/logout&lt;/code&gt;, then &lt;code&gt;/login&lt;/code&gt;, and the same for Cursor or Codex), and every key stored in a LiteLLM proxy. After that come the usual suspects: &lt;code&gt;.env&lt;/code&gt; contents, GitHub and package registry tokens, SSH keys and cloud credentials. It's also worth checking each provider's usage page for anything since October 21 you don't recognize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't install anything younger than 3 days
&lt;/h2&gt;

&lt;p&gt;The bad release was up for less than a day. If your installer had simply ignored anything only a few hours old, you'd have skipped it without noticing. Flatt Security runs a registry proxy with a 3-day quarantine, and its users weren't affected. You don't need a proxy to get the same effect, though. Both uv and pip can do it today.&lt;/p&gt;

&lt;p&gt;In uv, &lt;code&gt;exclude-newer&lt;/code&gt; takes a duration like &lt;code&gt;24 hours&lt;/code&gt;, &lt;code&gt;1 week&lt;/code&gt; or &lt;code&gt;3 days&lt;/code&gt;, or an ISO 8601 duration like &lt;code&gt;P3D&lt;/code&gt;, and compares it against each file's upload time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# pyproject.toml&lt;/span&gt;
&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;exclude-newer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3 days"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a global &lt;code&gt;~/.config/uv/uv.toml&lt;/code&gt;, leave out the &lt;code&gt;[tool.uv]&lt;/code&gt; line.&lt;/p&gt;

&lt;p&gt;In pip, the option is &lt;code&gt;--uploaded-prior-to&lt;/code&gt;, and it takes days in the &lt;code&gt;P3D&lt;/code&gt; form. It only works with indexes that report upload times, which PyPI does. If your pip doesn't know the option, upgrade it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--uploaded-prior-to&lt;/span&gt; P3D litellm

&lt;span class="c"&gt;# or make it the default&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PIP_UPLOADED_PRIOR_TO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;P3D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downside is that urgent fixes get held back too, and you can't sit on a same-day security patch for three days. uv handles this with &lt;code&gt;exclude-newer-package&lt;/code&gt;, which lets you exempt a single package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;exclude-newer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3 days"&lt;/span&gt;
&lt;span class="py"&gt;exclude-newer-package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;cryptography&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With pip, unset the variable for that one install. I like this split because skipping the delay becomes a deliberate act that shows up in your shell history or a commit, rather than something that happens by default.&lt;/p&gt;

&lt;p&gt;If you're on Node.js, pnpm has a setting for the same thing called &lt;code&gt;minimumReleaseAge&lt;/code&gt; (&lt;a href="https://pnpm.io/settings" rel="noopener noreferrer"&gt;https://pnpm.io/settings&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;litellm==1.107.0&lt;/code&gt; is malware, and 1.106.2 is the version to use. Search for installed copies with &lt;code&gt;find&lt;/code&gt; instead of trusting &lt;code&gt;pip show&lt;/code&gt;, check your lockfiles, and rotate your provider API keys along with everything else if you find it. Going forward, a 3-day delay in uv or pip would have kept this release off your machine.&lt;/p&gt;

&lt;p&gt;One more thing if you run Claude agents on Claude Managed Agents. Vault credentials of type &lt;code&gt;environment_variable&lt;/code&gt; only exist inside the sandbox as placeholders and get swapped for the real secret on the way out, so a poisoned dependency that dumps environment variables ends up with the placeholder. The docs are here: &lt;a href="https://platform.claude.com/docs/en/managed-agents/vaults" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/managed-agents/vaults&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with help from Claude, Anthropic's AI model. It drafted the post from Flatt Security's analysis and the uv, pip and Claude Code docs. I ran the checks on my own machine and reviewed everything before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>I built an AI coworker that works while I sleep, using nothing but Claude Code</title>
      <dc:creator>Pesimmoq</dc:creator>
      <pubDate>Fri, 16 Oct 2026 03:06:05 +0000</pubDate>
      <link>https://dev.to/persimmoq/i-built-an-ai-coworker-that-works-while-i-sleep-using-nothing-but-claude-code-6k8</link>
      <guid>https://dev.to/persimmoq/i-built-an-ai-coworker-that-works-while-i-sleep-using-nothing-but-claude-code-6k8</guid>
      <description>&lt;p&gt;Claude Code has a feature called &lt;strong&gt;routines&lt;/strong&gt;: a cloud session that starts on a cron schedule, clones a GitHub repository, uses the MCP connectors you've attached on claude.ai (Slack, Gmail), and exits. No server, no extra tooling.&lt;/p&gt;

&lt;p&gt;I used it to build a watcher that, every morning at 09:00 JST, reads 12 pages of Anthropic's official docs, diffs them against yesterday, posts a Japanese summary to Slack, and, when a change affects my books, pushes a draft article to GitHub. The motivation was embarrassing: I write guides about the Claude API, and I missed the Haiku 5.5 launch for &lt;strong&gt;five days&lt;/strong&gt;. A local script had been posting "changed" notices to Slack every day. Nobody was reading them.&lt;/p&gt;

&lt;p&gt;Everything below is as of October 16, 2026. Routines are moving fast; check the official docs for the current shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a routine is
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A 5-field cron expression (UTC) or a one-time timestamp starts a cloud session. &lt;strong&gt;Minimum interval is one hour.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The session starts with your GitHub repository cloned. It cannot see your local files, local environment variables, or local services.&lt;/li&gt;
&lt;li&gt;It can use MCP connectors attached on claude.ai (Slack, Gmail, ...). MCP servers configured only in Claude Code are not available.&lt;/li&gt;
&lt;li&gt;Model, allowed tools, and the prompt go in one JSON object. The prompt is read from scratch every run, so it has to be self-contained.&lt;/li&gt;
&lt;li&gt;Create, update, run-now, and run logs are available through an API (from inside Claude Code, the &lt;code&gt;RemoteTrigger&lt;/code&gt; tool). Deleting is web-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volvox-ltd/claude-docs-watch (private)
├── pages.json        # 12 official pages (the .md versions on platform.claude.com)
├── watch.py          # fetch -&amp;gt; compare with state/ -&amp;gt; diffs/&amp;lt;date&amp;gt;/ + result.json (stdlib only)
├── state/            # yesterday's text
├── diffs/            # unified diffs per day
├── drafts/           # article drafts the routine writes when a change matters
├── log/              # daily summaries
└── ROUTINE_PROMPT.md # the instructions the routine receives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design decision that matters: &lt;strong&gt;Python finds the diff; the model only judges and writes.&lt;/strong&gt; Asking a model to "read these 12 pages and tell me what changed" means reading ~300k characters every morning and missing things. &lt;code&gt;watch.py&lt;/code&gt; produces the diff files; the model reads only those.&lt;/p&gt;

&lt;h2&gt;
  
  
  The routine definition
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-docs-watch 09:00 JST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cron_expression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0 0 * * *"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"job_config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ccr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"env_…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"session_context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"git_repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/volvox-ltd/claude-docs-watch"&lt;/span&gt;&lt;span class="p"&gt;}}],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allowed_tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="s2"&gt;"mcp__Slack__slack_send_message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp__Slack__slack_search_channels"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"parent_tool_use_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;the prompt below&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}}}]}},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcp_connections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"connector_uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Slack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.slack.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sonnet 5.5 is plenty: the daily job is "read a diff and summarize it".&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The repository https://github.com/volvox-ltd/claude-docs-watch is already cloned in your
working directory (branch main). Configure git with user.name 'docs-watch-bot' and
user.email 'contact@volvox-ltd.com' before committing.

You are the daily docs watcher for persimmoq, an author of Claude API guides. You run
inside a fresh clone of this repository every morning. Work in Japanese for all
human-facing text.

Do these steps in order.

1. Run `git pull --rebase origin main` first (the checkout can be stale). Then run
   `python3 watch.py` from the repository root and read `result.json`.

2. If `changed` is empty and `errors` is empty: append one line `&amp;lt;date&amp;gt; no changes` to
   `log/&amp;lt;YYYY-MM&amp;gt;.md`, commit it, push to `main`, and send ONE short Slack message to the
   private channel #mail (channel_id C0BEY8JUCDR). Then stop.

3. If there are changes: read every file listed under `changed[].diff`. Ignore pure
   formatting changes. For the substantive changes, write a summary with, per page: the
   page key, 1–3 sentences on what changed, and a judgement on whether it affects the
   author's published material (the books on Claude 5 migration and on Managed Agents,
   and the articles on those topics). Mark each page as affected / not affected / check.

4. Write that summary to `log/&amp;lt;YYYY-MM&amp;gt;.md` under a heading `## &amp;lt;date&amp;gt;` and post the same
   summary to the Slack channel. Keep it under 2,000 characters; link each page URL.

5. If at least one page is affected and the change is a new model, a new price, a new
   breaking change, a deprecation/retirement, or a new Managed Agents feature: write ONE
   draft article in `drafts/&amp;lt;date&amp;gt;-&amp;lt;short-slug&amp;gt;.md` (front matter with published: false),
   quoting the exact new text from the diff where it matters, linking the official page,
   and ending with a section "What I verified" containing only a placeholder for the author
   to fill in after testing against the live API. Do not invent behavior that the
   documentation does not state. Do not claim anything was tested.

6. `git add -A`, commit, push to `main`. If the push fails, retry once after `git pull --rebase`.

7. If any `errors` are present, mention them at the end of the Slack message.

Never publish anything outside this repository. Never edit files under `state/` by hand.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 5 is the point. &lt;strong&gt;The coworker drafts; it never publishes.&lt;/strong&gt; The draft ends with an empty "What I verified" section that a human fills in after running the real API. The prompt says "do not claim anything was tested" for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened on the first two runs (October 16)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Run 1 (23 seconds, 8 turns).&lt;/strong&gt; Sandbox allocated, repo cloned, &lt;code&gt;watch.py&lt;/code&gt; found no changes, log appended, push succeeded, Slack post failed with &lt;code&gt;channel_not_found&lt;/code&gt;. The agent searched for the channel, found nothing, and fell back to a mobile push notification. Cause: the Slack connector on claude.ai was attached to a different workspace. I reconnected it and put the channel ID in the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 2 (51 seconds, 9 turns).&lt;/strong&gt; Slack post succeeded. Then the push was rejected: the sandbox's checkout predated run 1's commit. The agent ran &lt;code&gt;git pull --rebase&lt;/code&gt;, saw its own commit was a duplicate line, discarded it, and exited cleanly. Reasonable, but I added &lt;code&gt;git pull --rebase&lt;/code&gt; as step 1 so it doesn't happen every morning.&lt;/p&gt;

&lt;p&gt;The run log is available from the API with timestamps for every tool call, error, and the final message. You can read everything the coworker did while you were asleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that broke
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The GitHub App must be installed on the organization.&lt;/strong&gt; Connecting GitHub on claude.ai was not enough for a private repo under an org; creating the routine failed with &lt;code&gt;repo_access_denied&lt;/code&gt; until I installed github.com/apps/claude on the org and granted the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A connector points at one workspace.&lt;/strong&gt; With several Slack workspaces, it was attached to the wrong one. Use channel IDs, not names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The checkout is not always fresh.&lt;/strong&gt; A routine that pushes should pull first.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cost and limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Runs use the allowance of my Claude subscription plan; I have seen no separate API charges. A no-change run is a few thousand tokens. Check the pricing page for your plan.&lt;/li&gt;
&lt;li&gt;Minimum interval one hour. Not a tool for "poll every five minutes".&lt;/li&gt;
&lt;li&gt;No access to your local machine. Anything the job needs must be in the repo or reachable from the cloud.&lt;/li&gt;
&lt;li&gt;Deletion is web-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three ways to build the same thing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code routine&lt;/th&gt;
&lt;th&gt;Managed Agents&lt;/th&gt;
&lt;th&gt;cron + API on your machine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs on&lt;/td&gt;
&lt;td&gt;Anthropic's cloud&lt;/td&gt;
&lt;td&gt;Anthropic's cloud&lt;/td&gt;
&lt;td&gt;your machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;one JSON&lt;/td&gt;
&lt;td&gt;Agent + Deployment&lt;/td&gt;
&lt;td&gt;plist/crontab + script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing&lt;/td&gt;
&lt;td&gt;plan allowance&lt;/td&gt;
&lt;td&gt;tokens + session time ($0.08/h)&lt;/td&gt;
&lt;td&gt;API tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;periodic jobs that fit in a repo + connectors&lt;/td&gt;
&lt;td&gt;long-running agents that need a hosted toolbox&lt;/td&gt;
&lt;td&gt;jobs that need local files or local credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notify via&lt;/td&gt;
&lt;td&gt;connectors (Slack/Gmail), push&lt;/td&gt;
&lt;td&gt;webhooks&lt;/td&gt;
&lt;td&gt;your own&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I use all three. This watcher is a routine; my production agent platform is Managed Agents; the weekly Search Console report, which needs local OAuth tokens, stays on launchd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Claude Code routine gives you a cron'd cloud agent with a repo and Slack, with no infrastructure.&lt;/li&gt;
&lt;li&gt;Split the roles: Python detects, the model judges and writes, a human publishes.&lt;/li&gt;
&lt;li&gt;What breaks is permissions and connections, not the prompt.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I wrote this with help from Claude, Anthropic's AI model. The routine, the repository and the problems described are my own setup; Claude drafted the text from my notes and I checked it before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>anthropic</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Opus 5.5 / Sonnet 5.5: 5 settings that return 400 when you only change the model name</title>
      <dc:creator>Pesimmoq</dc:creator>
      <pubDate>Thu, 15 Oct 2026 01:47:16 +0000</pubDate>
      <link>https://dev.to/persimmoq/claude-opus-55-sonnet-55-5-settings-that-return-400-when-you-only-change-the-model-name-5g36</link>
      <guid>https://dev.to/persimmoq/claude-opus-55-sonnet-55-5-settings-that-return-400-when-you-only-change-the-model-name-5g36</guid>
      <description>&lt;p&gt;Claude Opus 5.5 shipped on September 22, 2026 and Claude Sonnet 5.5 on September 28. If you take code that runs on Opus 5 or Sonnet 5 and change only the &lt;code&gt;model&lt;/code&gt; string, one of these five settings will return an HTTP 400. All five are in Anthropic's official migration guides; I reproduced the first one against the live API on October 6 and pasted the exact responses below.&lt;/p&gt;

&lt;p&gt;A static scanner that finds these in your code (file:line) is open source: &lt;a href="https://github.com/volvox-ltd/claude5-migration-kit" rel="noopener noreferrer"&gt;claude5-migration-kit&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scan.py path/to/your/app &lt;span class="nt"&gt;--target&lt;/span&gt; opus-5-5
python scan.py path/to/your/app &lt;span class="nt"&gt;--target&lt;/span&gt; sonnet-5-5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. &lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt; returns 400
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Opus 5.5 cannot turn thinking off.&lt;/strong&gt; On Opus 5 you could send &lt;code&gt;disabled&lt;/code&gt; at &lt;code&gt;high&lt;/code&gt; effort or below. On Opus 5.5, both &lt;code&gt;disabled&lt;/code&gt; and &lt;code&gt;enabled&lt;/code&gt; + &lt;code&gt;budget_tokens&lt;/code&gt; are rejected at every effort level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"thinking.type.disabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omit &lt;code&gt;thinking&lt;/code&gt; (same as adaptive) and lower &lt;code&gt;output_config.effort&lt;/code&gt; where you used to disable thinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sonnet 5.5 replaces &lt;code&gt;disabled&lt;/code&gt; with &lt;code&gt;between_tools&lt;/code&gt;.&lt;/strong&gt; It is the lowest thinking setting: no up-front thinking, only the short notes between tool calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;To turn thinking off on this model, send "thinking": {"type": "between_tools"} instead of {"type": "disabled"}. The model does not think before responding. The short updates it writes between tool calls come back as thinking blocks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Sonnet 5
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;output_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xhigh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...])&lt;/span&gt;

&lt;span class="c1"&gt;# Sonnet 5.5
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;between_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;output_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;between_tools&lt;/code&gt; works only at &lt;code&gt;high&lt;/code&gt; effort or below (400 with &lt;code&gt;xhigh&lt;/code&gt; / &lt;code&gt;max&lt;/code&gt;), takes no other field (&lt;code&gt;display&lt;/code&gt;, &lt;code&gt;budget_tokens&lt;/code&gt; and &lt;code&gt;block_binding&lt;/code&gt; are 400), and cannot be combined with per-message effort changes. In a request without tools it answers without thinking at all, so move multi-step reasoning tasks back to adaptive.&lt;/p&gt;

&lt;p&gt;Both responses above are verbatim from the API (&lt;code&gt;max_tokens: 64&lt;/code&gt;, no charge for a 400).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Forced &lt;code&gt;tool_choice&lt;/code&gt; returns 400
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;{"type": "any"}&lt;/code&gt; and &lt;code&gt;{"type": "tool", "name": ...}&lt;/code&gt; are rejected on both models, including on the token-counting endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool_choice: type "tool" and "any" are not supported for this model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;tool_choice: {"type": "auto"}&lt;/code&gt;, mark the tool &lt;code&gt;strict: true&lt;/code&gt; so the input matches the schema, and say in the prompt when to call it. Strict tools are capped at 20 per request and need &lt;code&gt;additionalProperties: false&lt;/code&gt; on every object. If you only wanted JSON, use structured outputs (&lt;code&gt;output_config.format&lt;/code&gt;) instead of a tool. &lt;strong&gt;On Amazon Bedrock, Opus 5.5 and Sonnet 5.5 have no structured outputs (strict included)&lt;/strong&gt;: send &lt;code&gt;auto&lt;/code&gt;, name the tool in the prompt, and validate the input yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;computer_20251124&lt;/code&gt; returns 400 on the Claude API and Google Cloud
&lt;/h2&gt;

&lt;p&gt;Declare computer use as the &lt;code&gt;computer_toolset_20260801&lt;/code&gt; toolset (no beta header). Sonnet 5.5 also rejects &lt;code&gt;computer_20250124&lt;/code&gt; on every platform. Amazon Bedrock keeps &lt;code&gt;computer_20251124&lt;/code&gt;. The &lt;code&gt;fine-grained-tool-streaming-2025-05-14&lt;/code&gt; header is a 400 next to a toolset entry; set &lt;code&gt;eager_input_streaming: true&lt;/code&gt; on the tools that need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Editing conversation history returns 400 (or drops the thinking)
&lt;/h2&gt;

&lt;p&gt;The preserved-thinking check that Fable 5.1 introduced now applies to both models: thinking blocks are bound to the model and to the conversation prefix. Editing &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt; or earlier turns and then sending the thinking blocks back is a 400 on accounts created on or after August 31, 2026 (enforced by default), or a dropped block otherwise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep history append-only. Change instructions with a mid-conversation &lt;code&gt;{"role": "system", ...}&lt;/code&gt; message.&lt;/li&gt;
&lt;li&gt;Change tools with &lt;code&gt;tool_addition&lt;/code&gt; / &lt;code&gt;tool_removal&lt;/code&gt;; with the &lt;code&gt;inline-tools-2026-09-15&lt;/code&gt; beta you can include full definitions.&lt;/li&gt;
&lt;li&gt;To shrink history, use on-demand compaction (&lt;code&gt;compact-2026-09-04&lt;/code&gt;): you receive a signed summary block and send it first on the next request.&lt;/li&gt;
&lt;li&gt;Sonnet 5.5 thinking blocks are also &lt;strong&gt;account-bound&lt;/strong&gt;: sent from another organization they are silently dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Sonnet 5.5 rejects Opus 4.x / Sonnet 5 as advisors
&lt;/h2&gt;

&lt;p&gt;With a Sonnet 5.5 executor, the advisor must be Opus 5, Opus 5.5, Sonnet 5.5, Fable 5 / 5.1 or Mythos 5 / 5.1. Opus 4.8 / 4.7 / 4.6, Sonnet 5, Sonnet 4.6 and Haiku 5.5 return 400. Advice comes back encrypted as &lt;code&gt;advisor_redacted_result&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changes that don't fail, but matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus 5.5 defaults to &lt;code&gt;effort: medium&lt;/code&gt;&lt;/strong&gt; (Opus 5: &lt;code&gt;high&lt;/code&gt;). Carry over a request without &lt;code&gt;effort&lt;/code&gt; and it runs one level lower. Anthropic reports that 5.5 at &lt;code&gt;medium&lt;/code&gt; matches or beats Opus 5 at &lt;code&gt;high&lt;/code&gt;; start there and sweep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet 5.5 effort levels are recalibrated.&lt;/strong&gt; Start coding agents at &lt;code&gt;medium&lt;/code&gt;, general work at &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt; 128K with streaming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text between tool calls now arrives in &lt;code&gt;thinking&lt;/code&gt; blocks&lt;/strong&gt;, empty at the default &lt;code&gt;display&lt;/code&gt;. A UI that streams progress notes goes quiet with no error. Use &lt;code&gt;display: "updates"&lt;/code&gt; (beta header &lt;code&gt;thinking-display-updates-2026-08-18&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More refusal categories.&lt;/strong&gt; Opus 5.5 adds &lt;code&gt;bio&lt;/code&gt;; Sonnet 5.5 has &lt;code&gt;cyber&lt;/code&gt;, &lt;code&gt;bio&lt;/code&gt;, &lt;code&gt;frontier_llm&lt;/code&gt;, &lt;code&gt;reasoning_extraction&lt;/code&gt;, &lt;code&gt;general_harms&lt;/code&gt;. Server-side fallback only retries &lt;code&gt;cyber&lt;/code&gt; and &lt;code&gt;frontier_llm&lt;/code&gt; on Sonnet 5. Since September 24, refusals before any output in &lt;code&gt;bio&lt;/code&gt; / &lt;code&gt;frontier_llm&lt;/code&gt; / &lt;code&gt;reasoning_extraction&lt;/code&gt; are billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt cache minimum is 512 tokens&lt;/strong&gt; (Sonnet 5: 1,024). Sonnet 5.5 cache reads dropped to $0.10/MTok on October 7 (0.05x).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Priority Tier is not supported on Opus 5.5.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet 4.5 retires on November 30, 2026.&lt;/strong&gt; Replacement: Sonnet 5.5.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;disabled&lt;/code&gt; → 400&lt;/td&gt;
&lt;td&gt;Opus 5.5: omit + lower effort. Sonnet 5.5: &lt;code&gt;between_tools&lt;/code&gt; (≤ &lt;code&gt;high&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool_choice: any&lt;/code&gt; → 400&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auto&lt;/code&gt; + &lt;code&gt;strict&lt;/code&gt; + name the tool in the prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;computer_20251124&lt;/code&gt; → 400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;computer_toolset_20260801&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;edited history → 400&lt;/td&gt;
&lt;td&gt;append-only; system messages, tool_addition, compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;advisor → 400&lt;/td&gt;
&lt;td&gt;Opus 5 / 5.5, Sonnet 5.5, Fable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The seven things that already broke when moving from 4.x to Opus 5 / Sonnet 5 (&lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;budget_tokens&lt;/code&gt;, prefill, &lt;code&gt;max_tokens&lt;/code&gt; eaten by thinking, &lt;code&gt;content[0]&lt;/code&gt; on refusals, empty thinking text, the ~30% tokenizer increase) still apply.&lt;/p&gt;

&lt;p&gt;Official sources: &lt;a href="https://platform.claude.com/docs/en/models/opus-5-5/migration-guide" rel="noopener noreferrer"&gt;Opus 5.5 migration guide&lt;/a&gt; · &lt;a href="https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide" rel="noopener noreferrer"&gt;Sonnet 5.5 migration guide&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with help from Claude, Anthropic's AI model. It drafted the post from the official migration guides and my notes. I checked every 400 against the live API before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>anthropic</category>
      <category>llm</category>
      <category>api</category>
    </item>
  </channel>
</rss>
