On October 21 and 22, I spent two long days patching our own servers. PHP had announced an emergency release for two critical remote code execution bugs, now known as "PHP2Shell". We run seven RunCloud servers with about 30 WordPress sites, and several of them are on PHP versions that no longer get official fixes.
I did most of the work together with Hermes, my AI agent, running Claude Opus 5.5. It went well until it didn't. In the middle of the job, the cloud model stopped answering. My local model, Qwen3.8-Flash-Next on my own workstation, picked up the same session and finished the work.
This post covers what happened, why the cloud model stopped, and why I now see a local model as a real backup.
The job
The work came in phases over two days:
- Patch PHP on all servers. Where RunCloud no longer ships builds (PHP 5.6 to 8.1 on old Ubuntu releases), we rebuilt the PHP binaries ourselves with the upstream fixes.
- Clean up and restore a handful of WordPress sites that had been compromised. The cause turned out to be an old copy of The Grid, a plugin bundled with a Themeco theme that we can't update without breaking the site layouts.
- Make a full inventory of every plugin on every site, look up known vulnerabilities for the exact installed versions, and backport the fixes into the old plugin code by hand. For these sites, a plugin update is not an option for the same reason.
Phase 3 is where it got stuck.
"Your request was not processed"
The agent had built the inventory: 32 sites, 206 plugins, 300 plugin/version pairs. It had matched them against the free WPVulnerability database and was checking access logs for suspicious requests to wp-content/uploads.
Then every message I sent got the same reply:
Your request was not processed. Send it again if you still want me to carry it out.
I tried "continue", "try it another way", and "try again, differently". The reply was the same each time. When I asked where it got stuck, it answered normally. It said it had not executed anything in those turns and could not see why. Its guess was a context length issue or an outage.
It wasn't either of those. The Hermes log had the real reason:
2026-10-22 14:49:28 WARNING [20261021_095417_816396] agent.conversation_loop:
Model declined to respond (finish_reason=content_filter).
model=claude-opus-5-5[1m] ...
refusal=This request triggered restrictions on violative cyber content and was
blocked under Anthropic's Usage Policy.
There were five of these in that session between 14:49 and 16:42, and one more in a separate session where I asked for a client report about the same breach. That report session was actually the first one I moved to the local model, and it finished the report there.
What actually happened
Claude Opus 5.5 runs behind safety classifiers. When one fires, the raw API returns a normal response with stop_reason: "refusal" and a category. Hermes logged it as finish_reason=content_filter, without the category field, but the refusal text points to the cyber category. Anthropic's refusals and fallback docs describe it like this: the category covers requests that "could enable cyber harm, such as malware or exploit development", and "benign cybersecurity work can also trigger this category."
That second sentence was us. Look at what was in the context window at that point:
- Access log lines with requests for
code-execution.phpin an uploads folder and../../wp-config.phppath traversal attempts - A list of 47 serious CVEs: remote code execution, file upload, SQL injection, privilege escalation
- Diffs between vulnerable and fixed plugin versions, such as
maybe_unserialize()being replaced withunserialize( ..., array( 'allowed_classes' => false ) ) - Earlier in the session: forensics on real web shells and backdoor admin accounts
To a classifier, "read the vulnerable code path and figure out exactly how the bug works" looks the same whether you want to patch it or exploit it. My reading is that the classifier looks at the request as a whole, including the tool output the agent had just read. My message was "continue". The context was the problem.
A few details made it harder to see:
- The refusal was invisible to the model. The agent got a short placeholder in place of its own turn. It didn't know it had been refused, so it couldn't tell me, and it guessed wrong about the cause.
- Retrying doesn't help. The docs say so too: "Re-sending a refused request to the same model usually earns another refusal." Each retry sent the same context.
- It wasn't consistent. After a non-technical question, the session worked again. The next phase (an nginx rule that blocks PHP execution in uploads on all 32 sites) went fine. Then, once it was reading Gravity Forms diffs, the next refusal came at 16:42.
To be fair: this isn't a bug, and I understand why the classifier exists. The docs also describe a server-side fallbacks option that retries a refused request on a different Claude model. My setup goes through a subscription-based provider in Hermes that doesn't use it, so for me the session just stopped. If you build on the API directly, check whether your refusal category has a recommended fallback model, and log refusals as their own event. A refusal is an HTTP 200, so error-rate monitoring won't see it.
Switching to the local model
My workstation is a Ryzen 9 9950X3D with an RTX 5090 and 128 GB of RAM. I already run several large open models on it. I wrote about Qwen3.8-Flash-Next with Strata and a 5-bit quant and compared four local models side by side.
Hermes treats the local server as just another OpenAI-compatible provider:
providers:
strata-qwen:
name: Strata Qwen3.8-Flash-Next (UD-Q5_K_XL)
base_url: http://127.0.0.1:8081/v1
default_model: strata
models:
strata:
context_length: 524288
The switch took two steps:
- Free the GPU (a background job was using it) and start the Strata server with the Unsloth UD-Q5_K_XL quant. That was at 17:22.
- In the same Hermes session, switch the model with
/modeland type (in Dutch): "Continue (now with a different model, hopefully no restrictions this time)."
Hermes keeps the session history, so Qwen picked up the full context: the inventory, the CVE list, the research that Opus and its subagents had already done, the plugin sources already downloaded, the earlier decisions, and my standing rules (no plugin updates, back up first, roll back on any error).
What Qwen finished
From 17:45 to 18:38, Qwen made 66 model calls. The prompt grew from about 121k to 212k tokens, well within the 512k context. Once the prompt cache was warm, turns took 3 to 20 seconds. About 20 turns missed the cache, mostly while it was writing the patcher, and took 70 to 115 seconds each. That's slower than the cloud, but it kept going.
In that time it:
- Finished filtering the 47 serious CVEs (started under Opus) down to the ones that work without a login, and wrote down why each of the others was skipped
- Wrote a patcher (
patch_plugins.py) with exact code anchors, aHERMES-PATCHmarker, and CRLF handling, and tested it locally on copies of each affected version - Where an upstream fix was available, took it from the official release by diffing the vulnerable and fixed versions, instead of writing its own
- Wrote a deploy script with a backup per server,
php -lwith each site's own PHP version, and automatic rollback - Patched 48 plugin copies (39 files) across our servers: Gravity Forms, Broken Link Checker, MonsterInsights, Media Library Assistant, WooCommerce, WPForms, Relevanssi, StoryChief, and a product tabs plugin. No rollbacks were needed, and all 32 sites still returned 200.
- After I asked for it, moved MySQL to listen on
127.0.0.1only on the five servers that still listened on all interfaces. On two servers, the real setting turned out to be in a RunCloud file (mariadb.conf.d/99-server.cnf) that overrode the drop-in it had added first. It noticed, found the right file, and fixed it there. - Did a final read-only check across all servers when I asked it to verify everything, and explained a monitoring alert as the short MySQL restart it was
It wasn't flawless. The patcher had two bugs: \$ in Python replacement strings, and a slug comparison that missed folder names with version numbers. It found both in its own local test run and fixed them. One count in its final summary (56 patched plugin copies) didn't match the earlier report (48), and I haven't fully reconciled that difference yet. Those are the kinds of things you check anyway, whatever model you use.
What I take away from this
A local model is insurance. I don't run Qwen locally because it's better than Opus. For most work, I still use the cloud model. But the cloud model can stop on you for reasons you don't control: a classifier, an outage, a rate limit, a policy change. When that happens halfway through patching live servers, "come back later" isn't a good option. A local model with the same tools, the same agent, and the same session history means you can keep going.
A few practical points if you want this setup:
- Use one agent framework for both. The handover only worked because Hermes kept the session and tools the same. The model changed and nothing else did.
- Keep the local model ready. Mine loads its 92 GiB of weights in under a minute, once I've stopped the other job on the GPU. Know your start script and test it now and then.
- Pick a model that handles long tool-calling sessions. A 200k-token agent context with dozens of shell calls is very different from a chat benchmark. In my own tests, Qwen3.8-Flash-Next with Strata was the fastest and most reliable model on this hardware.
-
Check the logs when something stops. "Your request was not processed" told me nothing.
finish_reason=content_filtertold me what had happened. Look for refusals separately from errors. - Verify the work the same way. Backups, linting, before-and-after HTTP checks, and rollback scripts did their job no matter which model wrote the patch.
I expect cloud models to stay strict in some areas, and that's their call to make. Security work on your own servers will always look a bit like attack work. Having a capable model on your own hardware means that decision doesn't stop your work.













