-
Remote control shares the browser, not one tab — a remote agent connected with
playwriter session new --remote <id>now gets the same access as a local agent. It sees and drives every tab where Playwriter is enabled, and opens new background tabs withcontext.newPage()in the session'sremotetab group.context.pages()[0]is still the tab where sharing started.playwriter session new --remote <id> playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://example.com')"
Whole-profile cookie APIs (
Network.getAllCookies,Network.clearBrowserCookies,Storage.*Cookies) andNetwork.clearBrowserCachestay blocked, so an agent cannot wipe every login by accident. Remote control is still not a sandbox: share the id only with agents you fully trust.Stop sharing works from the Remote ON dropdown of any Playwriter tab and closes the tunnel immediately. Tabs the remote agent opened are detached and left open. Sharing also ends when the tab where it started is closed. Remote sessions only regroup or rename tabs they opened, and never touch the tabs of a local session with the same id.
-
Breaking: no default
pageglobal, Playwriter never opens tabs on its own — before, every execute call made sure a default page existed and openedabout:blankwhen no tab was open, even when your code never usedpage. Now readingpagethrows with a hint, the relay no longer auto-creates a tab, headless and direct CDP sessions start without a tab, andPLAYWRITER_AUTO_ENABLEis removed. Store your own page instate:// new tab state.page = await context.newPage() await state.page.goto('https://example.com') // a tab the user already opened (last match = most recently opened) state.page = context.pages().findLast((p) => p.url().includes('example.com')) await snapshot({ page: state.page })
Helpers now need an explicit page:
snapshot(also accepts alocatororframe),getLatestLogs,waitForPageLoad,refToLocator,screenshotWithAccessibilityLabels,getPageMarkdown,ghostCursor.*,recording.*,stream.*, andcloud.sendCookies({ from }).playwriter stream startstreamsstate.page;stream stopandstream statusalso work after a session reset.resetno longer reports a current page URL. -
connectViaExtension()closes only its own tabs, supportsawait using, and can keep tabs — the connection closes automatically at scope end, also when code throws.close()closes only the tabs that connection opened, plus popups they opened, so two SDK scripts or an SDK script next to a CLI session no longer close each other's tabs. PasscloseTabsOnEnd: falseto keep the tabs, so a later process can reuse them:import { connectViaExtension } from 'playwriter' // process A await using a = await connectViaExtension({ tabGroup: 'job', closeTabsOnEnd: false }) await (await a.browser.contexts()[0].newPage()).goto('https://example.com') // process B, later await using b = await connectViaExtension({ tabGroup: 'job' }) const page = b.browser.contexts()[0].pages().findLast((p) => p.url().includes('example.com'))
await usingneeds Node 24+, or TypeScript 5.2+ / tsx. Tab cleanup needs the latest extension; older extensions leave the tabs open and log a warning.deleteRelaySession({ sessionId, closeCreatedTabs: true })exposes the same cleanup for custom clients. Popups opened from a session's tab now belong to that session, sosession update --tab-groupmoves them too. After a relay restart, a new session1never owns or closes the tabs of an older session1.The recorder instructions and
playwriter skillnow show how to turn a recording into a standalone typed SDK withconnectViaExtension. See https://playwriter.dev/docs/sessions#node-api. -
Pick a browser with
selectBrowserandlistBrowsers()— when the extension runs in more than one browser or profile, choose which oneconnectViaExtension()controls:import { connectViaExtension, listBrowsers } from 'playwriter' await using connection = await connectViaExtension({ selectBrowser: (browsers) => { // [{ id, browser: 'Brave', email: 'me@work.com', activeTabs, playwriterVersion }] return browsers.find((b) => b.browser === 'Brave' && b.email === 'me@work.com') }, }) // same list, without connecting const browsers = await listBrowsers()
idis stable across restarts, so you can save it. WithoutselectBrowser, connecting with several browsers fails with a list of connected browsers. -
Anonymous usage stats — the local relay sends event names, a random install id, the Playwriter version, OS, session kind, and hourly execute counts to Strada. It never sends code, URLs, page content, emails, or hostnames. The CLI prints a one-time notice. Opt out, then restart the relay:
export PLAYWRITER_TELEMETRY=0 # or DO_NOT_TRACK=1 playwriter serve restart
-
One clear "extension not connected" error —
playwriter session new,playwriter -eandconnectViaExtension()print the same message, written so an agent can pass the install steps to the user. The text and the install URL are exported asEXTENSION_NOT_CONNECTED_ERRORandPLAYWRITER_EXTENSION_URL. -
Simpler extension welcome page — leads with a short blurb and a "Copy agent prompt to install" button that copies a prompt telling your coding agent to install the Playwriter skill. Full setup docs moved into a collapsible section. The tutorial page is shorter.
-
Fixed agent clicks blocked by the in-page toolbar —
locator.click(),hover(),tap()anddragTo()on elements under the toolbar no longer fail with<div data-playwriter-toolbar> intercepts pointer events. Toolbar buttons are no longer focusable, so agentTab,EnterorSpacepresses can never trigger them. Needs the latest extension. Fixes #126. -
Fixed
PLAYWRITER_TOKENbeing ignored by auto-started relays — the relay that the CLI and MCP start in the background now enforces the token on privileged requests, the same asplaywriter serve. Fixes #119. -
Fixed enabling a tab while the extension reconnects — the tab was attached twice, failed with
Another debugger is already attached, and later commands such asrecorder startfailed withNo tab found.