Your team uses 10 SaaS tools. You wish each one had a Linda agent. You don’t own any of those tools. You can’t add a script tag.

You can ship a Chrome extension.

@linda/extension is a Manifest V3 reference scaffold for exactly this case. One extension, a different agent per origin, content-script injection, per-origin config. ~200 lines of glue around Linda’s core SDK.

The shape

my-team-copilot/
├── manifest.json          # MV3 — minimal permissions
├── public/
│   ├── icons/
│   └── linda.js           # bundled Linda runtime
├── src/
│   ├── content.ts         # injects Linda into matching origins
│   ├── background.ts      # service worker — key storage, config fetch
│   ├── popup/             # extension popup UI (settings, status)
│   └── configs/
│       ├── zendesk.json   # "you are a Zendesk macro assistant"
│       ├── salesforce.json
│       └── hubspot.json
└── package.json

The manifest

{
  "manifest_version": 3,
  "name": "Acme Team Copilot",
  "version": "0.1.0",
  "permissions": ["storage", "scripting"],
  "host_permissions": [
    "https://*.zendesk.com/*",
    "https://*.salesforce.com/*",
    "https://*.hubspot.com/*"
  ],
  "content_scripts": [
    {
      "matches": [
        "https://*.zendesk.com/*",
        "https://*.salesforce.com/*",
        "https://*.hubspot.com/*"
      ],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ],
  "background": { "service_worker": "background.js" },
  "action": { "default_popup": "popup/index.html" },
  "icons": { "128": "icons/128.png" }
}

The content script

// src/content.ts — runs in every matching tab.
import { Linda } from "@linda/core";

async function load() {
  const host = location.host;
  const configRes = await fetch(chrome.runtime.getURL(`configs/${matchHost(host)}.json`));
  const config = await configRes.json();

  const { apiKey, provider, model } = await chrome.storage.sync.get([
    "apiKey", "provider", "model",
  ]);

  const linda = new Linda({
    transport: { mode: "browser", provider, apiKey, model },
    config,
  });

  linda.attach({ chatButton: { position: "bottom-right" } });
}

function matchHost(host: string) {
  if (host.endsWith(".zendesk.com")) return "zendesk";
  if (host.endsWith(".salesforce.com")) return "salesforce";
  if (host.endsWith(".hubspot.com")) return "hubspot";
  return "default";
}

load();

That’s the whole content script. Linda’s runtime handles the rest.

The per-origin configs

// configs/zendesk.json
{
  "persona": "You're a Zendesk macro assistant. Read the ticket, suggest a macro, draft a response in the agent's voice.",
  "target": { "type": "page" },
  "primitives": ["observe", "extract", "act"],
  "skills": ["consent-flow"]
}
// configs/salesforce.json
{
  "persona": "You're a Salesforce lead enrichment assistant. Read the current account view, summarize the recent activity, and suggest the next best action.",
  "target": { "type": "page" },
  "primitives": ["observe", "extract"],
  "tools": {
    "log_activity": {
      "description": "Log a call/email/meeting to the current account.",
      "handler": "https://api.example.com/sf-activity"
    }
  }
}

Same Linda runtime. Different brain per origin.

What passes Chrome Web Store review

We’ve shipped extensions through Chrome Web Store; here’s what reviewers care about.

Do:

  • Declare exact host_permissions for the origins you target. No <all_urls>.
  • Load all code from the extension package. No remote-loaded JS.
  • Document the LLM call destination in the privacy policy.
  • Show users their data flow: “we send the current page text to your chosen LLM provider; here’s their privacy policy.”

Don’t:

  • Inject scripts via eval or remote <script>. MV3 forbids this, and reviewers reject it.
  • Use unlimitedStorage unless you have a real reason.
  • Bundle a 40 MB Whisper model unless your feature needs it (and document it).
  • Ask for tabs permission if activeTab is enough.

For LLM-specific reviews: yes, an extension that “sends user data to AI” is going to get reviewed. Be ready to explain what data, to which provider, on which user action. Have a clean settings page where the user picks the provider and key.

The activeTab pattern (alternative)

If you don’t want to declare every host upfront, use activeTab + scripting.executeScript:

chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id! },
    files: ["content.js"],
  });
});

This injects Linda only when the user clicks the extension icon. No declared hosts. Friendlier review (you’re not running on every page). Trade-off: the agent isn’t “always on” — the user has to invoke it.

Distribution patterns

Open-source extension. Ship the scaffold under MIT. Each user adds their own API key. Useful for personal-productivity tools and dev-tool augmentations.

Internal-only. Distribute via your org’s Chrome Enterprise policy. Pre-configure with an @linda/server proxy URL so users don’t need their own keys. Used for sales / support team enablement.

Vertical SaaS. Build a specific extension for a specific vertical (e.g., sales) bundling configs for the 5 tools sales people use. Charge for premium configs or hosted features. Yes, you can sell extensions even when the underlying SDK is MIT — the value is in the configs and the brand.

What you can’t do

Browser extensions, even with host_permissions, can’t:

  • See cross-origin iframes from the parent.
  • Bypass the SaaS app’s authentication.
  • Access protected DOM nodes the site has explicitly shielded.

These are features, not bugs. They mean an extension shipped to your team isn’t a security risk — the agent only sees what the user is already authorized to see.

Try it

Clone packages/extension, update the manifest with your origins, write your configs, and load unpacked into Chrome’s developer mode. You’re 30 minutes from a working per-origin copilot. Ship a build to your team and you’ve got a tool your competitors don’t.

FAQ

Does this work on Firefox / Edge / Brave?

Yes — Manifest V3 is supported across all Chromium-based browsers, and Firefox has its own MV3 implementation. Linda doesn't care which browser; the scaffold targets MV3 spec.

What permissions does the extension need?

Minimal — host_permissions for the origins you target, storage, scripting (for the active-tab pattern). No remote code, no network beyond the LLM call configured by the user.