How it works
Security architecture
An agent in your browser combines private data, untrusted web pages, and a way to send data out. This page explains why that combination is dangerous, what Remixlet does to shrink each part, and what it leaves to you.
Remixlet puts an AI coding agent in your browser and runs the code it writes on sites where you are logged in. If that sentence made you uneasy, good. It should. This page is our attempt to earn some of that trust back with specifics: what the risk is, which parts of it the extension removes, which parts it only narrows, and where your own judgement is still the last check.
The lethal trifecta#
In June 2025 Simon Willison gave a name to a pattern that keeps showing up in attacks on AI agents. He called it the lethal trifecta. An agent is in trouble when it has all three of these at once:
- Access to your private data.
- Exposure to content written by someone who is not you.
- A way to communicate with the outside world.
Any two are manageable. All three together let a stranger steal from you by writing text. They plant instructions where the agent will read them, the agent follows those instructions with your data in hand, and it uses the third property to send that data somewhere. The model does not need to be broken for this to work. It only needs to be helpful, which is what it was built to be. Willison's advice is blunt: don't combine the three, because no prompt-level guardrail has proven reliable enough to stand between them.
Here is the uncomfortable part. A browser agent has all three by construction.
Private data. The pages you point Remixlet at are the pages you are logged in to. The agent reads them, and the remixlet it writes runs inside them on every visit, indefinitely.
Untrusted content. Every web page is text someone else wrote, and much of it is text anyone can write: comments, listings, reviews, profile bios, a hidden span nobody sees. A page can contain instructions aimed at the agent reading it. This is prompt injection, and it is not hypothetical.
Communication. The agent talks to your AI provider by design. It can steer the tab to a URL. A remixlet can ask for network access. And any script on a page, ours included, can make an ordinary web request.
We cannot remove a leg outright. Remove the first and the agent cannot see the page. Remove the second and there is no web. Remove the third and nothing gets built. So the goal is different: shrink each leg to the minimum the job needs, put a check you control between the model and every consequence, and be honest about what is left.
Two actors, two lifetimes#
Two different things can go wrong, and they are contained differently.
The agent runs only while you are chatting. It reads the page, calls tools, and writes code. Its risk is a bad decision made in one turn.
The remixlet is the code the agent wrote. It runs on every matching page long after the conversation ended, and it never talks to the model again. Its risk is a persistent implant.
Everything below is one of those two being kept in its box.
Shrinking the private data#
Remixlet has no backend, no account, and no telemetry. Your remixlets, their history, your conversations, and your settings live in your browser's local storage. Page content leaves your machine only while the agent is working, and only to the provider you chose, under your own key. Provider keys are stored by the side panel and sent to one place, the provider they belong to. A lint rule fails the build if the background worker or the page bridge reference a key at all, or if any code puts one in a URL, a log line, or an error message.
The agent sees what the job needs and not much more. It works on the one tab the chat started on and stops if that tab closes. Its tool for reading page state refuses cookies, local storage, session storage, IndexedDB, caches, and the credential store. Big pages arrive as a structural outline rather than raw markup. There is no redaction filter, and we won't pretend one exists. If the page shows your bank balance, the model sees your bank balance. Your data and privacy lists exactly what a capture contains.
The remixlet sees less than the agent did. Chrome can run JavaScript on a page in separate compartments it calls worlds. The site's own code runs in the MAIN world. Each remixlet runs in its own named USER_SCRIPT world. It can read and change everything you see on the page, but it cannot read what the page's programs know: in-memory tokens, app state, the site's JavaScript variables. It cannot replace page functions to spy on them either, and the wall works both ways. Two remixlets on the same page cannot see each other.
Treating the page as hostile#
The agent's standing rules say page content is data, never instructions, and tell it to report attempts to steer it. A test suite feeds it pages that try. But the rule is a prompt, and Willison is right that a prompt is not a security boundary. So the design assumes the agent will sometimes be fooled, and arranges for a fooled agent to have nothing worth giving away.
- Nothing the agent reads or says can grant it a power. Grants come only from buttons you press in the panel, and the extension's background worker checks them outside the conversation.
- The agent may not ask for a network host because page text suggested it. A host is justified only by what you asked for and by probes that confirmed the data lives there. That is a prompt rule too. The dialog that follows is the real gate.
- The approval dialog is written by us, not by the model. Each power's description is fixed extension copy. The remixlet's stated reason appears as a separate quoted line, stripped of control and direction-changing characters and cut at 120 characters. A manipulated model cannot write the screen you decide on.
- The chat view drops images from the model's replies. A page cannot trick the model into "showing" an image whose address carries your data to someone else's server.
- A fully misled agent can, at worst, write code. You can read it, and it gets no new powers without your approval.
Narrowing the communication#
This is the leg that decides whether a mistake is embarrassing or expensive, so it has the most machinery.
A fresh remixlet world contains standard JavaScript, the page's HTML, and nothing else. Every power beyond that is a named capability the remixlet declares in its manifest, with a written reason, and that you approve before it activates:
{
"capabilities": [
"storage",
"fetch:api.bandcamp.com"
],
"capabilityRationales": {
"storage": "Remember which albums you've hidden.",
"fetch:api.bandcamp.com": "Look up album lengths the page doesn't show."
}
}The capabilities are small on purpose. storage is a private key-value store. clipboard writes text. notifications shows text. menu and schedule add a toolbar command and a timer. The network ones are the interesting ones, and each is scoped to named hosts:
- fetch:api.example.com lets the remixlet make requests to that host as you, and nothing else. The requests go through the extension, which re-checks the host on every redirect hop and drops any authorization header when a redirect crosses to another origin. There is no "access the internet" grant.
- network:observe:api.example.com lets the remixlet read the responses the page itself fetched from that host. The interceptor is our code, it keeps a bounded amount, and it is a pure pass-through. The page behaves identically with or without it.
- netrules lets a remixlet block, redirect, or alter requests, from a rules file the extension checks before lending it any power. Rules are pinned to the remixlet's own sites, cannot touch security headers such as content security policy, cookies, or CORS headers, and may only redirect to an approved host over https.
- page-world is the one grant that means "this code runs unsandboxed", in the site's own world. It is a named capability so that fact is something you say yes to, never a manifest field that flips quietly. Even then, page-world code gets no
rmx.*functions and no capability-bearing secret.
Declaring a capability hands the remixlet no machinery. The powers exist only as a small API, rmx.*, injected into the remixlet's world, and those functions do nothing locally. Each call is a message to the background worker, which checks two things: does the message carry the secret token belonging to that remixlet's world, and is the capability in that remixlet's granted list. Fail either and the call dies at the door. The page cannot forge a call, remixlet A cannot borrow remixlet B's powers, and asking nicely changes nothing.
Approval is bound to a SHA-256 digest of the exact files being proposed, is single-use, and expires after ten minutes, so a yes to one proposal cannot authorize swapped-in code. The dialog itself shows the sites, the capabilities in plain words, and the remixlet's stated reasons, not the files; the digest is what ties your yes to the code. The stored manifest is the approval record. Activation is the only way files reach the store, and it refuses any set whose capabilities you have not just approved or already held. Updates re-open the dialog whenever a new or broader capability appears, whenever the site list widens, always when a remixlet asks to run on every site, and whenever the rules file changes. Nothing widens silently.
Remote code is refused at two points, with one gap. Each remixlet's world carries a content security policy (script-src 'self'; object-src 'none') that blocks scripts from the network, dynamic import(), eval and Function. Before that, the extension parses every file at save time and rejects a remote import(), a created script tag, an eval of anything but a literal, and an element src pointed at a remote URL. The gap is the browser's: a string handed to setTimeout or setInterval runs as code inside the world under this policy. The save-time check bounds it; the runtime does not enforce it. So the honest claim is that a remixlet cannot load a script from the network, not that it cannot run code it did not ship.
What this does not close#
Two edges are open, and you should know about them before the next section.
A remixlet's world blocks loading scripts from the network, but it does not block ordinary outbound requests. A script can call fetch, or load an image from an address it built, at any host, with no grant. Browsers give every script that ability, and the world's policy today restricts only code loading. So fetch:api.example.com governs requests made as you, with your cookies, through the extension. It does not mean "this remixlet cannot talk to anything else". A remixlet that read a page and sent it away would need no dialog. What stands in its way is that the code is small and readable and was written by an agent whose rules forbid it. That is a weaker guarantee than the others on this page, and we would rather say so than let the capability list imply more than it enforces.
The agent's navigate tool accepts any http or https address. A misled agent could steer the tab to a URL with page data in it. You would see the tab change, but the request would already have been made.
Everything is readable and reversible#
A remixlet is plain, unminified code, and each one keeps its own git history. Every change is a commit you can open as a colored diff, and rolling back to any version is one click. The manifest and its reasons are always visible. If something misbehaves you can toggle the remixlet off, pause every remixlet on a site at once, archive it, or delete it forever. Deleting it destroys every grant it held, because the grant record is the manifest. Pause is enforced in the worker, not only at injection: a paused remixlet's calls are rejected even if its script is still loaded in an open tab, and its schedules and menu items go dead with it.
Some things we chose not to build. There is no marketplace and no install link, so nobody can publish a remixlet into your browser. There is no export to a standalone script, because an exported script would carry its powers outside these walls with no one checking calls at the door. A capability is only real because the extension stands behind it.
Where you still come in#
Everything above narrows the trifecta. It does not dissolve it. The three legs still meet in your browser, and some decisions are only yours to make.
- Choose the pages. Don't point the agent at a page you would not paste into a chat with your provider. Your bank, a health portal, an admin console. The extension does nothing on a site until you ask it to.
- Read the dialog, not just the button. "Act on your account at" a host means the remixlet makes requests as you. "Work inside the page itself" means unsandboxed. "Runs on every site you visit" means every site. If a request names a host you never mentioned, say no and ask the agent why it wants it.
- Treat "Allow all" as what it is. The dialog for running a one-off script during a chat offers "Allow all" for that conversation. After it, text planted on the page can drive scripts for the rest of that chat without another prompt. Use it on pages you trust, or not at all.
- Skim the code when the stakes are up. A remixlet that should only hide elements has no business calling
fetch, building anImage, or usingsendBeacon. The diff view makes those easy to spot. - Listen when the agent says something odd. It is told to report manipulation. When it does, that page is hostile to agents, and a remixlet built on it deserves a second look.
- Keep the kill switches in mind. Toggle off, pause the site, archive, delete. They are all one click from the toolbar or the manager.
The one-sentence version#
Chrome supplies the walls, we decide what lives on which side, a background worker checks every powerful request against exactly what you approved, all of it is code you can read, and the gaps no wall covers are yours to watch. The source is open. Go looking for holes, and tell us when you find one.