# Mv3 Doctor

> Use when building, modifying, migrating, or reviewing a Chrome extension — anything involving manifest.json, a background service worker, content scripts, or Manifest V3. Encodes the MV3 rules that models routinely get wrong, and ships a validator to check the result.

- **Type:** Skill
- **Install:** `agentstack add skill-n0liu-mv3-doctor-mv3-doctor`
- **Verified:** Pending review
- **Seller:** [n0liu](https://agentstack.voostack.com/s/n0liu)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [n0liu](https://github.com/n0liu)
- **Source:** https://github.com/n0liu/mv3-doctor

## Install

```sh
agentstack add skill-n0liu-mv3-doctor-mv3-doctor
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Building Chrome Manifest V3 Extensions

Manifest V3 broke assumptions that a decade of extension tutorials — and most
training data — still take for granted. Code that looks correct, passes review
by eye, and even runs during a quick manual test will fail in production because
the service worker was still warm when you tested it.

Follow the rules below while writing, then **run the validator before you claim
the work is done.**

## The one idea that explains most MV3 bugs

**The background service worker is ephemeral.** Chrome starts it to deliver an
event and terminates it after roughly 30 seconds of inactivity. It is not a
long-running process. Every mistake below follows from forgetting this.

MV2 mental model (wrong): a background page that stays alive, holds state in
memory, keeps sockets open, and runs timers.

MV3 mental model (right): a collection of stateless event handlers. Each one
starts from nothing, reads what it needs from storage, does its work, and may be
killed the moment it goes idle.

## Rules

### 1. Never keep state in module-level variables

```js
// WRONG — resets to 0 without warning when the worker restarts
let requestCount = 0;
chrome.runtime.onMessage.addListener(() => { requestCount += 1; });

// RIGHT — state outlives the worker
chrome.runtime.onMessage.addListener(async () => {
  const { requestCount = 0 } = await chrome.storage.session.get("requestCount");
  await chrome.storage.session.set({ requestCount: requestCount + 1 });
});
```

`chrome.storage.session` is in-memory and cleared when the browser closes — good
for tokens. `chrome.storage.local` persists to disk. Both are async.

Module-level `const` for configuration is fine. The problem is *mutable* state.

### 2. Never use timers for anything beyond a few seconds

`setInterval` stops when the worker dies. A `setTimeout` longer than the ~30s
idle timeout never fires.

```js
// WRONG
setInterval(poll, 60_000);

// RIGHT — alarms wake the worker back up
chrome.alarms.create("poll", { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === "poll") poll();
});
```

Note `periodInMinutes` has a 30-second floor. If you need finer granularity than
that, reconsider the design — an extension probably should not be polling that
aggressively.

### 3. There is no DOM in a service worker

`document`, `window`, `localStorage`, `sessionStorage` and `alert` do not exist
and will throw. Use `self` for the global scope. When you genuinely need DOM
APIs (parsing HTML, canvas, audio), create an **offscreen document**:

```js
await chrome.offscreen.createDocument({
  url: "offscreen.html",
  reasons: ["DOM_PARSER"],
  justification: "Parse HTML returned by the API",
});
```

### 4. All code must ship inside the package

MV3 forbids remote code. No `importScripts("https://…")`, no
`import("https://…")`, no injecting a ``, no `eval()`,
no `new Function()`. Bundle dependencies at build time. A CSP with a remote
`script-src` or `'unsafe-eval'` is rejected at review.

Remote *data* (JSON from your API) is fine. Remote *code* is not.

### 5. Blocking webRequest is gone

Use `declarativeNetRequest`. Rules are declared up front so Chrome can apply
them without waking your worker:

```json
{
  "permissions": ["declarativeNetRequest"],
  "declarative_net_request": {
    "rule_resources": [{ "id": "ruleset", "enabled": true, "path": "rules.json" }]
  }
}
```

You can still *observe* requests with non-blocking `webRequest`, but you cannot
modify or cancel them that way.

### 6. Request the narrowest permissions you can justify

The most common Web Store rejections are permissions the extension does not
visibly use, and `` without justification. Prefer `activeTab`, or
request hosts at runtime with `chrome.permissions.request()`. Write the
justification text as if a reviewer who has never seen your extension is reading
it — because that is what happens.

### 7. Register listeners synchronously at the top level

A listener registered inside an `async` callback may not exist yet when the
worker is woken to handle that event.

```js
// WRONG — the listener may be registered too late
(async () => {
  await init();
  chrome.runtime.onMessage.addListener(handler);
})();

// RIGHT — register immediately, do async work inside the handler
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  handleAsync(message).then(sendResponse);
  return true; // keeps the message channel open for the async response
});
```

Returning `true` from an `onMessage` listener is required whenever you respond
asynchronously. Forgetting it is a very common, very confusing bug.

## Verify before finishing

Run the validator on the extension directory and fix every error:

```bash
npx mv3-doctor path/to/extension
```

Use `--json` when you need to parse the findings programmatically. Exit code is
`1` if any error was found, `0` otherwise (warnings do not fail the run).

Do not report the work as complete until the validator is clean, or until you
can explain why a specific finding is a false positive. To knowingly accept one:

```js
// mv3-doctor-disable-next-line
setInterval(cheapUiTick, 1000);
```

## Deeper references

Read these only when the task actually touches the area:

- `references/service-worker-lifecycle.md` — termination, wake-up, keep-alive
- `references/storage-and-state.md` — choosing between the storage areas
- `references/network-and-dnr.md` — declarativeNetRequest patterns
- `references/web-store-review.md` — permission justifications and rejections

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [n0liu](https://github.com/n0liu)
- **Source:** [n0liu/mv3-doctor](https://github.com/n0liu/mv3-doctor)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** yes

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-n0liu-mv3-doctor-mv3-doctor
- Seller: https://agentstack.voostack.com/s/n0liu
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
