<?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: Harish Kumar</title>
    <description>The latest articles on DEV Community by Harish Kumar (@harish_kumar).</description>
    <link>https://dev.to/harish_kumar</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%2F201861%2F003407b2-31ea-47d7-b683-7ec9c30ca3ae.png</url>
      <title>DEV Community: Harish Kumar</title>
      <link>https://dev.to/harish_kumar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harish_kumar"/>
    <language>en</language>
    <item>
      <title>What I learned building a Claude Code plugin for MV3 Chrome extensions</title>
      <dc:creator>Harish Kumar</dc:creator>
      <pubDate>Sat, 13 Jun 2026 08:41:26 +0000</pubDate>
      <link>https://dev.to/harish_kumar/what-i-learned-building-a-claude-code-plugin-for-mv3-chrome-extensions-514i</link>
      <guid>https://dev.to/harish_kumar/what-i-learned-building-a-claude-code-plugin-for-mv3-chrome-extensions-514i</guid>
      <description>&lt;p&gt;Claude Code writes extension code fine. That was never the problem.&lt;/p&gt;

&lt;p&gt;Ask it for a content script that highlights matched text, or a background service worker that debounces a fetch, and it produces something reasonable on the first try. Where it trips is everything around the code: the Manifest V3 rules, the permission model, the content security policy, and the unwritten expectations of Chrome Web Store review. Those are the parts that fail late, at install, at build, or three weeks after you thought you shipped. And they fail quietly.&lt;/p&gt;

&lt;p&gt;I spent a while turning that gap into a plugin called Chrome Extension Builder. This is less a pitch for the plugin and more a writeup of the three things that broke while I built it, because each one taught me something I'd want to know if I were building any developer tool, plugin or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem
&lt;/h2&gt;

&lt;p&gt;A model that writes plausible code will also write a plausible manifest. The trouble is that "plausible" and "valid" diverge hard in MV3. A manifest with &lt;code&gt;content_security_policy.extension_pages&lt;/code&gt; containing &lt;code&gt;unsafe-eval&lt;/code&gt; looks fine to a generator that learned from years of MV2 examples. It is forbidden in MV3 and the extension will not load. The same goes for over-broad host permissions (&lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; when &lt;code&gt;activeTab&lt;/code&gt; would do), MV2 background pages instead of a service worker, and remote script in the CSP.&lt;/p&gt;

&lt;p&gt;So the design constraint was never "make Claude write extensions." It was: catch the MV3-specific mistakes deterministically, before they reach a human reviewer or a user's browser. That pushed the whole thing toward validators and hooks rather than cleverer prompts.&lt;/p&gt;

&lt;p&gt;The command surface ended up small on purpose. Five slash commands: &lt;code&gt;/chrome-ext:new&lt;/code&gt; runs an eight-phase guided scaffold; &lt;code&gt;/chrome-ext:validate&lt;/code&gt; runs the manifest, CSP, and permission checks; &lt;code&gt;/chrome-ext:add-feature&lt;/code&gt; wires in a popup or content script; &lt;code&gt;/chrome-ext:publish&lt;/code&gt; builds release artifacts; and &lt;code&gt;/chrome-ext:migrate-mv2&lt;/code&gt; walks an old extension forward. Three agents back them: an architect that designs but has no Bash, a read-only manifest auditor, and a test runner that can build, lint, and drive Playwright but cannot edit files. The capability boundaries are deliberate: the thing that audits your manifest physically cannot rewrite it.&lt;/p&gt;

&lt;p&gt;The defaults are opinionated. MV3 only, TypeScript, &lt;code&gt;activeTab&lt;/code&gt; over &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;, a strict CSP with no &lt;code&gt;unsafe-eval&lt;/code&gt;, no inline, no remote, &lt;code&gt;_locales&lt;/code&gt; for i18n, typed message passing, reproducible builds. WXT is the default framework, but not mandatory. Plasmo, CRXJS, and vanilla MV3 are all supported. I'll come back to why "default but not mandatory" matters, because the default framework is exactly what broke first.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story 1: a floating dependency drifted onto a breaking release
&lt;/h2&gt;

&lt;p&gt;WXT 0.20.26 removed the &lt;code&gt;wxt/sandbox&lt;/code&gt; export.&lt;/p&gt;

&lt;p&gt;My scaffold and skill docs imported &lt;code&gt;defineBackground&lt;/code&gt; and &lt;code&gt;defineContentScript&lt;/code&gt; from &lt;code&gt;wxt/sandbox&lt;/code&gt;, the way the docs showed when I wrote them. The dependency was pinned at &lt;code&gt;^0.20.0&lt;/code&gt;. So the day 0.20.26 published, a fresh scaffold started failing at &lt;code&gt;pnpm install&lt;/code&gt; (the &lt;code&gt;wxt prepare&lt;/code&gt; postinstall step choked) and again at &lt;code&gt;pnpm build&lt;/code&gt;, with &lt;code&gt;./sandbox is not exported&lt;/code&gt;. Nobody changed my code. The caret did.&lt;/p&gt;

&lt;p&gt;The fix was mechanical: import from &lt;code&gt;wxt/utils/define-background&lt;/code&gt; and &lt;code&gt;wxt/utils/define-content-script&lt;/code&gt;, and pin &lt;code&gt;~0.20.26&lt;/code&gt; instead of letting the caret float across a minor that turned out to carry a breaking change.&lt;/p&gt;

&lt;p&gt;The lesson is older than this plugin and I keep relearning it. A scaffolding tool's job is to emit code that compiles today and tomorrow. A floating range on a fast-moving dependency quietly delegates that promise to an upstream maintainer's versioning discipline. When the thing you generate is supposed to be a known-good starting point, pin it. The whole value of a scaffold is that it works on the first run; a caret can take that away without a single line of your own changing.&lt;/p&gt;

&lt;p&gt;This is also the clearest argument for "WXT default, not mandatory." Betting the entire tool on one framework's API stability is how one upstream release becomes your outage. Keeping Plasmo, CRXJS, and vanilla MV3 as real paths means a break in one default doesn't take everyone down with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story 2: the validator that lies (politely)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude plugin validate&lt;/code&gt; passes manifests that the runtime loader then rejects.&lt;/p&gt;

&lt;p&gt;I hit this twice. Once with a &lt;code&gt;userConfig&lt;/code&gt; field that included an &lt;code&gt;enum&lt;/code&gt; key: &lt;code&gt;validate&lt;/code&gt; was happy, install was not. Once with a &lt;code&gt;hooks.json&lt;/code&gt; that was missing its outer &lt;code&gt;"hooks"&lt;/code&gt; wrapper. Again: &lt;code&gt;validate&lt;/code&gt; green, install red. Both times I'd run the validator, seen it pass, committed, and only found out at the real install step that the CLI validator is a strict subset of the runtime schema. It checks for a class of errors. It does not check for all of them.&lt;/p&gt;

&lt;p&gt;The fix wasn't to argue with the validator. It was to stop trusting it as the source of truth. I added a CI job that runs the actual &lt;code&gt;claude plugin install&lt;/code&gt; against the built plugin, because the only ground truth for "does this load" is loading it. (I also filed the divergence upstream.)&lt;/p&gt;

&lt;p&gt;The same trap waits in any toolchain: a validator is a model of correctness, and every model is incomplete. If a green check from a linter or schema validator is your release gate, you are gating on the model, not on reality. Where it's cheap to run the real thing (an actual install, an actual boot, an actual build), make that the gate and let the fast validator be the early warning, not the verdict.&lt;/p&gt;

&lt;p&gt;Here's where the project's own validators sit on the other side of that line: they run against real manifests, not a schema's idea of one. A clean run looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Summary ─ critical: 0, warnings: 0 ──
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when I deliberately feed it an MV3 manifest with &lt;code&gt;unsafe-eval&lt;/code&gt; in the CSP, it catches it and exits non-zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRITICAL  content_security_policy.extension_pages  contains 'unsafe-eval'. Forbidden in MV3.
── CSP validation: critical=1 ──
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;critical=1&lt;/code&gt; isn't cosmetic. The PostToolUse hook runs these validators on every manifest write and exits 2 on a critical finding, so a generated manifest with a forbidden CSP fails the write instead of sailing through to a build. There's a PreToolUse hook too, which blocks &lt;code&gt;chrome-webstore-upload-cli --auto-publish&lt;/code&gt; unless &lt;code&gt;CONFIRM_PUBLISH_LIVE=1&lt;/code&gt; is set, so the tool does not push a live store release by accident. And a UserPromptSubmit hook nudges when it sees MV2 mentions, since "convert my MV2 extension" is where a lot of the forbidden-API mistakes originate.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story 3: don't force-push a repo something else pins by SHA
&lt;/h2&gt;

&lt;p&gt;This one cost me three weeks and I didn't notice for most of them.&lt;/p&gt;

&lt;p&gt;The community marketplace pins each plugin to a specific commit SHA and auto-bumps that pin over time. I force-pushed my repo (to fix a commit author identity, of all things), and that force-push orphaned the exact commit the marketplace had pinned. The auto-bump, finding its anchor gone, silently skipped my plugin. For about three weeks the published install served a stale, broken version. No error surfaced to me. The CI was green. My local was fine. The only people seeing the problem were the people installing it, and I wasn't one of them.&lt;/p&gt;

&lt;p&gt;The lesson is specific and I'll state it plainly: if an external system pins your commits by SHA, your history is now an API. Rewriting it is a breaking change to a consumer you can't see. Force-push is a local-feeling operation with a remote, invisible blast radius. The author-identity cleanup I wanted was not worth orphaning a pinned commit; a fresh commit on top would have cost nothing.&lt;/p&gt;

&lt;p&gt;More generally: the failure modes that hurt most are the ones with no error message. A red build you fix in an hour. A silently-skipped auto-bump you find when someone mentions in passing that the install "doesn't work for them." Build the alarm for the silent failures first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest non-goals
&lt;/h2&gt;

&lt;p&gt;I want to be precise about what this does not do, because the failures above made me allergic to overpromising.&lt;/p&gt;

&lt;p&gt;It does not guarantee Chrome Web Store approval. Review is done by humans against policy, and no validator predicts a reviewer. It does not replace the WXT, Plasmo, or CRXJS docs; it leans on them and points you at them, it is not a substitute for reading them. It does not make unsafe permissions acceptable; it makes them visible and harder to ship by accident, which is not the same thing. And it does not publish a live store release by accident; that's the whole point of the confirmation gate.&lt;/p&gt;

&lt;p&gt;What I actually learned, across all three stories, is that the hard part of a code-generation tool isn't the generation. It's the verification, the pinning, and the boring discipline around the seams where your tool meets someone else's system. The model writes the content script. The work is making sure the manifest around it survives install, build, store review, and the next upstream release.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it or break it
&lt;/h2&gt;

&lt;p&gt;The plugin is MIT and lives at &lt;a href="https://github.com/harry-harish/chrome-extension-builder" rel="noopener noreferrer"&gt;github.com/harry-harish/chrome-extension-builder&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;/plugin marketplace add anthropics/claude-plugins-community
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;chrome-extension-builder@claude-community
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's new, so I'm more interested in real-repo feedback than stars. If you run &lt;code&gt;/chrome-ext:new&lt;/code&gt; on a real extension and it generates something that won't install, or &lt;code&gt;/chrome-ext:validate&lt;/code&gt; misses a CSP problem it should have caught, open an issue with the manifest. The validators only get better against manifests that actually broke, and after war story two, I trust real failures more than green checks.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>chromeextensions</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What I'm building, and why</title>
      <dc:creator>Harish Kumar</dc:creator>
      <pubDate>Wed, 03 Jun 2026 09:57:16 +0000</pubDate>
      <link>https://dev.to/harish_kumar/what-im-building-and-why-4o3n</link>
      <guid>https://dev.to/harish_kumar/what-im-building-and-why-4o3n</guid>
      <description>&lt;p&gt;I've shipped two OSS products this month: &lt;strong&gt;tracelane&lt;/strong&gt; and &lt;strong&gt;peek&lt;/strong&gt;. They share a recording engine and a trust model. Neither has a SaaS, a dashboard, or a signup. Both work fully offline. This post is the why.&lt;/p&gt;

&lt;h2&gt;
  
  
  tracelane
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The reporter for your WebdriverIO, Playwright, and Cypress tests. Self-contained HTML for every run — replay failures, audit successes, attach to any bug tracker. No SaaS, no dashboard, no signup.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have lost many afternoons to a CI line that says "Element not visible: &lt;code&gt;[data-test=submit]&lt;/code&gt;" and nothing else. The screenshot tells me the page is white. The console log is empty because the assertion fired before the app got to throw anything. The video, if there is one, is hosted on a vendor I'd rather not pay for, behind an auth wall I'd rather not maintain, and gone in 30 days.&lt;/p&gt;

&lt;p&gt;The fix is not novel. &lt;a href="https://www.rrweb.io" rel="noopener noreferrer"&gt;rrweb&lt;/a&gt; records the DOM and console at usable fidelity. The novel part is the constraint I want to enforce: the resulting artifact must be a &lt;strong&gt;single &lt;code&gt;.html&lt;/code&gt; file on disk&lt;/strong&gt; that opens in any browser, fully offline, with the player and event blob inlined. No cloud upload. No signup. Attach it to a Jira ticket, drop it in Slack, archive it in S3, send it to a contractor outside your network — it's just a file.&lt;/p&gt;

&lt;p&gt;Tracelane is one WebdriverIO &lt;strong&gt;Service&lt;/strong&gt; that injects rrweb, drains the in-page buffer on a poll, attaches CDP for failed-network capture, and builds the HTML when a test fails. The Playwright and Cypress integrations follow the same pattern; they ship later in the year.&lt;/p&gt;

&lt;p&gt;The closest commercial equivalents are Cypress Cloud, Replay.io, and Sentry Session Replay. They're all good products. I just didn't want to operate infrastructure for a side project, and the self-contained HTML constraint genuinely changes the shape of "where can this artifact live?" — your bug tracker doesn't need to learn about a new vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  peek
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Your real browser, exposed to your AI coding agent over MCP — capture once, query forever, never leaves your machine.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code, Cursor, Cline, Windsurf — they're all blocked on the same thing: they don't know what's actually in the browser tab I'm asking them about. They can read the source on my disk. They can read documentation. They cannot see the rendered DOM, the network panel, or the &lt;code&gt;console.error&lt;/code&gt; that just fired in the iframe I'm wrestling with. When I describe a bug in plain English they reconstruct what I see from text alone, which is the conversational equivalent of fixing a JavaScript bug over a phone call.&lt;/p&gt;

&lt;p&gt;Peek is a Chrome MV3 extension plus a stdio MCP server. You enable it per-origin from the side panel (off by default for every site). It records via the same &lt;code&gt;@cubenest/rrweb-core&lt;/code&gt; substrate tracelane uses, writes into a local SQLite DB at &lt;code&gt;~/.peek/sessions.db&lt;/code&gt;, and exposes ~20 read-only tools to your AI client over MCP. Your agent can now ask: &lt;em&gt;"console errors from the last 10 seconds"&lt;/em&gt;, &lt;em&gt;"network requests with status &amp;gt;= 400 on &lt;code&gt;example.com&lt;/code&gt;"&lt;/em&gt;, &lt;em&gt;"reconstruct the DOM at the timestamp the click happened"&lt;/em&gt;. Write operations (clicks, inputs, navigation) exist but require explicit per-action authorization recorded in an audit log.&lt;/p&gt;

&lt;p&gt;There is no remote. The MCP transport is stdio. Your agent launches &lt;code&gt;peek-mcp&lt;/code&gt; as a child process, talks to it over stdin/stdout, kills it on exit. Captures live in your home directory until you delete them. The Chrome Web Store submission is pending; alpha testers load the extension unpacked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one repo, two packages, one fork
&lt;/h2&gt;

&lt;p&gt;Both products record. The recorder is the same. So I forked PostHog's well-maintained rrweb lineage into &lt;a href="https://www.npmjs.com/package/@cubenest/rrweb-core" rel="noopener noreferrer"&gt;&lt;code&gt;@cubenest/rrweb-core&lt;/code&gt;&lt;/a&gt;, pinned to a specific commit, with the masking primitives and screenshot fallback I needed already in place. PostHog's fork is ahead of upstream on the things I cared about — masking, large-DOM throttling — and behind on a few things I don't. The fork is vendored (not a transitive npm dep) because the Shai-Hulud 2.0 supply-chain wave in late 2025 made me reconsider the cost of every transitive dep that touches user DOM. One pinned SHA, one audit surface, two products downstream.&lt;/p&gt;

&lt;p&gt;Two products in one repo because the recorder is the load-bearing piece. Splitting them at this stage would mean syncing the fork across two repos manually. When they each justify their own release cadence, they can split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest pre-1.0 disclosure
&lt;/h2&gt;

&lt;p&gt;This is alpha. Every package version starts with &lt;code&gt;0.1.0-alpha.&lt;/code&gt; The API may shift. Branch protection is on &lt;code&gt;main&lt;/code&gt; (PR + 1 review + CI + DCO + linear history). Every publish goes through npm Trusted Publishing OIDC and ships with SLSA provenance. Renovate runs with a 7-day cooldown (21 days for the &lt;code&gt;@posthog/rrweb&lt;/code&gt; lineage). &lt;a href="https://scorecard.dev/viewer/?uri=github.com/Cubenest/rrweb-stack" rel="noopener noreferrer"&gt;OpenSSF Scorecard&lt;/a&gt; runs weekly. I'm one person; the sustainability budget is documented at &lt;a href="https://github.com/Cubenest/rrweb-stack/blob/main/docs/SUSTAINABILITY.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/SUSTAINABILITY.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tracelane/cli init  &lt;span class="c"&gt;# WebdriverIO project: install + wire in one command&lt;/span&gt;
npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @peekdev/cli    &lt;span class="c"&gt;# peek: CLI install, then `peek init` wires the MCP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache 2.0. DCO sign-off on contributions. No telemetry from either tool. Issues + PRs at &lt;a href="https://github.com/Cubenest/rrweb-stack" rel="noopener noreferrer"&gt;&lt;code&gt;Cubenest/rrweb-stack&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you find these useful, &lt;a href="https://github.com/sponsors/harry-harish" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt; keeps them maintained at the cadence I can sustain alongside a day job.&lt;/p&gt;

&lt;p&gt;— Harish&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>testing</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
