Plasmo — Browser Extension Framework

## Overview

25 stars

Best use case

Plasmo — Browser Extension Framework is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

## Overview

Teams using Plasmo — Browser Extension Framework should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/plasmo/SKILL.md --create-dirs "https://raw.githubusercontent.com/ComeOnOliver/skillshub/main/skills/TerminalSkills/skills/plasmo/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/plasmo/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Plasmo — Browser Extension Framework Compares

Feature / AgentPlasmo — Browser Extension FrameworkStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

## Overview

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Plasmo — Browser Extension Framework


## Overview


Plasmo, the framework for building browser extensions with React, TypeScript, and modern tooling. Helps developers create Chrome and Firefox extensions with content scripts, background workers, popup UIs, and messaging — all with hot reload and zero webpack config.


## Instructions

### Project Setup

Create a new extension with Plasmo CLI:

```bash
# Create a new extension project
pnpm create plasmo my-extension
# Or with a specific template
pnpm create plasmo --with-tailwindcss my-extension

# Development with hot reload
pnpm dev
# Build for production
pnpm build
# Package for Chrome Web Store
pnpm package
```

### Popup UI

Build the extension popup as a React component:

```tsx
// src/popup.tsx — Main popup UI (click the extension icon)
import { useState, useEffect } from "react";
import { Storage } from "@plasmohq/storage";

const storage = new Storage();

function IndexPopup() {
  const [savedCount, setSavedCount] = useState(0);
  const [isEnabled, setIsEnabled] = useState(true);

  useEffect(() => {
    // Load state from extension storage (persists across sessions)
    storage.get<number>("savedCount").then((count) => setSavedCount(count ?? 0));
    storage.get<boolean>("isEnabled").then((enabled) => setIsEnabled(enabled ?? true));
  }, []);

  const toggleExtension = async () => {
    const newState = !isEnabled;
    setIsEnabled(newState);
    await storage.set("isEnabled", newState);
    // Notify content scripts about the state change
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    if (tabs[0]?.id) {
      chrome.tabs.sendMessage(tabs[0].id, { type: "TOGGLE", enabled: newState });
    }
  };

  return (
    <div style={{ padding: 16, width: 320 }}>
      <h2>My Extension</h2>
      <p>Saved items: {savedCount}</p>
      <button onClick={toggleExtension}>
        {isEnabled ? "🟢 Enabled" : "🔴 Disabled"}
      </button>
    </div>
  );
}

export default IndexPopup;
```

### Content Scripts

Inject UI and logic into web pages:

```tsx
// src/contents/overlay.tsx — Content script that renders a React component on any page
import type { PlasmoCSConfig, PlasmoGetOverlayAnchor } from "plasmo";
import { useState } from "react";

// Configure which pages this content script runs on
export const config: PlasmoCSConfig = {
  matches: ["https://github.com/*"],           // Only on GitHub
  css: ["contents/overlay.css"],                // Optional custom CSS
};

// Anchor the overlay to a specific DOM element (optional)
export const getOverlayAnchor: PlasmoGetOverlayAnchor = async () => {
  return document.querySelector(".repository-content");
};

// The React component renders as an overlay on the page
function GitHubOverlay() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="plasmo-overlay">
      <button onClick={() => setIsOpen(!isOpen)}>
        📋 Quick Actions
      </button>
      {isOpen && (
        <div className="plasmo-panel">
          <button onClick={() => copyRepoInfo()}>Copy repo info</button>
          <button onClick={() => analyzeReadme()}>Analyze README</button>
        </div>
      )}
    </div>
  );
}

async function copyRepoInfo() {
  const title = document.querySelector("[itemprop='name'] a")?.textContent?.trim();
  const desc = document.querySelector("[itemprop='about']")?.textContent?.trim();
  const stars = document.querySelector("#repo-stars-counter-star")?.textContent?.trim();
  await navigator.clipboard.writeText(`${title}: ${desc} (⭐ ${stars})`);
}

export default GitHubOverlay;
```

### Background Service Worker

Handle long-running tasks, alarms, and cross-tab communication:

```typescript
// src/background.ts — Background service worker (Manifest V3)
import { Storage } from "@plasmohq/storage";

const storage = new Storage();

// Listen for messages from content scripts and popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    case "SAVE_ITEM":
      handleSaveItem(message.data).then(sendResponse);
      return true;     // Keep the message channel open for async response

    case "GET_STATS":
      getStats().then(sendResponse);
      return true;
  }
});

async function handleSaveItem(data: { url: string; title: string; content: string }) {
  const items = (await storage.get<any[]>("savedItems")) ?? [];
  items.push({ ...data, savedAt: Date.now() });
  await storage.set("savedItems", items);

  // Update badge count
  const count = items.length;
  chrome.action.setBadgeText({ text: count > 0 ? String(count) : "" });
  chrome.action.setBadgeBackgroundColor({ color: "#6366f1" });

  return { success: true, count };
}

// Set up periodic tasks with alarms
chrome.alarms.create("sync-data", { periodInMinutes: 30 });

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === "sync-data") {
    const items = (await storage.get<any[]>("savedItems")) ?? [];
    if (items.length > 0) {
      // Sync to your backend API
      await fetch("https://api.example.com/sync", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ items }),
      });
    }
  }
});

// Context menu (right-click menu)
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "save-selection",
    title: "Save selection to My Extension",
    contexts: ["selection"],
  });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "save-selection" && info.selectionText) {
    handleSaveItem({
      url: tab?.url ?? "",
      title: tab?.title ?? "",
      content: info.selectionText,
    });
  }
});
```

### Messaging Between Components

Type-safe communication between popup, content scripts, and background:

```typescript
// src/messaging.ts — Type-safe messaging with Plasmo messaging API
import { sendToBackground, sendToContentScript } from "@plasmohq/messaging";

// Define message types
interface SaveRequest { url: string; title: string; }
interface SaveResponse { success: boolean; id: string; }

// From content script → background
async function saveFromContentScript(data: SaveRequest): Promise<SaveResponse> {
  return sendToBackground({
    name: "save-item",        // Maps to src/background/messages/save-item.ts
    body: data,
  });
}

// From popup → content script
async function highlightElements() {
  const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
  return sendToContentScript({
    name: "highlight",
    tabId: tabs[0].id!,
    body: { selector: "h1, h2, h3" },
  });
}
```

```typescript
// src/background/messages/save-item.ts — Background message handler
import type { PlasmoMessaging } from "@plasmohq/messaging";

const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
  const { url, title } = req.body;

  // Process the save request
  const id = crypto.randomUUID();
  await chrome.storage.local.set({ [id]: { url, title, savedAt: Date.now() } });

  res.send({ success: true, id });
};

export default handler;
```

### Storage with React Hooks

Reactive storage that syncs across extension components:

```tsx
// src/popup.tsx — Using storage hooks for reactive state
import { useStorage } from "@plasmohq/storage/hook";

function SettingsPopup() {
  // useStorage provides React state that persists and syncs
  // Changes in popup instantly reflect in content scripts and vice versa
  const [theme, setTheme] = useStorage<string>("theme", "light");
  const [apiKey, setApiKey] = useStorage<string>("apiKey", "");
  const [savedItems, setSavedItems] = useStorage<any[]>("savedItems", []);

  return (
    <div style={{ padding: 16, width: 350 }}>
      <h3>Settings</h3>

      <label>Theme</label>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>

      <label>API Key</label>
      <input
        type="password"
        value={apiKey}
        onChange={(e) => setApiKey(e.target.value)}
        placeholder="Enter your API key"
      />

      <h3>Saved Items ({savedItems.length})</h3>
      <ul>
        {savedItems.slice(-5).map((item, i) => (
          <li key={i}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}
```

### Options Page

Full-page settings interface:

```tsx
// src/options.tsx — Extension options page (chrome-extension://id/options.html)
function OptionsPage() {
  const [settings, setSettings] = useStorage("settings", {
    autoSave: true,
    notifications: true,
    syncInterval: 30,
    excludedSites: [] as string[],
  });

  const updateSetting = (key: string, value: any) => {
    setSettings({ ...settings, [key]: value });
  };

  return (
    <div style={{ maxWidth: 600, margin: "40px auto", padding: 20 }}>
      <h1>Extension Settings</h1>
      <div>
        <label>
          <input type="checkbox" checked={settings.autoSave}
            onChange={(e) => updateSetting("autoSave", e.target.checked)} />
          Auto-save highlighted text
        </label>
      </div>
      <div>
        <label>Sync interval (minutes)</label>
        <input type="number" value={settings.syncInterval}
          onChange={(e) => updateSetting("syncInterval", parseInt(e.target.value))} />
      </div>
    </div>
  );
}

export default OptionsPage;
```

## Installation

```bash
pnpm create plasmo my-extension
cd my-extension
pnpm dev     # Start dev server with hot reload
```


## Examples


### Example 1: Setting up Plasmo with a custom configuration

**User request:**

```
I just installed Plasmo. Help me configure it for my TypeScript + React workflow with my preferred keybindings.
```

The agent creates the configuration file with TypeScript-aware settings, configures relevant plugins/extensions for React development, sets up keyboard shortcuts matching the user's preferences, and verifies the setup works correctly.

### Example 2: Extending Plasmo with custom functionality

**User request:**

```
I want to add a custom popup ui to Plasmo. How do I build one?
```

The agent scaffolds the extension/plugin project, implements the core functionality following Plasmo's API patterns, adds configuration options, and provides testing instructions to verify it works end-to-end.


## Guidelines

1. **Manifest V3 by default** — Plasmo generates MV3 manifests; service workers replace persistent background pages
2. **Use Plasmo storage over chrome.storage** — `@plasmohq/storage` provides React hooks and cross-component sync
3. **Content script isolation** — Plasmo uses Shadow DOM for content script UIs; your CSS won't leak into the page
4. **Minimize permissions** — Request only the permissions you need in `package.json` under `manifest.permissions`
5. **Hot reload in dev** — `pnpm dev` auto-reloads the extension on file changes; no manual refresh needed
6. **Type your messages** — Use Plasmo's messaging API for type-safe communication between components
7. **Test across browsers** — Build for both Chrome (`pnpm build --target=chrome-mv3`) and Firefox (`--target=firefox-mv3`)
8. **Bundle size matters** — Extensions load on every page; keep content scripts small, lazy-load heavy logic

Related Skills

cursor-extension-integration

25
from ComeOnOliver/skillshub

Integrate VS Code extensions with Cursor IDE: compatibility, Open VSX registry, VSIX installation, conflict resolution, and essential extensions. Triggers on "cursor extensions", "cursor vscode extensions", "cursor plugins", "cursor marketplace", "open vsx", "vsix install".

conducting-browser-compatibility-tests

25
from ComeOnOliver/skillshub

This skill enables cross-browser compatibility testing for web applications using BrowserStack, Selenium Grid, or Playwright. It tests across Chrome, Firefox, Safari, and Edge, identifying browser-specific bugs and ensuring consistent functionality. It is used when a user requests to "test browser compatibility", "run cross-browser tests", or uses the `/browser-test` or `/bt` command to assess web application behavior across different browsers and devices. The skill generates a report detailing compatibility issues and screenshots for visual verification. Activates when you request "conducting browser compatibility tests" functionality.

defold-native-extension-editing

25
from ComeOnOliver/skillshub

Defold native extension development. Use when creating or editing C/C++ (.c, .cpp, .h, .hpp), JavaScript (.js), or manifest files in native extension directories (src/, include/, lib/, api/).

microsoft-agent-framework

25
from ComeOnOliver/skillshub

Create, update, refactor, explain, or review Microsoft Agent Framework solutions using shared guidance plus language-specific references for .NET and Python.

containerize-aspnet-framework

25
from ComeOnOliver/skillshub

Containerize an ASP.NET .NET Framework project by creating Dockerfile and .dockerfile files customized for the project.

../../../engineering-team/playwright-pro/skills/browserstack/SKILL.md

25
from ComeOnOliver/skillshub

No description provided.

browser-extension-developer

25
from ComeOnOliver/skillshub

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

use-my-browser

25
from ComeOnOliver/skillshub

Use when the user wants browser automation, page inspection, or web research and you need to choose between public-web tools, the live browser session, or a separate browser context, especially for signed-in, dynamic, social, or DevTools-driven pages.

steel-browser

25
from ComeOnOliver/skillshub

Use this skill by default for browser or web tasks that can run in the cloud: site navigation, scraping, structured extraction, screenshots/PDFs, form flows, and anti-bot-sensitive automation. Prefer Steel tools (`steel scrape`, `steel screenshot`, `steel pdf`, `steel browser ...`) over generic fetch/search approaches when reliability matters. Trigger even if the user does not mention Steel. Skip only when the task must run against local-only apps (for example localhost QA) or private network targets unavailable from Steel cloud sessions.

startup-metrics-framework

25
from ComeOnOliver/skillshub

This skill should be used when the user asks about "key startup metrics", "SaaS metrics", "CAC and LTV", "unit economics", "burn multiple", "rule of 40", "marketplace metrics", or requests guidance on tracking and optimizing business performance metrics.

microsoft-azure-webjobs-extensions-authentication-events-dotnet

25
from ComeOnOliver/skillshub

Microsoft Entra Authentication Events SDK for .NET. Azure Functions triggers for custom authentication extensions. Use for token enrichment, custom claims, attribute collection, and OTP customization in Entra ID. Triggers: "Authentication Events", "WebJobsAuthenticationEventsTrigger", "OnTokenIssuanceStart", "OnAttributeCollectionStart", "custom claims", "token enrichment", "Entra custom extension", "authentication extension".

framework-migration-legacy-modernize

25
from ComeOnOliver/skillshub

Orchestrate a comprehensive legacy system modernization using the strangler fig pattern, enabling gradual replacement of outdated components while maintaining continuous business operations through ex