tabKey
Allow-list
Local-first
Remote Tab Opener
Control one or multiple named tabs from your own secure page: open, navigate, focus, read state, and run safe predefined DOM actions — all locally, inside the browser.
What It Is
Remote Tab Opener is a local browser extension that lets a trusted page you control
orchestrate real tabs through a safe bridge. Your page emits requests; the extension enforces an allow-list,
targets a specific tab using tabKey or tabId, then executes predefined actions inside that tab.
How It Works (3 steps)
-
Your page sends a request using
window.postMessage()(or a small helper likeask()that adds requestId + timeout). -
The extension validates it (origin/host allow-list, HTTPS rules, optional LAN/localhost flags) and routes it to the targeted tab
(
tabKeyortabId). -
The action runs either in the background (tab control like
openTab,navigate,focusTab,closeTab,getUrl) or inside the tab via a content script (safe DOM automation via acommandenvelope).
This does not bypass SOP: actions only run in tabs on domains you explicitly allow.
Key Features
- Multi-tab orchestration using
tabKey(name tabs like"auth","monitor","sales"). - Tab control: open, navigate, focus, close, read URL/title.
- Safe DOM automation on allow-listed hosts (type, click, submit, wait, extract bounded text/HTML).
- Local-first: no server, no proxy, no cloud relay.
- Explicit allow-list + optional LAN/localhost opt-in for intranet tooling.
- Favorites management in the extension popup.
Capabilities at a Glance
Tab Control (background)
openTab– open (or reuse) a controlled tab by URL, optionally withtabKey;navigate– change the URL of a targeted tab (tabKeyortabId);focusTab– bring the targeted tab to front;closeTab– close a targeted tab;getUrl– read currenturl+title;waitForNavigation– wait until navigation settles;listTabs,adoptTab,releaseTab— manage multiple tabs;screenshot— capture a screenshot of the controlled tab (where permitted).
Uses standard extension APIs: tabs.create / update / sendMessage / query / get / remove.
DOM Actions (content script via command)
setValue,typeText,pressKeyclick,submit/submitFormwaitFor/waitForSelector(SPA timing)getText,getHtml,getAttr,getValue(bounded)highlight,overlay*helpers (visual guidance)selectOption,selectSetValue,setChecked
Runs inside the target page, only on allow-listed hosts. No arbitrary eval.
Messaging & Events
- Detect gate:
detect→detectResult - Requests:
window.postMessage→ extension → tab - Events back to your page:
tabStatus,tabPageChanged
Stable schemas + requestId pairing to avoid cross-talk.
Safety
- Domain allow-list configured in the popup
- LAN/localhost require explicit opt-in + allow-list (double barrier)
- Local-only: no data collection, no external requests
- Respects browser security boundaries (SOP)
Deny-by-default model: if a host isn’t allowed, actions are rejected.
Real-World Flows — without the overhead
What this means: orchestrate a real user path end-to-end — open → type → click → navigate → verify — using your own page. No Selenium grid, no remote runner.
- ✔️ Runs locally with a real, visible tab.
- ✔️ requestId + timeout for robust request/response pairing.
- ✔️ Safe by default: allow-list + named actions.
- 🚫 No SOP bypass, no cookie access, no hidden network interception.
A minimal flow (tab control + DOM commands)
<script type="module">
const RTO_PAGE_ORIGIN = "rto-page";
const RTO_EXT_ORIGIN = "rto-extension";
function makeId(){ return "rto_" + Math.random().toString(16).slice(2) + "_" + Date.now(); }
// Tab actions handled by background:
const TAB_ACTIONS = new Set(["openTab","navigate","focusTab","closeTab","getUrl","waitForNavigation","listTabs","adoptTab","releaseTab","screenshot"]);
// DOM actions go through { type:"command", action, payload }:
const DOM_ACTIONS = new Set([
"setValue","typeText","pressKey","click","submit","submitForm",
"waitFor","waitForSelector","getText","getHtml","getAttr","getValue",
"highlight","setChecked","selectOption","selectSetValue"
]);
function ask(action, args = {}, opts = {}) {
const requestId = makeId();
const timeoutMs = Number(opts.timeoutMs || 8000);
const isDom = DOM_ACTIONS.has(action);
const isTab = TAB_ACTIONS.has(action);
const isDetect = (action === "detect");
const msg = isDetect
? { origin: RTO_PAGE_ORIGIN, type: "detect", requestId }
: isDom
? { origin: RTO_PAGE_ORIGIN, type: "command", action, payload: args, requestId, tabKey: args.tabKey, tabId: args.tabId }
: { origin: RTO_PAGE_ORIGIN, type: action, ...args, requestId };
const expectedType = isDetect ? "detectResult"
: (isDom ? (action + "Result") : (action + "Result"));
return new Promise((resolve) => {
const t = setTimeout(() => done({ ok:false, error:{ code:"TIMEOUT", message:"No response within timeout" } }), timeoutMs);
function done(out){
clearTimeout(t);
window.removeEventListener("message", onMsg);
resolve(out);
}
function normalizeError(e){
if (!e) return { code:"ACTION_FAILED", message:"Unknown error" };
if (typeof e === "string") return { code:e, message:e };
const code = e.code || e.error || e.name || "ACTION_FAILED";
const message = e.message || e.msg || String(code);
return { code, message };
}
function onMsg(ev){
const d = ev && ev.data ? ev.data : null;
if (!d || typeof d !== "object") return;
if (d.origin !== RTO_EXT_ORIGIN) return;
if (d.requestId !== requestId) return;
if (d.type !== expectedType) return;
if (d.ok === true) done({ ok:true, data:d.data || d });
else done({ ok:false, error: normalizeError(d.error || d) });
}
window.addEventListener("message", onMsg);
window.postMessage(msg, "*");
});
}
(async () => {
const det = await ask("detect", {}, { timeoutMs: 1200 });
if (!det.ok) { console.warn("RTO not detected. Install the extension and reload."); return; }
// 1) Open a named tab
const opened = await ask("openTab", { url:"https://example.org/login", tabKey:"auth", focus:true });
if (!opened.ok) { console.warn("openTab failed:", opened.error); return; }
// 2) DOM automation (never hardcode real credentials in examples)
await ask("setValue", { tabKey:"auth", selector:"#email", value:"user@example.org" });
await ask("setValue", { tabKey:"auth", selector:"#password", value:"example-password" });
await ask("click", { tabKey:"auth", selector:"form button[type=submit]" });
// 3) Read tab state
const st = await ask("getUrl", { tabKey:"auth" });
console.log("Auth tab:", st.ok ? st.data : st.error);
})();
</script>
- Drive multiple tabs using
tabKey/tabId. - Wait for SPA elements and click/type safely.
- Read current URL/title and take screenshots (where permitted).
- Bypass Same-Origin Policy or read cookies.
- Execute arbitrary code on arbitrary sites.
- Automate file uploads (
<input type="file">limitation).
See the live E2E demonstration examples now.
Security by design
- Runs locally in your browser — no backend required.
- Works only on domains you authorize (allow-list).
- LAN/localhost require explicit opt-in in the popup and must be present in the allow-list.
- Predefined actions executed by the extension’s content script (no arbitrary code).
- Respects browser security rules (no SOP bypass; clean messaging).
See the LAN/localhost page and full notes in the official documentation.
Controlled tab indicator (optional)
Controlled tabs can display a thin indicator banner at the very top (when enabled by the extension / page policy).
It helps users understand they’re in a tab driven by RTO, and can surface the current tabKey.
- Visible only in the controlled tab (never in other tabs).
- Can be minimized into a compact, draggable chip.
- Helps debugging: you instantly see “this tab is controlled”.
- If a site blocks content script injection, the indicator won’t appear (tab is not controlled).
- Open a remote tab from your master page (e.g.
tabKey:"sales"). - Minimize the banner.
- Drag the chip wherever you like.
- Reload the tab to confirm it’s still controlled.
Try it: Click the "✕" (Minimize) to collapse the banner into a draggable chip.
Drag the chip anywhere; click ▲ to restore the banner.
Browser Limits — What RTO Solves
Remote Tab Opener does not break browser security. It uses the extension layer to provide capabilities that ordinary pages don’t have, while still respecting SOP and user consent.
| Native Limitation | What RTO Enables (legit & local) | What RTO Will Not Do |
|---|---|---|
| Page A cannot directly control a tab on another domain. | Open / navigate / focus / close a tab via the extension bridge; run predefined DOM actions inside that tab (allow-listed host). | No universal control of arbitrary sites; no cross-origin injection without permission. |
| Pages can’t reliably bring background tabs to front. | focus brings the targeted tab to foreground (via extension APIs). | No stealing focus randomly; only for known targeted tabs. |
| Direct DOM automation is scoped to same-origin only. | command DOM actions run in the target tab via a content script on hosts you approved. | No arbitrary eval(); no access to cookies or sessions. |
| Reading another tab’s URL/title is restricted. | getUrl returns current url + title from the targeted tab. |
No network interception; no webRequest spying. |
| SPAs render late; selectors are brittle. | waitForSelector helps you act only when UI is ready. | Does not bypass CSRF/auth; does not fill <input type="file">. |
Hard Guarantees
- No SOP bypass. Actions run only with explicit host permission (allow-list).
- No data collection. No remote servers required.
- Local-only. All logic lives in the browser (background/content + postMessage).
- User control. You choose which domains are controllable in the popup.
Quick Snippets (copy & run)
Open a named tab + wait + click (modern contract)
<script type="module">
// Minimal: you can copy the ask() helper from the E2E section above.
// Then:
const det = await ask("detect", {}, { timeoutMs: 1200 });
if (!det.ok) return;
await ask("openTab", { url:"https://example.org/", tabKey:"demo", focus:true });
await ask("waitForSelector", { tabKey:"demo", selector:"main a[href]" }, { timeoutMs: 12000 });
await ask("click", { tabKey:"demo", selector:"main a[href]" });
</script>
Handle the allow-list error cleanly
const r = await ask("openTab", { url:"https://not-allowed.example", tabKey:"x" });
if (!r.ok && r.error && r.error.code === "DOMAIN_NOT_ALLOWED") {
alert("Domain not allowed. Add it in the extension popup Allow-list, then retry.");
}
Tip: add your admin origin + target hosts in the allow-list. For intranet/localhost, enable the popup flags and keep hosts in the allow-list.
Use Cases
Moderate external content
Open a controlled tab to preview user content and move through submissions without leaving your dashboard.
QA navigation flows
Launch test sessions, navigate across cases, wait for SPA UI, and verify URL/title programmatically.
Guided support
Open the right help page, bring it to front when needed, and close it once the issue is resolved.
Admin workflows
Streamline repetitive navigation across domains from a single secure hub — safely, locally, visibly.
FAQ (short)
Does this bypass the Same-Origin Policy? No. The extension executes actions inside the target tab with explicit host permissions.
Do I need a server or proxy? No. Everything is local in the browser.
Is any data collected? No. The extension declares no data collection.
Where do I configure allowed domains? In the extension popup (allow-list). For LAN/localhost, enable the dedicated flags too.
How do I control 2 tabs? Use tabKey (e.g. "sales", "monitor") or a tabId from listTabs. Pass tabKey/tabId on each action.
Changelog (short)
- v7.11.5: current Firefox MV3 release line — multi-tab orchestration via
tabKey, moderncommandDOM actions, strong allow-list + optional LAN/localhost opt-in.
Docs & Links
| 🟩 Install on AMO |
Firefox Add-ons listing, stable builds.
|
|---|---|
| 📘 Official Documentation and SDK |
Concepts, security notes, APIs, setup (multi-tabs & LAN).
Read docs
|
| 🧪 Examples |
Full working examples step by step and source code.
Browse examples
|
| ❓ FAQ |
Common questions.
View FAQ
|
| 🔒 Privacy & Permissions |
Data & scopes.
Privacy notes
|
| 🤖 Ask the Assistant (ChatGPT) |
Interactive help to generate your scripts and debug errors.
Open assistant
|
Honest comparison — RTO vs. alternatives
A visual guide to when each approach shines.
tabKey —
great for demos, teaching, and local tooling.
What RTO does best
- ✅ Reads the rendered (post-JS) DOM from a live tab
- ✅ Named tab control: open / focus / close / navigate
- ✅ Teaching-friendly UX: overlays/highlights, safe automation primitives
- ✅ Local & visible: no cloud relay, allow-listed actions
- ✅ Low setup: extension + small snippets
When another tool is better
- 🏭 Headless at scale (CI farms): Playwright / Puppeteer
- 🧪 Heavy E2E UI testing: WebDriver / RPA suites
- 📄 Pure SSR/static pages:
fetch()/ view-source suffices - 🧩 One-off tweaks: userscripts / bookmarklets
Competitors
RTO - Remote Tab Opener
Live DOM orchestration for real, visible tabs — great for demos and education.
- Use for: multi-tab demos, manual QA, teaching SPA realities
- Avoid for: huge headless farms / heavy CI
Lightweight View-source / fetch()
Reads downloaded HTML only — great for SSR, blind to SPA hydration.
- Use for: server-rendered pages, sitemap sweeps
- Avoid for: SPAs (no hydrated anchors)
Headless Playwright / Puppeteer
Full-power automation, perfect for CI — less ideal for live teaching.
- Use for: scale, reliability, regression checks
- Avoid for: human-visible demos / quick workshops
Enterprise WebDriver / RPA
Broad UI control across apps, but verbose and brittle for fast local tooling.
- Use for: enterprise E2E & regulated environments
- Avoid for: lightweight, local demos
Hobbyist Userscripts / Bookmarklets
Quick wins inside the current page; limited cross-tab control.
- Use for: small tweaks, shortcuts, helpers
- Avoid for: multi-tab workflows