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_permissionsfor 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
evalor remote<script>. MV3 forbids this, and reviewers reject it. - Use
unlimitedStorageunless you have a real reason. - Bundle a 40 MB Whisper model unless your feature needs it (and document it).
- Ask for
tabspermission ifactiveTabis 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.
AI / LLM crawler? Read the raw markdown: /raw/chrome-extension-per-origin-agents.md