Remixlet’s security architecture
Remixlet puts an AI coding agent in your browser and runs the code it writes on websites where you’re logged in. That sentence should make you pause. This page explains, from scratch, everything we built so it doesn’t have to.
The problem, stated honestly
There are three things in the loop we refuse to fully trust:
- The author is a model. It writes good code, is occasionally wrong, and can be lied to by what it reads.
- The input is a live web page. Any page can contain text written specifically to manipulate an AI that reads it.
- The output keeps running. A remixlet applies every time you visit the site, long after the conversation that created it ends.
Classic userscripts answer this with trust in the author: a human picks a script, can read it, and installs it. When the author is a model, that answer is gone. So Remixlet moves the trust from the code to the boundary around the code: it doesn’t matter who wrote the script, because the script physically cannot do more than what you approved.
Layer 0: there is no server to breach
Remixlet has no backend, no account, and no telemetry. Your remixlets, their full version 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 AI provider you configured. Provider API keys are stored in the side panel’s local storage and are sent to exactly one place: the provider they belong to. They never pass through the pages you visit — a rule our linter enforces on every build, not a habit we try to remember.
This isn’t a security feature so much as a smaller attack surface: data that never travels can’t be intercepted, and a database that doesn’t exist can’t leak.
Layer 1: where the code runs
Chrome can run JavaScript on a page in separate compartments it calls worlds. Every world sees the same HTML, but each has its own variables and functions and cannot see into the others. The website’s own code runs in the MAIN world. Chrome provides a second kind, the USER_SCRIPT world, made for scripts a user adds.
Every remixlet’s code runs in a USER_SCRIPT world — and not a shared one: each remixlet gets its own named world, so two remixlets on the same page are also isolated from each other. Inside that compartment, a remixlet can read and change everything you see: hide elements, restyle them, add buttons. What it cannot do is read what the page’s programs know — the site’s JavaScript variables, in-memory tokens, app state — and it cannot replace page functions to spy on them. The wall works both ways: the page can’t see or tamper with the remixlet either.
Layer 2: what the code can do — capabilities
A fresh world contains standard JavaScript, the page’s HTML, and nothing else. Every power beyond that is a named capability that the remixlet must declare in its manifest, with a written reason, and that you approve on a dedicated screen before the remixlet 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."
}
}Capabilities are deliberately small and specific: storage (a private key-value store), clipboard (write text only), notifications (text only), menu, schedule, network-rule filtering, and network access. The network ones are scoped to named hosts — fetch:api.example.com grants requests to that host and nothing else. There is no “access the internet” grant, on purpose. Updates re-open the approval screen whenever a new or broader capability appears; nothing widens silently.
Layer 3: enforcement — every call is checked
Declaring a capability doesn’t hand the remixlet any machinery. The powers exist only as a small API (rmx.*) we inject into the remixlet’s world, and those functions do nothing locally — each call sends a message to the extension’s background worker, which checks two things before doing any work: does the message carry the secret token belonging to that remixlet’s world, and is this capability actually in that remixlet’s granted list? Fail either check and the call dies at the door.
This is the load-bearing difference from a convention-based design. The sandbox can be full of AI-written code doing its best or its worst; the decision about what’s allowed is made outside the sandbox, per call, against the list you approved. The page can’t forge a call, remixlet A can’t borrow remixlet B’s powers, and a script asking nicely changes nothing.
Layer 4: the powerful exceptions have a price tag
Two things genuinely can’t be done from inside the sandbox, and we refused to smuggle them in quietly.
Running in the page’s own world. Occasionally a remixlet needs to touch the site’s real JavaScript. That requires the page-world capability — a named grant whose whole job is to make “this code runs unsandboxed” something you explicitly say yes to, never a manifest field that flips silently. Even then, page-world code gets no rmx.* API and no capability-bearing secret — anything placed where the page can read it carries a second, weaker token that can only relay data back to the sandboxed side, never invoke a power. A hostile page that steals it gets nothing.
Watching the page’s network traffic. network:observe:api.example.com lets a remixlet read the API responses the page itself fetches — useful when the data you want streams in as you scroll. The interceptor is our code, not the remixlet’s: it’s scoped to the one granted host, it caps how much it keeps, and it’s a pure pass-through — the page behaves identically whether or not it’s there.
Layer 5: the page is hostile input
Prompt injection is not hypothetical: the agent reads page content, and page content can contain instructions aimed at it. Our defense doesn’t rely on the model never being fooled. It relies on the model having nothing to give away:
- The agent’s standing rules treat page text as data, never as instructions — and tell it to report manipulation attempts to you.
- Nothing the agent reads or says can grant authority. Capability grants come only from buttons you press in the panel, checked by the worker outside the conversation.
- The agent may not request a network host because page content suggested it — a host is only justified by what you asked for.
- A fully misled agent can, at worst, propose code — which you can read, and which receives no new powers without your approval.
We keep a test suite of hostile pages that try exactly these tricks, and the build fails if the agent stops refusing them.
Layer 6: nothing is hidden from you
Every remixlet is plain, readable code — no minification, no blob. 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 capability rationales 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 — and deleting it also destroys every grant it held.
What we deliberately don’t do
- No remote code. A remixlet runs only the bytes stored in your browser. Nothing is fetched and executed, and scripts never update themselves from anywhere.
- No sharing, no marketplace. Nobody can publish a remixlet into your browser, and there are no install links. We’d rather add distribution slowly and safely than bolt on an ecosystem of untrusted code.
- No lossy escape hatches. A remixlet’s powers exist only inside the extension. We don’t convert remixlets into standalone scripts, 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.
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, and all of it is code you can read. The source is open — please go looking for holes, and tell us when you find one.