Share an unpublished extension build for review
You have a build in dist/ that is not in a store and should not be. Someone needs to look at it: a designer, a product manager, a client, the person who filed the bug.
Mint a share and hand out the answer:
https://preview.extension.dev/?preview=gen_ndj4t8x2Whoever opens that link watches your extension run in a browser inside their tab. They install nothing, sign in to nothing, and change nothing about their own browser. The rest of this page is how to mint the link, what it hands over, how long it lives, and how to kill it.
Three ways to mint it
From an agent. With the MCP server configured, one tool call builds, uploads, and returns the link:
{
"tool": "extension_preview_web",
"projectPath": ".",
"share": true
}Without share: true the tool uploads nothing and returns a deep link that only opens on your machine. Without a token, sharing fails with a login hint and the local preview still succeeds, so a missing token never costs you the build.
From the console. Open the build in the console and share it from there.
From the API. The upload the agent performs is a POST to /api/artifacts with a project token (see Access tokens), carrying the dist/ files inline. It answers with the previewUrl, the zipUrl, the revokeUrl and the expiresAt.
Not the same as /api/cli/publish
POST /api/cli/publish is a different verb. It uploads nothing. It takes a buildSha your project has already built and returns a page on your workspace host, https://<workspace>.extension.dev/<project>/builds/<sha>, with a short-lived ?share= token appended when the project is private. Its ttlHours (1 to 168, default 24) is the lifetime of that token, not of a preview link. See Public build pages.
What the reviewer sees
They land on preview.extension.dev, which exists to be this door and nothing else. The page fetches the build, opens a session in the sandbox, and renders the running extension together with a summary of what it is: the name and version, how long the link has left, and every permission and host the manifest declares.
Be clear about the ordering, because it is the opposite of what a security notice usually implies. That summary is drawn from the manifest of a build that is already loaded and already running. It is a description, not a consent step. There is no prompt, nothing to accept, and nothing to decline, and a reviewer who reads the permission list and dislikes it is reading about code that has already started.
That is deliberate, and the reason it is safe to do is that the disclosure was never what protected them. The sandbox is. The build's real code executes on a separate origin, in a frame the page cannot reach into and that cannot reach the reviewer's browser profile, cookies, or installed extensions. Reading the permission list earlier would not change what the code can do.
Reversing that would mean promising a boundary the ordering does not supply, so the boundary itself is enforced instead: a shared build now runs isolated or it does not run at all. When the isolated origin is unavailable, the page refuses, says the sandbox is unavailable and that nothing was loaded, and stops. It used to quietly fall back to serving the build in the page's own renderer and explain afterwards, which was an apology for a decision the reviewer had never been offered.
That refusal applies to shared links only. Dragging in a build from your own disk, or loading one through your dev server, still falls back to in-page serving and tells you it did, because those bytes were already yours.
Once it is running, the surfaces the manifest declares, the popup, options page, side panel, background worker and content scripts, render in the frame, and a Trace tab lists every chrome.* call the extension makes while they use it, which is usually the fastest way for a reviewer to describe a bug precisely.
The trade
A share link hands over the built code, not just a rendering of it. zipUrl serves the whole uploaded dist/ as a .zip, unauthenticated, to anyone holding the link, for as long as the link is live.
That is normally what you want for a review, and it is exactly what a zip over chat would have done. But do not treat the link as a viewing-only wall, and do not share a build whose bundled code you would not hand out.
The uploader drops .env files and .pem, .key, .p12, .pfx and .keystore files anywhere under dist/ before anything leaves the machine. That is a narrow net for the common accident, not a secret scanner. A key pasted into a bundled .js file ships.
How long it lives
A shared preview lives 30 days. That is a fixed platform window, not a plan benefit, and it is the same on Free and on Pro.
After it expires the link stops resolving, and the sweep deletes both the stored build and its record.
Re-sharing does not mint a second link. Uploading the same bytes from the same project while the link is still live returns that same link, and doing it after the link has expired reuses the same address with a fresh 30 day window, which is the closest thing to an extension there is. Only a revoked address is replaced, and it is replaced by a different one.
How to kill it
DELETE the revokeUrl with the same token, or ask the agent: extension_shares with action: "revoke". Any URL of the share pasted in is enough to name it.
WARNING
Revocation is permanent, and it stays permanent.
Revoking deletes the stored build immediately and marks the record revoked. The record itself is deliberately kept, as a tombstone, and the sweep leaves it alone forever instead of tidying it away. That matters more than it sounds: a share id is derived from the build's own bytes, so if the tombstone were ever collected, re-uploading the same build would land back on the exact address that was revoked and silently restore access for everyone still holding the old link. The surviving record is what stops that. Each revocation is mixed into the derivation, so the next share of the same bytes gets a different address, and repeated revocations keep moving it.
You get a new link. Everyone holding the old one gets nothing.
Manage shares covers finding, listing, and revoking in full.
Two more doors
Nothing uploaded. Drag a build folder onto preview.extension.dev, or use the folder picker. The build runs in the same emulator, loaded straight from disk, and no byte leaves the machine. Good for looking before deciding to share.
A real browser. Append ?lane=real and the preview forwards chrome.* calls to a Live Preview carrier extension in an actually installed browser. Where Chrome refuses a call, the lane says so instead of faking an answer.
What a share can hold
| Limit | Value |
|---|---|
Files in dist/ | 2,000 |
| Payload, text-heavy build | about 64MB |
| Payload, binary-heavy build | about 48MB |
| Link lifetime | 30 days |
The two payload numbers are one ceiling, not two. The cap counts encoded characters, and files that are not valid UTF-8 text travel base64-encoded, which costs four characters for every three bytes. So a build of images, fonts and wasm spends its budget a third faster than a build of JavaScript and JSON, and runs out at roughly 48MB of actual bytes.
A refusal reports what it measured in the units you can act on, so it names your build's encoded size and both ceilings rather than quoting the raw cap at a build that never approached it.
When it fails
- Sharing fails with a login hint: the token is missing or lapsed. The local preview still succeeded; mint a token and share again.
- The link answers that it expired or was revoked: that is the whole message by design. The viewer never sees your extension's name. Re-share for a fresh link.
- The upload is refused for size: read the number in the message, not the ceiling. Source maps are the usual cause on a text-heavy build, so build without them; bundled media is the usual cause on a binary-heavy one.
- The reviewer is told the sandbox is unavailable: nothing loaded, and nothing about your build is wrong. The isolated origin was unreachable and a shared build will not run without it. Ask them to retry.
Next
- Manage shares for the record, the list, and the revoke
- Public build pages for a build anyone should find, rather than one person
- Inspect a published extension for the same panels pointed at a store listing
