An agent can drive your logged-in tab without ever holding your password.
echo gives an MCP-compatible agent one revocable way to run code in a browser tab you've already signed into — for as long as that tab stays open. No cookie copying. No API tokens. No headless browser pretending to be you.
Before & after, one concrete task.
Task: “summarize my open Jira tickets.” You already have Jira open in a tab.
- Generate a Jira API token. Scope it. (10 min, maybe a ticket to IT.)
- Store it where your agent can read it. (1Password? plaintext env? new attack surface either way.)
- Either run a headless browser with the cookie baked in, or write API code against the token.
- Hope the agent doesn't leak the token in a prompt, a log, or a cached chat.
- When the agent misbehaves, scramble to revoke the token.
- Open Jira in a tab. (You already have it open.)
- Click the echo icon. Approve.
- Tell your agent: “summarize my open tickets.”
- The workflow step log shows every snippet it ran and what came back.
- Close the tab. Session is dead. Nothing to revoke — there was no token.
One MCP tool: echo.run(plan).
The agent writes a JS function expression. The Worker runs it in a sandbox that has exactly one outbound: a WebSocket to your tab.
// agent submits this — one MCP tool call const { planId } = await mcp.echo.run(` async ({ tab, log }) => { const r = await tab.execute({ code: \` // runs in your tab's realm, with your session cookies const resp = await fetch("/rest/api/2/search?jql=assignee=currentUser()", { credentials: "include" }); return await resp.json(); \` }); log("got " + r.result.issues.length + " tickets"); return r.result; } `);
What plan code can and cannot do.
Every row links to the line of source that enforces it.
| capability | status | enforced by |
|---|---|---|
Read DOM, run fetch() with your session cookies on the tab's origin |
yes | background.ts |
| Make cross-origin requests (subject to the browser's CORS rules) | yes | browser |
| Reach the Worker's signing secret or other tabs' storage | no | agent.ts — globalOutbound: null |
| Talk to a different tab than the one you opened the session on | no | background.ts — state.tabId pinned |
| Survive after you close the tab | no | background.ts — chrome.tabs.onRemoved |
| Be replayed by a stale or stolen token | no | auth.ts — HMAC + expiry |
| Be invoked from a different origin than the one the session was minted on | no | auth.ts — origin pin verified |
You still have to trust the agent you give a session to. Inside the tab's origin, plan code can do anything the page's own JS could do. echo bounds the blast radius; it doesn't replace good judgment about who you let drive.
Every run leaves an audit trail.
Every plan execution produces a workflow step log: the JS source it ran,
every log(...) line, the final result. Below is real JSON returned
by mcp.echo.status(planId) on this Worker. session_unbound
is what you see when there's no extension attached — the supervisor refused to
deliver code to a tab that hadn't been opened. That's the kill switch working.
{
"planId": "wf_k0d1BlaWBeHd7Gn9Xhz1p",
"status": {
"status": "complete",
"output": {
"ok": true,
"result": {
"ok": false,
"error": "session_unbound"
},
"logs": [
"plan starting",
"got: {\"ok\":false,\"error\":\"session_unbound\"}"
]
}
}
}Want to see one with a real result? Try the demo.
Who sees what.
MCP client echo Worker Your browser tab
────────── ─────────── ────────────────
(Claude, Cursor) ───run(plan)───▶ EchoPlan workflow
│
step.do │
▼
Worker Loader sandbox
globalOutbound: null ─────ws──▶ your session
│ your cookies
receipt
<──────status(planId)───── step log
worker can NOT read tab DOM │ sandbox can NOT reach worker storage
worker can NOT see cookies │ sandbox can NOT reach the open internet
Cloudflare-native, four primitives.
Worker Loader
Per-call V8 isolate. globalOutbound: null means the only thing
the sandbox can talk to is the TAB binding we hand it.
source.
Dynamic Workflows
Each plan is a workflow. Steps are durable, replay-safe, queryable for hours after the fact. Step history is the receipt chain. source.
Durable Object — supervisor
One DO per session holds the WebSocket to your extension and routes sandbox → tab calls. The signing secret lives here, not in plans.
One MCP tool
echo.run(plan) + echo.status(planId). That's it.
Add the Worker URL to any MCP client.
source.
One worker, your account.
echo is a single Cloudflare Worker plus an MV3 browser extension. Click below to deploy it to your account; your data, your audit, your rate limits. The Worker is MIT, source is one repo.