<?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: Max Schottke</title>
    <description>The latest articles on DEV Community by Max Schottke (@max_schottke_6e27c7a80171).</description>
    <link>https://dev.to/max_schottke_6e27c7a80171</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3945644%2F1ccda7aa-9845-49a4-9979-f3b1091abe9a.jpg</url>
      <title>DEV Community: Max Schottke</title>
      <link>https://dev.to/max_schottke_6e27c7a80171</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/max_schottke_6e27c7a80171"/>
    <language>en</language>
    <item>
      <title>Building a Claude Code plugin with zero npm dependencies</title>
      <dc:creator>Max Schottke</dc:creator>
      <pubDate>Fri, 22 May 2026 08:29:44 +0000</pubDate>
      <link>https://dev.to/max_schottke_6e27c7a80171/building-a-claude-code-plugin-with-zero-npm-dependencies-2m2h</link>
      <guid>https://dev.to/max_schottke_6e27c7a80171/building-a-claude-code-plugin-with-zero-npm-dependencies-2m2h</guid>
      <description>&lt;h1&gt;
  
  
  Building a Claude Code plugin with zero npm dependencies
&lt;/h1&gt;

&lt;p&gt;I shipped a Claude Code plugin last week called seo-survival-kit. It generates publication-quality SEO outreach PDFs by pulling data from three APIs (Sistrix, DataForSEO, Google PageSpeed Insights) and rendering everything through headless Chrome. The whole thing is four Node.js files plus a small shared library, around 1000 lines of source. No package.json. No node_modules. No Puppeteer.&lt;/p&gt;

&lt;p&gt;This is the design log. Why no dependencies, how the Chrome render works without Puppeteer, what I learned about Claude Code plugin security, and the two bugs I only caught after running against a real domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dependency rule
&lt;/h2&gt;

&lt;p&gt;Every npm dependency in a Claude Code plugin is a supply-chain attack surface. The plugin runs in the user's shell with their privileges. One compromised transitive dep, one tampered postinstall script, and you've shipped a credential-stealer to everyone who installed your plugin.&lt;/p&gt;

&lt;p&gt;I made it a hard rule for this project. The plugin runs on raw Node 20 with whatever ships in node:fs, node:path, node:child_process, node:os. No package.json, so no install step, so no npm to compromise. The user reads a thousand lines of source and that's the full attack surface they're trusting.&lt;/p&gt;

&lt;p&gt;This sounds restrictive in 2026. Mostly it wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing Puppeteer with three Chrome flags
&lt;/h2&gt;

&lt;p&gt;Most "render HTML to PDF" pipelines reach for Puppeteer, which pulls down 100MB of Chromium plus a couple of dependency chains. I needed two things from Puppeteer: launch Chrome, give it an HTML file, get a PDF. Both are command-line flags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawnSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CHROME_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--headless=new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--disable-gpu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-default-browser-check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-first-run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`--user-data-dir=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome-profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`--print-to-pdf=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pdfPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-pdf-header-footer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`file://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tmpHtmlPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shell&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;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things in this snippet matter beyond "I called Chrome."&lt;/p&gt;

&lt;p&gt;The first version of this code used execSync with a string command. That made CHROME_PATH (which I read from env to allow override) a shell-injection vector. Anyone who could set the env var could append a semicolon and arbitrary shell. Switching to spawnSync with an argv array kills that. Node passes each array entry to the OS as a literal argv element. Quoting, $VAR expansion, semicolons, backticks all become impossible.&lt;/p&gt;

&lt;p&gt;The --user-data-dir flag points at an isolated directory I create per run with fs.mkdtempSync. Without it, headless Chrome opens with the user's real profile. Your cookies, extensions, saved passwords are all in scope during the render. With the isolated dir, none of that loads.&lt;/p&gt;

&lt;p&gt;The HTML carries a Content-Security-Policy meta tag. Chrome rendering file:// URLs has read access to the local filesystem. If a malicious string slips into my HTML and emits &lt;code&gt;&amp;lt;iframe src="file:///etc/passwd"&amp;gt;&lt;/code&gt;, Chrome will obediently render the file's contents into the iframe, and that ends up in my PDF. The fix is a CSP that refuses to load iframes and external resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt;
      &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"default-src 'none'; style-src 'unsafe-inline';
               img-src data:; font-src data:;
               base-uri 'none'; form-action 'none';
               frame-src 'none'; object-src 'none'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if my HTML escaper has a bug, Chrome refuses to load the iframe. Two layers of defense for the cost of one meta tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path validation everywhere, not just one place
&lt;/h2&gt;

&lt;p&gt;The plugin reads an audit-config.json with target entries like &lt;code&gt;{"slug": "client-a", "domain": "example.com"}&lt;/code&gt;. The slug becomes part of every cache filename and intermediate path. So what happens if slug equals "../../etc/foo"?&lt;/p&gt;

&lt;p&gt;The check is one regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9_-&lt;/span&gt;&lt;span class="se"&gt;]{0,63}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsafe slug rejected: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is calling it at every file boundary, not just one. My first version validated only in the PDF-render script. The security review caught that the two upstream scripts (the extract and the on-page parser) accepted raw slugs straight into path interpolations. A crafted config could write files outside the cache dir. After that I pulled safeSlug into a shared lib/safe.js and called it at every config load.&lt;/p&gt;

&lt;h2&gt;
  
  
  /tmp is a trap on macOS
&lt;/h2&gt;

&lt;p&gt;The first version cached intermediate JSON files in /tmp. The pattern was /tmp/seo-${slug}-summary.json. On macOS, /tmp is world-writable (mode 1777). If you write to a predictable path there and another local process pre-creates that path as a symlink to ~/.ssh/authorized_keys, your fs.writeFileSync follows the symlink and overwrites the target. Done.&lt;/p&gt;

&lt;p&gt;I moved cache to ~/.cache/seo-rescue/, mode 0700, refused if the path itself is a symlink:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCacheDir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEO_CACHE_DIR&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
              &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;homedir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;seo-rescue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mo"&gt;0o700&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chmodSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0o700&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstatSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isSymbolicLink&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SEO_CACHE_DIR must not be a symlink: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For intermediate HTML that Chrome reads, I create a per-run mkdtempSync directory under $TMPDIR, write the HTML there, render, then rmSync it in a finally block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs I missed until live-testing
&lt;/h2&gt;

&lt;p&gt;I shipped v0.2.0 after a clean smoke test with mock data. Then I ran the pipeline against a real domain. Two things broke.&lt;/p&gt;

&lt;p&gt;The position distribution chart in the PDF was always nearly 100% Top 3. The DataForSEO ranked_keywords call used limit:100 with order_by:rank_group asc. So the 100 returned items were exactly the 100 best-ranking keywords. The chart was meant to show how keywords spread across Top 3, 4-10, 11-20, 21-50, 51-100. With that fetch it was lying. The fix was limit:1000 with order_by:search_volume desc, then re-sort locally for the Top-Keywords table. Real distribution went from &lt;code&gt;{ t3:100, t10:0, t20:0, t50:0, t100:0 }&lt;/code&gt; to &lt;code&gt;{ t3:101, t10:226, t20:203, t50:376, t100:94 }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The target domain was also listed as its own #1 competitor. DataForSEO's competitors_domain endpoint returns the target with 100% SERP overlap as the first item. Trivial to filter, painful to discover via embarrassed re-read of the generated PDF.&lt;/p&gt;

&lt;p&gt;Neither bug was caught by my mock tests because the mocks returned clean, target-free, evenly-distributed data. There's a useful lesson in that, but mostly it's just "run against real data sooner."&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping a wrong manifest path for two releases
&lt;/h2&gt;

&lt;p&gt;A separate kind of bug I want to mention because it cost me a release cycle. The first two tagged releases (v0.2.0 and v0.2.1) had the plugin manifest at plugins/seo-rescue/plugin.json. Claude Code expects it at plugins/seo-rescue/.claude-plugin/plugin.json. The plugin was never installable from the published marketplace.json. The marketplace.json metadata pointed to the plugin folder. The folder existed. The manifest existed. Just not at the path the loader checks first.&lt;/p&gt;

&lt;p&gt;Anthropic ships claude plugin validate as a CLI command. Running it would have caught the path issue immediately. I just hadn't run it. v0.2.2 is the first release where /plugin install actually works, which is humbling to write in release notes.&lt;/p&gt;

&lt;p&gt;The lesson is to run the platform's validator before tagging. Specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin validate plugins/seo-rescue
claude plugin validate &lt;span class="nb"&gt;.&lt;/span&gt;  &lt;span class="c"&gt;# validates marketplace.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both passing is now a release prerequisite for this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills are domain knowledge, not just code
&lt;/h2&gt;

&lt;p&gt;This is the part Claude Code's plugin model enables that I didn't expect to use as much as I did. Two of the six skills in seo-survival-kit are pure Markdown. No scripts, no automation, just frameworks. The post-core-update-recovery skill is 152 lines of Markdown that encode a diagnose decision tree and a 4-phase recovery plan I built from one extended real recovery case. Claude reads it, applies it to whatever specific situation the user describes, and produces tailored output.&lt;/p&gt;

&lt;p&gt;You're not shipping code, you're shipping a way of thinking about a problem. The code is the optional automation around it. That separation actually makes the plugin more useful for SEO consultants who want to read the framework without installing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo and install
&lt;/h2&gt;

&lt;p&gt;Tag-pinned install is the recommended default in the README, to protect installers from any future maintainer-account compromise. v0.2.2 is the first installable release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add maxschottke-spec/seo-survival-kit#v0.2.2
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;seo-rescue@seo-survival-kit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/maxschottke-spec/seo-survival-kit" rel="noopener noreferrer"&gt;https://github.com/maxschottke-spec/seo-survival-kit&lt;/a&gt;&lt;br&gt;
License: MIT&lt;br&gt;
Source: ~1000 lines across four scripts + lib/safe.js&lt;br&gt;
Runtime dependencies: zero&lt;/p&gt;

</description>
      <category>seo</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>claudecode</category>
    </item>
  </channel>
</rss>
