<?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: Baz</title>
    <description>The latest articles on DEV Community by Baz (@bazzz).</description>
    <link>https://dev.to/bazzz</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%2F3803374%2F492ac010-b5f7-4922-9f67-9302434c7a76.png</url>
      <title>DEV Community: Baz</title>
      <link>https://dev.to/bazzz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bazzz"/>
    <language>en</language>
    <item>
      <title>Your MCP server didn't break. The protocol did.</title>
      <dc:creator>Baz</dc:creator>
      <pubDate>Fri, 14 Aug 2026 19:01:30 +0000</pubDate>
      <link>https://dev.to/bazzz/your-mcp-server-didnt-break-the-protocol-did-3abp</link>
      <guid>https://dev.to/bazzz/your-mcp-server-didnt-break-the-protocol-did-3abp</guid>
      <description>&lt;p&gt;I updated my editor last week and three MCP servers stopped working. Same error each time:&lt;br&gt;
&lt;/p&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="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;-32601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Method not found: initialize"&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;I spent about forty minutes assuming I'd broken something. I hadn't. Neither had the server authors. The spec changed underneath all of us.&lt;/p&gt;

&lt;p&gt;If you got here from googling &lt;code&gt;-32601 method not found mcp&lt;/code&gt; or &lt;code&gt;initialize method not found&lt;/code&gt; or you're just staring at a server that worked fine on Friday, this is what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;MCP revision &lt;code&gt;2026-07-28&lt;/code&gt; made the protocol stateless. Not "added a stateless mode". Made it stateless, and deleted the parts that assumed otherwise.&lt;/p&gt;

&lt;p&gt;Gone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialize&lt;/code&gt; and &lt;code&gt;notifications/initialized&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ping&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logging/setLevel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resources/subscribe&lt;/code&gt; and &lt;code&gt;resources/unsubscribe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notifications/roots/list_changed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;server-initiated requests entirely, which means &lt;code&gt;sampling/createMessage&lt;/code&gt;, &lt;code&gt;elicitation/create&lt;/code&gt; and &lt;code&gt;roots/list&lt;/code&gt; no longer work the way they did&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every MCP server written before mid-2026 relies on at least the first item. That's why yours broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the errors actually mean
&lt;/h2&gt;

&lt;p&gt;Here's the mapping, because the error text on its own is not very helpful.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-32601 method not found&lt;/code&gt; on &lt;code&gt;initialize&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Your client is modern, your server is not. The client never sent a handshake because there is no handshake anymore. Instead it sends &lt;code&gt;server/discover&lt;/code&gt;, which your server has never heard of, and then the reverse happens: your server waits for an &lt;code&gt;initialize&lt;/code&gt; that never comes.&lt;/p&gt;

&lt;p&gt;This is the single most common symptom.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-32602&lt;/code&gt; with "missing required request metadata"
&lt;/h3&gt;

&lt;p&gt;Because there's no session, every single request now has to carry its own context. It goes in &lt;code&gt;params._meta&lt;/code&gt;:&lt;br&gt;
&lt;/p&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"_meta"&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;"io.modelcontextprotocol/protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-28"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"io.modelcontextprotocol/clientCapabilities"&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;"roots"&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="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="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;Protocol version and client capabilities on every request. Not once at startup. Every request. If you're writing a client by hand and skipped this, that's your error.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-32022 unsupported protocol version&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The version in that &lt;code&gt;_meta&lt;/code&gt; block didn't match. Worth knowing that the error codes got renumbered in this revision too, so old code checking for &lt;code&gt;-32001&lt;/code&gt; will silently stop matching. &lt;code&gt;-32020&lt;/code&gt; is header mismatch, &lt;code&gt;-32021&lt;/code&gt; is a missing client capability, &lt;code&gt;-32022&lt;/code&gt; is the version one.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-32021&lt;/code&gt; and a &lt;code&gt;requiredCapabilities&lt;/code&gt; object
&lt;/h3&gt;

&lt;p&gt;The server needed something the client didn't declare. Since capabilities now arrive per request instead of being negotiated once, a client that forgets to declare &lt;code&gt;sampling&lt;/code&gt; will get this the moment a tool tries to use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your server hangs and never answers
&lt;/h3&gt;

&lt;p&gt;If your server calls back to the client mid-request (an elicitation prompt, a sampling call, asking for roots), it's now waiting forever. Modern clients cannot receive pushes. There's nothing listening.&lt;/p&gt;

&lt;p&gt;This one is nastier than the others because there's no error at all. It just sits there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replacement for server-initiated requests
&lt;/h2&gt;

&lt;p&gt;This is the part I found genuinely clever, and it's worth understanding even if you never write a server.&lt;/p&gt;

&lt;p&gt;Old world: server interrupts its own call, asks the client something, waits for an answer, continues.&lt;/p&gt;

&lt;p&gt;New world: the server can't push, so it returns early with a result that says "I need input", and the client calls the same thing again with the answers attached.&lt;br&gt;
&lt;/p&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;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"input_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputRequests"&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;"ir_1000"&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"elicitation/create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"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;"Which environment?"&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="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;"requestState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0f3a...e21"&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;The client answers by re-sending the original request with &lt;code&gt;requestState&lt;/code&gt; and an &lt;code&gt;inputResponses&lt;/code&gt; map keyed the same way. The call picks up where it left off. It's called a multi round-trip request, MRTR if you read the SEPs.&lt;/p&gt;

&lt;p&gt;The bit that catches people: the values in &lt;code&gt;inputResponses&lt;/code&gt; are the response body itself, not wrapped in a &lt;code&gt;result&lt;/code&gt; field. I got that wrong the first three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notifications moved too
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resources/subscribe&lt;/code&gt; is gone. Change notifications now live on a single long-lived stream you opt into, and you name the types you want:&lt;br&gt;
&lt;/p&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.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;"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;"listen-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscriptions/listen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"notifications"&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;"toolsListChanged"&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;"resourceSubscriptions"&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;"file:///project/config.json"&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="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;Two things here are easy to miss. The request stays open, and its JSON-RPC id doubles as the subscription id that gets stamped on every notification. And the server must not send you anything you didn't ask for.&lt;/p&gt;

&lt;p&gt;Progress and logging notifications have no home on that stream, by the way. They belong to an in-flight request, and a stateless request/response shape has nowhere to put them. If you were relying on &lt;code&gt;notifications/progress&lt;/code&gt; for a progress bar, that's a real loss and there isn't a workaround yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what do you actually do
&lt;/h2&gt;

&lt;p&gt;Three options, in the order I'd try them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update the server.&lt;/strong&gt; If you wrote it, or it's actively maintained, this is the right answer. The official SDKs handle most of the migration for you, and the TypeScript SDK ships a compatibility shim so handlers written in the new style still serve old clients. Check if there's a newer version before doing anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin your client.&lt;/strong&gt; Buys you time, costs you everything else in the update. Fine for a week, bad as a plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrap the server.&lt;/strong&gt; This is the option nobody talks about, and it's the one I needed, because two of my three broken servers hadn't been touched in over a year and one was a vendor binary I don't have source for. No amount of "just update it" helps there.&lt;/p&gt;

&lt;p&gt;I ended up writing the wrapper, so treat the rest of this as biased. It's called &lt;a href="https://github.com/MohibShaikh/mcp-uplift" rel="noopener noreferrer"&gt;mcp-uplift&lt;/a&gt;. You point your client at it instead of at the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; mcp-uplift &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @modelcontextprotocol/server-filesystem &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It keeps one legacy session warm behind the scenes and does the translation: synthesizes &lt;code&gt;server/discover&lt;/code&gt; from the old handshake, turns server-initiated requests into &lt;code&gt;input_required&lt;/code&gt; round trips, filters legacy notifications onto a &lt;code&gt;subscriptions/listen&lt;/code&gt; stream, and answers the deleted methods itself instead of forwarding them.&lt;/p&gt;

&lt;p&gt;The server doesn't change. It doesn't even know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it work
&lt;/h2&gt;

&lt;p&gt;Fair question, since a protocol translator that's subtly wrong is worse than nothing.&lt;/p&gt;

&lt;p&gt;I didn't trust my own test suite, because I wrote both sides of it and it only proves I'm internally consistent. So I ran it against servers I didn't write: 79 published legacy MCP packages, on a clean CI runner, checking the whole lifecycle each time. Discovery, subscription acknowledgement, the acknowledged filter never claiming a capability the server didn't declare, no response arriving while the stream is open, and a graceful close on shutdown.&lt;/p&gt;

&lt;p&gt;79 reached discovery, zero protocol failures, 36 completed a full subscription lifecycle. The &lt;a href="https://github.com/MohibShaikh/mcp-uplift/actions" rel="noopener noreferrer"&gt;run is public&lt;/a&gt; if you want to read the log rather than take my word.&lt;/p&gt;

&lt;p&gt;I deliberately dropped every package that needs an API key. They stop at the missing credential, never exercise the bridge, and only make the number look bigger. 79 real ones beat 100 with a fifth of them unreachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things it can't do
&lt;/h2&gt;

&lt;p&gt;Because I'd rather you find out here than after adopting it.&lt;/p&gt;

&lt;p&gt;Calls are serialized. A legacy server can interrupt any call to ask the client something, and the old protocol never linked that question back to the call that caused it, so only one call runs upstream at a time. Correct attribution, worse throughput.&lt;/p&gt;

&lt;p&gt;Progress and logging notifications are dropped, for the reason above. Nothing to be done about that one.&lt;/p&gt;

&lt;p&gt;Parked calls don't survive a restart, because each one is waiting on a child process that dies with the bridge.&lt;/p&gt;

&lt;p&gt;And wrapping a server runs that server with your permissions. It's a compatibility layer, not a sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deadline
&lt;/h2&gt;

&lt;p&gt;Roots, sampling and logging are deprecated with about twelve months of runway. That's the window. After it, "just update it" stops being optional and wrapping stops being a bridge and starts being life support.&lt;/p&gt;

&lt;p&gt;Update what you can. Wrap what you can't. Don't pin your client and forget about it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you hit an error I didn't cover, drop it in the comments and I'll add it. The list above is from servers that actually broke, not from reading the changelog.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a 6-pass security scanner for OpenClaw skills after 824 malicious ones were found on ClawHub</title>
      <dc:creator>Baz</dc:creator>
      <pubDate>Tue, 03 Mar 2026 09:13:27 +0000</pubDate>
      <link>https://dev.to/bazzz/i-built-a-6-pass-security-scanner-for-openclaw-skills-after-824-malicious-ones-were-found-on-clawhub-2n05</link>
      <guid>https://dev.to/bazzz/i-built-a-6-pass-security-scanner-for-openclaw-skills-after-824-malicious-ones-were-found-on-clawhub-2n05</guid>
      <description>&lt;p&gt;In February 2026, security researchers discovered that roughly 20% of skills on ClawHub — the marketplace for OpenClaw AI agent plugins — were malicious. The "ClawHavoc" campaign had been silently distributing infostealers disguised as productivity tools. Skills that claimed to manage your calendar were actually exfiltrating your API keys, SSH credentials, and browser data to C2 servers.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;clawvet&lt;/strong&gt; to solve this. It's a CLI tool that runs 6 independent analysis passes on any OpenClaw &lt;code&gt;SKILL.md&lt;/code&gt; file before you install it.&lt;/p&gt;

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

&lt;p&gt;Most existing scanners run a single pass of regex matching. clawvet runs six:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pass&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skill Parser&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extracts YAML frontmatter, code blocks, URLs, IPs, domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static Analysis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;54 regex patterns across 12 categories (RCE, credential theft, reverse shells, DNS exfil, obfuscation, prompt injection)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metadata Validator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flags undeclared binaries, env vars, missing descriptions, bad semver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency Checker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detects &lt;code&gt;npx -y&lt;/code&gt; auto-install, global npm installs, risky packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typosquat Detector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Levenshtein distance against popular skills catches name impersonation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Analysis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Optional) Claude AI analyzes instructions for social engineering and hidden functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Scan a local skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx clawvet scan ./suspicious-skill/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON output for CI/CD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx clawvet scan ./my-skill &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What a scan looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ClawVet Scan Report

  Skill:   productivity-boost
  Version: 1.0.0
  Risk Score: 100/100  Grade: F

  [CRITICAL] Curl piped to shell
    curl -sL https://...setup.sh | bash

  [HIGH] Known malicious IP
    91.92.242.15

  [HIGH] API key exfiltration
    ANTHROPIC_API_KEY → webhook.site

  Recommendation: BLOCK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why 6 passes matter
&lt;/h2&gt;

&lt;p&gt;The ClawHavoc skills were clever. They passed basic regex checks by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Splitting malicious commands across multiple code blocks&lt;/li&gt;
&lt;li&gt;Using base64 encoding and hex payloads&lt;/li&gt;
&lt;li&gt;Hiding C2 IPs in YAML metadata fields&lt;/li&gt;
&lt;li&gt;Using typosquatted names (&lt;code&gt;todoistt&lt;/code&gt; instead of &lt;code&gt;todoist-cli&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Embedding prompt injection to make the AI agent run commands the user didn't ask for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single analysis technique catches all of these. The 6-pass approach means each layer catches what the others miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;54&lt;/strong&gt; static detection patterns (reverse shells, DNS exfil, credential theft, obfuscation, prompt injection, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61&lt;/strong&gt; tests (unit, integration, regex safety, CLI end-to-end)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12&lt;/strong&gt; threat categories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6&lt;/strong&gt; test fixtures from benign to fully malicious&lt;/li&gt;
&lt;li&gt;Catastrophic backtracking protection on all regex patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use it in CI/CD
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Vet skill before merge&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx clawvet scan ./my-skill --format json --fail-on high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;p&gt;The full source is on&lt;br&gt;
GitHub: &lt;a href="https://github.com/MohibShaikh/clawvet" rel="noopener noreferrer"&gt;https://github.com/MohibShaikh/clawvet&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/clawvet" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/clawvet&lt;/a&gt;&lt;br&gt;
ClawHub: &lt;a href="https://clawhub.ai/MohibShaikh/clawvet" rel="noopener noreferrer"&gt;https://clawhub.ai/MohibShaikh/clawvet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; clawvet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using OpenClaw, I'd appreciate it if you gave it a try and let me know what you think. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>ai</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
