These CDP commands are profile-wide destructive operations that wipe ALL
cookies/cache across every domain in the user Chrome profile, not just
the current page. A previous incident wiped Gmail, GitHub, and every
authenticated session.
Changes:
- cdp-relay.ts: added blocklist at top of routeCdpCommand() that throws
a descriptive error if an agent or script tries to send either command.
The error message explains the safe alternative (Network.getCookies +
Network.deleteCookies for per-domain cookie clearing).
- skill.md: added prominent warning in the cookies section with the
correct per-domain cookie clearing pattern using getCookies/deleteCookies.
- MEMORY.md: documented the incident for future sessions.
Session: ses_12890788fffeo527ik9Os483KR
Reviewer flagged the loopback-bypass added in 0.1.5 as a full auth
bypass under the recommended remote-access topology: tunnel agents
(traforo, ngrok, cloudflared) run as local processes and forward
public traffic to localhost:19988, so every public request reaches
the relay from 127.0.0.1. The bypass made `/cli/execute` (RCE),
`/cli/session/*`, `/recording/*` (file writes via outputPath), and
`/mcp-log` callable by anyone with the public tunnel URL.
Fix
---
1. cdp-relay.ts: privilegedRouteMiddleware now requires the token on
every request, regardless of source. Loopback bypass removed.
2. cli.ts (`serve` action): exports `process.env.PLAYWRITER_TOKEN` at
startup so in-process callers can attach Authorization: Bearer ...
when calling the relay's own privileged endpoints.
3. screen-recording.ts: new `recordingHeaders()` helper reads
PLAYWRITER_TOKEN from env and attaches the Bearer header to all
four /recording/* calls (start, stop, status, cancel). Mirrors
what mcp.ts already does for /mcp-log.
4. cli.ts (`executeCode`): refactored the inline header-build into
the shared `buildAuthHeaders` helper so the six /cli/* fetch
sites stay in sync.
5. connect-cdp-demo.ts: replaced the hardcoded 'secret_token'
fallback with a hard error so the demo can never accidentally be
the source of a leaked credential warning.
Bumps playwriter to 0.1.6 + CHANGELOG entry. Adds a MEMORY.md note
so the next person doesn't re-introduce the bypass.
Session: ses_20d25dcc7ffeJhVK7aC8Zh7OkW
The Chrome extension now injects the ghost-cursor-client.js bundle into
every attached tab via CDP Page.addScriptToEvaluateOnNewDocument +
Runtime.evaluate. The cursor auto-enables on load (top frame only, skips
iframes) and stays on the last spot Playwright clicked or moved to.
Key changes:
- Extension injects cursor bundle via ?raw vite import, inlined into
background.js at build time. Torn down on detachTab.
- RecordingGhostCursorController renamed to GhostCursorController with
attachToPage/detachFromPage instead of enableForRecording/disableForRecording.
Single instance on PlaywrightExecutor, wired in setupPageListeners.
- Recording no longer toggles the cursor on/off.
- 5s idle debounce: cursor fades out (600ms) after no activity, wakes on
next action by teleporting to the new target and fading back in (140ms).
- Animation polish (Emil Kowalski guidelines): easeInOutCubic for moves,
dedicated 140ms press duration with strong ease-out, subtler 0.95 press
scale, transform-origin anchored to cursor hotspot.
- Slower cinematic defaults: 220ms min, 1500ms max, 1.2 px/ms.
- Test: verify cursor element exists on attached tab without explicit enable.
Closes #84\n\nThe CLI started importing browser-start helpers at module load time, which\npulled @xmorse/playwright-core into generic startup paths like\nplaywriter --help and playwriter serve. In clean installs that immediately\nreached the vendored extract-zip runtime, which still uses CommonJS\nrequire('get-stream'), and crashed with ERR_REQUIRE_ESM once the fork had\nget-stream v9 installed.\n\nThis change fixes the failure from both sides:\n- lazily load browser-start-only modules so normal CLI startup/help paths\n do not pull playwright-core unless browser start is actually invoked\n- bump the playwright submodule to @xmorse/playwright-core 1.59.8, pinning\n get-stream back to ^5.2.0 so the vendored unzip runtime stays compatible\n- add a black-box regression test that runs root help and serve help\n through vite-node\n- update versions, changelogs, lockfile, and local memory notes\n\nVerified with pnpm typecheck, a direct require of playwright-core, and\npnpm vitest --run src/cli-help.test.ts.\n\nNote: lintcn still reports unrelated pre-existing warnings/errors in\ncli.ts, cdp-relay.ts, aria-snapshot.ts, and mcp.ts that were not part of\nthis fix.
Chrome popup windows (window.open with features, OAuth login popups)
could not be controlled by Playwriter because chrome.debugger does not
attach to separate popup windows. Agents had to work around this with
cmd+click (`{ modifiers: ['Meta'] }`) to coerce new tabs instead.
The extension now listens for chrome.windows.onCreated with
type === 'popup' and relocates the tab(s) into the source tab's window,
then auto-attaches Playwriter so the new tab appears in context.pages().
Gating: the popup is only relocated when the source tab is already
Playwriter-connected. Otherwise the popup is left alone so unrelated
sites keep their normal Chrome popup behavior. Source tab is discovered
via chrome.webNavigation.onCreatedNavigationTarget because
chrome.tabs.Tab.openerTabId is unreliable for window.open popups on
Chromium 145 (leaves it null).
Focus is not stolen — the relocated tab is appended at the end of the
source tab's strip (index: -1) but the user's active tab stays active.
Executor changes
- Replaced setupPopupDetection with setupNewPageLogging. Warning
message changed from "Popup window detected ... cannot be controlled"
to "New page opened from current page (index N, initial url: ...)"
pointing the agent at the controllable tab.
- Added MIN_EXTENSION_PLAYWRITER_VERSION check so agents running with
old extensions see a targeted warning to update via chrome://extensions.
- enqueueWarning is now called from warnIfExtensionOutdated so the LLM
sees version-skew messages in its warning stream, not just stdout.
Extension changes
- New `webNavigation` permission for the source-tab correlation map.
- connectTab now verifies `chrome.tabs.get(tabId)` before writing an
`error` state, preventing dead tabs from leaking into store.tabs /
badge counts / group sync when a tab closes mid-attach.
- popupSourceTabMap entries are cleaned up both on a 10s timeout and
when chrome.tabs.onRemoved fires, capping memory for plain-new-tab
cases that never trigger windows.onCreated.
Tests
- popup-relocation.test.ts verifies both paths: when a Playwriter tab
is connected the popup is relocated into the main window and appears
in context.pages(); when no tab is connected the popup-type window
remains (extension does not interfere).
Skill docs
- Removed section #10's cmd+click workaround; added a note about
auto-relocation under "working with pages".
Versions
- extension 0.0.79 -> 0.0.80
- playwriter 0.0.103 -> 0.0.104
When a single-expression code snippet auto-returns a Playwright object
(e.g. `await page.goto(url)` returning a Response), the CLI previously
called util.inspect() at depth 4 on the handle, producing 400K+ chars
that included every process.env variable (API keys, tokens, secrets).
Even with the playwright-core util.inspect fix (@xmorse/playwright-core@1.59.7),
these handles are useless output — they're programmatic references with no
display value. Agents and users get nothing meaningful from seeing:
[return value] Response@response@abc123 { request: Channel<...>, ... }
Fix: add isPlaywrightChannelOwner() duck-type check in the executor return
value path. Handles are silently skipped (duck: _type + _guid + _connection).
The existing 'Code executed successfully (no output)' fallback applies when
there's no other output.
To see data, users must explicitly:
return response.url() → prints the URL string
return response.status() → prints 200
console.log(response) → prints safe summary via playwright-core
Also includes:
- Regression test (channel-owner-inspect.test.ts) for inspect handler
- isPlaywrightChannelOwner() unit tests (executor.unit.test.ts)
- Stale color-scheme snapshot fix (relay-core.test.ts)
- MEMORY.md with learnings (Node util.inspect bypass of Proxy traps)